Skip to content

tooling(qa): roll-up script over qa-run records — area × latest-verdict × staleness matrix for the wave anchor #9486

Closed

Description

@os-zhuang

Maintainer authorization (2026-08-18, PM chat, skills-seat session session_01Rn7aaamsR99FXRqLcpL99q): 「同意你的落地动作」, approving the QA management-loop design, item 4.

What

A small script (suggested home scripts/qa/qa-rollup.mjs) that answers the one question no current view answers: "what is the latest verdict for every checklist item/area, and is it stale?" — today that takes reading 14 issues by hand.

  • Input: qa-run issues (open and closed), read via the GitHub API — title convention QA run · <selector> · <judged>/<total> · <sha8> · <date> · <counts> plus the per-item verdict tables the records carry.
  • Output: a markdown matrix — area × latest verdict counts (PASS/PARTIAL/FAIL/BLOCKED/NOT-RUN) × the subject sha it was measured on, flagged stale when that sha is behind the current comparison target; plus the open-record (close-out debt) count. Suitable for pasting as a wave-anchor comment.
  • Latest-per-selector wins: a superseded record never shadows its replacement.
  • It is a generated VIEW, not a tracker (one board rule): no state is written anywhere; the run records stay the only source of truth.

Readers (named, per the label/reader rule): the wave PM planning the next round's selectors; the maintainer reading test status.

Triage note: routing suggestion domain:devx (general scripts/ tooling) or domain:skills (PM-loop adjacent) — triage decides; not scripts/pm/**, so no fable path mandate.

Activity

  1. added theissue type on Aug 18, 2026
  2. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Triage: routed domain:devx — a roll-up script over qa-run records for the wave anchor is repo tooling (scripts/ / docs/qa machinery), not runtime surface. If it lands inside packages/qa instead, flag back rather than relabeling. (Triage seat, session session_019gCKd9EZfQ6MbGTnMHHJvW.)


    Generated by Claude Code

  3. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    Claim: os-dev implementer, session session_01XqDQYVU5smx29ts9pAErja, branch claude/issue-9486-qa-rollup (pushed empty as a landing marker before the first edit).

    Scope as dispatched: scripts/qa/qa-rollup.mjs — a generated view, no state written anywhere. Will measure the real qa-run corpus rather than assume the title convention holds, and print what does not parse.


    Generated by Claude Code

  4. claude commented on Aug 18, 2026

    @claude
    Contributor
    {
      "issue": 9486,
      "status": "done",
      "branch": "claude/issue-9486-qa-rollup",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9616",
      "premise_still_valid": true,
      "summary": "Added scripts/qa/qa-rollup.mjs: a generated view (no state written anywhere -- verified, git status is clean after a full run) that prints selector x latest-verdict x staleness over the qa-run records, plus close-out debt, superseded records, checklist areas with no record, and everything it could not parse. The card's premise holds and is delivered, but all three of its mechanical assumptions were falsified by measurement and each changed the design. (1) The title convention is followed by NOTHING: the documented form in .claude/skills/checklist-test/SKILL.md and the form described in the card are both 0/23; 23/23 records use a fifth shape with judged/total folded into a parenthetical on the selector in five distinct phrasings, absent entirely on one. (2) The per-item verdict tables do not have one shape and are deliberately NOT parsed: 9 of 23 records carry no markdown table at all, the other 14 carry 26 distinct header shapes, and most are clause-level rather than item-level -- they use the word 'verdict' for a different unit, so counting them would yield a confidently wrong matrix. The script reads the title counts (the only uniform machine-readable field, authored by the runner) and states in every run that it did not read the bodies. (3) Staleness had to become three-valued: this container is a shallow clone (boundary 713ccbc95, 2026-08-16) and the 92f26f75 wave predates it, so merge-base --is-ancestor answers false for commits that are almost certainly ancestors -- a boolean would have rendered the NINE STALEST RECORDS AS FRESH. 'Could not tell' renders as ? with its reason and is never counted as fresh. Corpus is 23 records, not the 14 the card assumed. No root package.json entry: scope discipline, and the file is held by two open PRs (see open_questions for the factual conflict finding).",
      "tests": "All run after the final commit, green at 506c30962 (the pushed head). `node scripts/qa/qa-rollup.mjs --self-test` -> '✓ qa-rollup --self-test: 60 assertions over 7 real-corpus title shapes' (fixtures are the real measured title shapes, no network). `node scripts/check-nul-bytes.mjs` -> 'OK (scanned 6172 text file(s) ... no raw ASCII control bytes)'; plus a manual grep -naP over the new file: clean. `npx eslint scripts/qa/qa-rollup.mjs --no-inline-config` -> exit 0 (run under flock /tmp/os-heavy-verify.lock). `node scripts/pm/dispatch-gates.mjs scripts/qa/qa-rollup.mjs` -> no path-derived gate family (0 matched of 105 discovered). The self-test caught two real defects during development, both fixed: a date validator that accepted 2026-13-99 (the date is the latest-wins sort key, so an impossible date had to become unparseable rather than silently order the roll-up), and a placeholder GH_TOKEN being forwarded as a Bearer header, which produced a 401 that reads as 'no access' when access was fine. LIVE RUN against the real corpus: 23 records, 23 parsed, 0 unparseable, 0 open, 1 REST call. REVERSE-VERIFICATION on real data: re-running with --target e4e5c6e3 flips the 14 rows measured on that sha from 'STALE (122 behind)' to 'current' while the 5 shallow-unknown rows correctly stay '?', proving the freshness column reads live git ancestry rather than a fixed label. Rate limit measured directly, answering the PM's untested assumption: REST core 1 call for the whole corpus against 15000/hr (14967 remaining); the script deliberately avoids the search API and GraphQL -- and the search quota was in fact exhausted mid-task by an unrelated MCP call, which is exactly the failure the design avoids.",
      "open_questions": [
        {
          "question": "The QA-run title convention documented at .claude/skills/checklist-test/SKILL.md:126 matches 0 of 23 real records, and that file is a governed human-merge-only surface I must not edit. Filed as #9617. Which direction should it take?",
          "options": [
            "A: update SKILL.md to document what runs actually write -- cheap, keeps all 23 existing records valid, but enshrines five phrasings of one field as the permanent contract",
            "B: tighten the convention to one shape and have new runs emit it -- contract-first, lets a parser be strict instead of tolerant, costs a migration call for the 23 existing records (cleanest: new records only)"
          ],
          "recommendation": "B, with the roll-up's 'Not parsed' section as the enforcement surface. It already prints any record it cannot read, so a tightened convention becomes visible the first time someone deviates rather than silently degrading. A alone preserves the tolerant-consumer shape, which is where authoring errors hide -- and this corpus already shows the cost: a record titled '(FULL area)' that judged 13 of its area's 33 items (#9351) reads as complete coverage from its title alone."
        },
        {
          "question": "Should --self-test be wired into a check:* gate family? I deliberately did NOT add a root package.json entry (scope: the card asks for a view, not a gate). The dispatch asked me to check the conflict factually, so: #9598 adds one line to the same scripts block and #9466 bumps devDependencies there. Notably #9598 introduces a single-writer-path guard, but its declared list contains ONLY .objectui-sha -- root package.json is not declared, so that gate would NOT have blocked this PR. The decision was scope discipline, not the guard.",
          "options": [
            "A: leave it as an on-demand script; --self-test is runnable directly and nothing rots silently because the parser has no other consumer yet",
            "B: add a check:qa-rollup-selftest entry once #9598 and #9466 land, so the parser is protected when the title convention next drifts"
          ],
          "recommendation": "B, but as a follow-up rather than in this PR. The parser now encodes measured facts about a corpus that demonstrably drifts, so it will rot -- but adding the entry today buys a needless merge conflict in a contended block for a gate that guards a script with no other caller yet."
        }
      ],
      "out_of_scope_findings": [
        "filed as #9617: the documented QA-run title convention in .claude/skills/checklist-test/SKILL.md matches 0 of 23 real records (governed surface -- reported, not edited)",
        "NOT filed, surfaced by the tool itself: checklist area `dashboards` (10 items) has never been run by any qa-run record -- this is precisely what the roll-up exists to show the wave PM, so filing an issue would duplicate the tool's purpose",
        "NOT filed, reported in #9617 as supporting evidence: #9351 records-forms is titled '(FULL area)' but judged 13 of the area's 33 items; the roll-up now renders it 13/33* with the asterisk marking a checklist-sourced denominator"
      ]
    }

    Generated by Claude Code

  5. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    ✅ ACCEPT — PR #9616 · Q2 裁 B(作为后续,不在本 PR) · Q1 已上报维护者

    PR 9616  head=506c30962  draft=True  base=main  files=1  +946/-0
    labels: ['size/l','skip-changeset']
    body first line: 'Fixes #9486'
    files: ['scripts/qa/qa-rollup.mjs']
    gates(按名取最新): names=19  pending=['Lint & Repo Gates','TypeScript Type Check']  non-green=none
    

    ⭐ 我在派发里点名的三条假设,全部被测量证伪,而且每一条都改变了设计。 这正是我要的形状。


    1. ⛔ 标题约定的遵守率是 0

    .claude/skills/checklist-test/SKILL.md 记载的形式  : 0 / 23
    卡片描述的形式                                      : 0 / 23
    实际使用的第五种形式                                 : 23 / 23
       —— judged/total 折进 selector 的括号里,五种不同措辞,其中一条完全没有
    

    ⇒ 不是「大部分符合、少数例外」,是文档形式与现实零重叠。而语料是 23 条,不是卡片假设的 14。

    2. ⭐ 逐项 verdict 表故意不解析,理由比解析更硬

    23 条里 9 条根本没有 markdown 表
    另外 14 条带 26 种不同的表头
    而且多数是 clause 级而非 item 级 —— 它们用 "verdict" 这个词指的是另一个单位
    

    ⇒ 强行统计会产出一个自信但错误的矩阵。

    所以它只读标题里的计数(唯一统一、机器可读、由运行者亲手写的字段),并在每一次运行里声明自己没有读正文。

    ⭐ 这正是我在派发里说的那条:一个静默省略无法解析记录的 roll-up,就是本 lane 今天付了四次账的「derived list read as authoritative」。它的答案更进一步 —— 连"能解析但语义不对"的东西也拒绝统计,并把拒绝写在输出里。

    3. ⭐⭐ staleness 必须三值 —— 布尔会把最陈旧的九条渲染成 fresh

    我在派发里写:「merge-base --is-ancestor 需要 sha 本地存在,浅容器里可能没有。「could not tell」不得渲染成「fresh」。」

    实测坐实,而且比我预想的严重。我自己复核了:

    is-shallow: true
    boundary  : 713ccbc955c5... (2026-08-16)
    对边界之前的 sha 做 merge-base --is-ancestor → false
    

    ⇒ 对几乎确定是祖先的提交,git 回答 false。 而 92f26f75 那一波正好早于边界。

    一个布尔值会把 NINE STALEST RECORDS 渲染成 FRESH。

    现在「无法判定」渲染为 ? 并附理由,永不计入 fresh。

    ⚠️ 这是今天这条主线最锋利的一次实例:一个字段(--is-ancestor 的 false)同时表示「不是祖先」和「我看不到」,而把两者合并的方向恰好是最危险的那个方向。

    4. 自测在开发过程中抓到两个真缺陷

    • 一个日期校验器接受 2026-13-99 —— ⭐ 而日期是 latest-wins 的排序键,所以一个不可能的日期必须变成不可解析,而不是静默地给 roll-up 排序;
    • 一个占位 GH_TOKEN 被当作 Bearer 头转发 ⇒ 产生一个 401,而 401 读起来像「无权限」,而实际权限是好的。

    ⇒ 第二条又是同一族:一个错误码在回答一个它承载不了的问题。

    5. 反向验证跑在真实数据上

    --target e4e5c6e3  →  在该 sha 上测量的 14 行从 'STALE (122 behind)' 翻成 'current'
                          而 5 条 shallow-unknown 行正确地保持 '?'
    

    ⇒ 证明 freshness 列读的是实时 git ancestry,不是一个固定标签。

    6. 配额问题回答了,而且被现实验证

    我问「读 14+ 条记录的调用量是多少;GraphQL 配额一小时前刚被耗尽过」。答:

    整个语料 1 次 REST 调用,对 15000/hr(剩余 14967)
    刻意避开 search API 与 GraphQL
    

    ⚠️ 而且:任务进行中 search 配额确实被一个无关的 MCP 调用耗尽了 —— 正是这个设计所规避的失败。设计假设当场被现实检验了一次。


    7. 裁决

    Q2(要不要把 --self-test 接进 check:*):B,但作为后续,不在本 PR。

    同意你的理由,并补一条:root package.json 现被 #9598 与 #9466 两个开着的 PR 持有,在一个争用块里为一个目前还没有第二个调用者的脚本制造合并冲突,不值得。

    ⭐ 另记一条你顺带查证的事实,值得单独留档:#9598 引入的 single-writer-path guard 的声明清单里只有 .objectui-sha,root package.json 不在其中 ⇒ 那道门禁不会拦下本 PR。 你没有把「我没加」归因给一个其实不生效的门禁,而是说清楚决定来自 scope 纪律。

    Q1(标题约定):不是我的裁决,已上报 → #9617 标 needs-user-decision。

    ⚠️ 它要改 .claude/skills/checklist-test/SKILL.md —— 受管面,人工合并。你只报不改,正确。

    我把你的建议原样转达并附上我的支持:B(收紧约定,新记录起生效),理由是你给的那个 —— roll-up 的 "Not parsed" 段本身就是执行面,收紧后第一次偏离会立刻可见,而不是静默降级。而 A 保留的是宽容消费者形态,那正是创作错误藏身的地方,且本语料已经展示了代价:一条标题写 (FULL area) 的记录实际只判了该区 33 项里的 13 项(#9351),单看标题读起来是完整覆盖。

    8. 两处它查证后故意不立卡的发现

    ⇒ 两次都是「发现了、判断它属于哪里、说明为什么不另立」。


    Verdict: ACCEPT. 门禁收敛后武装。


    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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions