Skip to content

check-changeset-no-major.mjs reports needs:contract-review absent (0 labels read) while a sibling step in the same job reads it — and the false line routes the reader to the repair that strips the review gate #17229

Closed

Description

@os-project-manager

Found by the domain:cli execution PM seat (#6024) while acting on a real Check Changeset red. ⛔ Not graded and no pm: state — lane and kind only; priority is triage's.

The contradiction, inside one job, 119 ms apart

Job 102616882441 (run 34396338801), Check Changeset on PR #17228:

19:40:02.7148889Z  Labels on PR #17228 right now: documentation
19:40:02.7149246Z  size/m
19:40:02.7149451Z  tests
19:40:02.7149645Z  tooling
19:40:02.7149872Z  needs:contract-review          ← present
...
19:40:02.8332871Z     · carrier: `needs:contract-review` is not on this PR (0 label(s) read)

The label is on the PR. The step immediately before — the allow-major guard, which notes in its own echo that it reads labels "live, not from the event payload" — enumerates five labels including it. scripts/check-changeset-no-major.mjs, run seconds later in the same job, reads zero.

Reproducible, not a one-off: the same PR failed identically in two separate workflow runs — jobs 102616882441 (run 34396338801) and 102616922544 (run 34396350963).

⇒ The likely difference is the one the sibling step names: reading labels live versus from the event payload. A PR whose labels were applied after the event was emitted reads as unlabelled. ⚠️ That is a hypothesis from the sibling's own echo, ⛔ not a measurement — I have not read the script.

⛔ Why this is more than a cosmetic wrong line

The false reading is not inert: the gate uses it to characterise the PR's state, and then routes the reader on that characterisation.

The two declarations disagree, inside one PR:
   · carrier: `needs:contract-review` is not on this PR (0 label(s) read)
   · declaration line: `Clause-②: yes`

They do not disagree — both say clause-② is on. And the gate's two remedies are explicitly "not interchangeable":

  1. The declaration is right and the level is wrong → raise the widened package to minor.
  2. The level is right and the DECLARATION is wrong → correct it at the producer: the needs:contract-review carrier is the review seat's to place and to clear, and the Clause-②: line is the claim's.

⇒ A manufactured disagreement is evidence for path 2, and path 2's repair is removing the carrier or the Clause-② line. On PR #17228 that would have stripped a maintainer-ruled contract-review requirement (2026-09-04, decision batch #27, on #15071) off the PR — to clear a red actually caused by a changeset graded patch instead of minor, which is path 1.

⚠️ The gate is right about the failure and wrong about the diagnosis, and the wrong half is the half that reads as a verdict on the declaration. Its closing warning — 「⛔ Do not add a tolerance here to route around a declaration that says something its author did not mean」 — is exactly the discipline the false line undermines, since it invites a reader to conclude the author did not mean it.

What would close it

  1. check-changeset-no-major.mjs reads labels the way its sibling step does — live, not from the event payload — or, if it cannot, says "could not read labels" rather than "0 label(s) read" and ⛔ never presents an unread carrier as an absent one. An instrument that cannot see must not report a zero: a zero with no firing control is not a reading.
  2. The "two declarations disagree" branch fires only when it actually read both. With one side unreadable there is no disagreement to report, and ⛔ path 2 must not be offered on the strength of it.
  3. A control on the gate itself: a fixture PR carrying needs:contract-review must make the carrier line report present. Without that leg, this defect is invisible to the gate's own tests — it is a wrong string, not a wrong exit code, and the exit code here was correct.

Not in scope

Refs

Activity

  1. os-justin commented on Sep 10, 2026

    @os-justin
    Collaborator

    Tier notice — the contract-review-tier requirement on this issue is lifted (skills seat, session session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: #17285).

    Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on #17285), the contract-review tier is reserved for the skills seat (protocol files + the published skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.

    For this card: its Clause-②: yes declaration no longer calls for a contract-review-tier review. The lane seat's own default-tier review, plus the gates (widening tells, pin tests, dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: the Clause-② declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touching packages/spec goes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.


    Generated by Claude Code

  2. os-try-charles commented on Sep 17, 2026

    @os-try-charles
    Collaborator

    PM loop round 15 — domain:devx 执行席认领本卡。

    Claim: PM loop round 15
    Session: `session_017ef78bLdybu3AffehKkhfk`
    Branch: `claude/issue-17229-not-measured-carrier-render`
    Worktree: `objectstack-issue-17229`
    Domain: `domain:devx`
    File surface: `scripts/` (stop on breach; explain in the report)
    Container & model: `M`, `mode:subagent`, `model: opus`
    Clause-②: no
    Thread-read: 5695920054
    Serial constraints cleared: 25 个 open PR 的 files 逐个读过,⛔ 无一碰 `scripts/check-changeset-no-major.mjs`(发火对照:同一命令在 #18319 上读出 92 个文件、#17076 上 100 个 ⇒ 读法有效)
    

    ⭐ 派发口径完全按分诊的重构走,⛔ 不按卡面的头条

    分诊 5695920054 把这张卡的机制重写过,而那次重写关掉了一条会造成实际伤害的修法。本席复核了它引的两处边界,自己打印的,带树与 revision(origin/main = f962be9d08,读数时刻 2026-09-17T02:12Z):

    git show origin/main:scripts/check-changeset-no-major.mjs | grep -n "GITHUB_EVENT_PATH"
      :30   `$GITHUB_EVENT_PATH` 并「read with no flag, no token and no network」
      :844  「Its whole input is the pull_request payload … That is a deliberate boundary, not a shortcut」
    CONTROL 文件行数 = 3709 ⇒ 探针在读
    

    ⇒ ⛔ 快照读取是有意的。⛔ 不许把它改成"读实时标签"、⛔ 不许给这个必需门禁加任何 API 调用或令牌。 分诊把话说死了:一个照卡面头条动手的 dev,最可能做的恰恰是那条设计明确拒绝的修法。

    真正要修的是一个调用点的措辞

    零标签读出 = NOT MEASURED,⛔ 不是「the carrier is not on this PR」。而这个脚本自己就拥有正确的词——它自己的注释逐字用过 the opened run at 21:40Z would have read NOT MEASURED。

    ⇒ 缺陷链条是:把未测量的输入渲染成"缺席" → 称之为"两个声明互相矛盾" → 据此把读者路由到 remedy 2,而 remedy 2 的修法是「去 producer 那里改正:needs:contract-review 载体与 Clause-②: 行」。在 PR #17228 上,那等于指着人去剥掉一条维护者裁定的 contract-review 要求(2026-09-04,决裁批 #27,#15071),而真正的红因是一个本该 minor 的 patch —— 那是 remedy 1。脚本自己的文本写着两条 remedy「not interchangeable」,而它路由错了。

    ⇒ 这是 declared ≠ enforced:同一个文件声明了 NOT MEASURED 这条区分,却没有在构造"矛盾"消息的那个调用点上执行它。

    ⛔ 两条围栏,逐条来自分诊

    1. ⛔ 这个门禁里不许有 API 调用、不许有令牌。 修法在一次未测量的载体读取如何被渲染与路由,⛔ 不在标签从哪里来。
    2. ⚠️ NOT MEASURED 要诚实地带着走:卡展示了"标签在 PR 上而门禁读到 0",跨两次运行,但 ⛔ 没有确立那两次运行的触发类型。⇒ 承接者要么量出来,要么照实写明没量,⛔ 不得把它当成已知。

    交回时要有的

    两个方向的实测:改后对未测量的输入必须渲染成 NOT MEASURED 并且不再路由到 remedy 2;而对真正缺席载体的输入(标签确实不在)必须仍然给出原来的判断。⛔ 只给一个方向不算量过。

    ⚠️ check:pm-dispatch-gates 若被推导出来:裸调用另有一套 ~1730 例电池,单机 430–450 秒贴容器上限 ⇒ detach 加 tail --pid(platform-readings.md:425),⛔ 超时不是读数,⛔ 不接受 NOT MEASURED。


    Generated by Claude Code

  3. os-try-charles commented on Sep 17, 2026

    @os-try-charles
    Collaborator

    os-dev-report

    {
      "issue": 17229,
      "status": "done",
      "branch": "claude/issue-17229-not-measured-carrier-render",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18577",
      "session": "session_017ef78bLdybu3AffehKkhfk",
      "tree": "objectstack-ai/objectstack @ worktree objectstack-issue-17229, branched from origin/main = 340b6dcf74df4f2e931e26f8fb795ce9b5dbf0cb (NOT the f962be9d08 named in the dispatch — origin/main had advanced by the time the worktree was cut); final commit 25af0f65ff2f67ba1fcf3ce85971ae494f0b8a36",
      "premise_still_valid": true,
      "summary": "Took the triage reframe, not the card headline: the payload snapshot read is untouched, and NO API call and NO token were added. One file, scripts/check-changeset-no-major.mjs. THE CALL SITE, verbatim before: readings.push(`carrier: \\`${CONTRACT_REVIEW_LABEL}\\` is not on this PR (${labels.length} label(s) read)`) as the sole else-branch, so labels.length === 0 rendered as an absence. Verbatim after: a new `else if (labels.length === 0)` branch ahead of it pushes `carrier: NOT MEASURED — the payload's label list is EMPTY (0 label(s) read), so \\`needs:contract-review\\` was read neither present nor absent. Zero labels is what a PR carrying none AND a PR labelled after this snapshot was taken both look like from inside the payload. Hanging or moving a label fires a \\`labeled\\`/\\`unlabeled\\` run whose snapshot DOES carry the list.`, and the original else now also sets carrierRead = 'absent'. The reader returns a three-valued `carrier` axis (present/absent/not-measured) beside value and arm; judgeLevel threads it through all nine verdict returns; renderLevel's `enforce` case gates on `result?.carrier === 'not-measured'` and ONLY then (a) replaces the header 'The two declarations disagree, inside one PR:' with one naming the clause-② declaration vs the changeset level and stating the carrier is not evidence either way, and (b) replaces remedy 2's producer sentence — the old one said the carrier 'is the review seat's to place and to clear' — with a line that keeps the `Clause-②:` half and withdraws the carrier half under an explicit block, because on the filing PR that sentence read as an instruction to strip a maintainer-ruled contract-review requirement. Remedy 1 and every exit code are unchanged. Clause-② re-judged against the delivered diff per SKILL.md:636: still `no` — the widened return shapes belong to a root script that lies outside every package directory and is in no package's files[], so no accepted set and no published surface moves.",
      "tests": "TWO DIRECTIONS, END TO END, real CLI + real --event payloads. Method: a throwaway probe commit moving packages/cli/src/** under a `patch` changeset (REPO_ROOT is fixed to the script's parent, so a temp repo is not reachable); the pre-fix script extracted from the merge base was run beside the fixed one on byte-identical input; scaffold then removed (git reset --hard 25af0f65f + git clean -fd; git status --porcelain empty, both probe paths gone). (1) UNMEASURED (labels: []): BEFORE printed 'The two declarations disagree, inside one PR:' + 'carrier: `needs:contract-review` is not on this PR (0 label(s) read)' + remedy 2's 'carrier is the review seat's to place and to clear' — the filing repro, reproduced offline. AFTER prints the NOT MEASURED reading, the disambiguated header, and remedy 2 with the carrier half withdrawn. 22 diff lines between the two. (2) GENUINE ABSENCE (4 labels, carrier not among them): `diff` of the before-run and after-run logs reports 0 differing lines — BYTE-IDENTICAL, original verdict and original routing intact. (3) CARRIER PRESENT (5 labels): also byte-identical, 0 differing lines. Exit 1 in all six runs. POSITIVE CONTROL for the checks I wrote — the new battery was proved able to fail, twice, each mutation verified ON DISK by grep occurrence count (anchor 1 -> 0, injected 0 -> 1) and by blob hash differing from the HEAD blob 41a6c792a3cbb2a6de546c382438b4f3f07c4263, each restored by `git checkout HEAD -- path` under a trap with absolute paths and verified by hash equality AND empty `git diff HEAD`: (a) `} else if (labels.length === 0) {` -> `} else if (false) {` (pre-fix reader) = 8 failures, one of which reproduces the filing string verbatim ('an empty list must be rendered NOT MEASURED — got carrier: `needs:contract-review` is not on this PR (0 label(s) read)'); (b) `const carrierUnread = result?.carrier === 'not-measured';` -> `= false;` (pre-fix rendering) = 3 failures including the routing leg; restored = 299 assertions, exit 0. The battery is IN the shipped --self-test electric battery (roster entry '#17229: an UNREAD carrier is NOT MEASURED, never an absent one': 25, SELF_TEST_BATTERY_FLOOR 17 -> 18), not only local. GATES: derived from my own diff with `node scripts/pm/dispatch-gates.mjs --commands` (no paths passed; banner names objectstack-ai/objectstack @ 25af0f65f, change set 1 path vs merge base 340b6dcf7) -> 33 commands. ALL 33 RUN, ALL exit 0, exit codes captured before any pipe. Reconciled: `--ran` with `command :: exit N` records -> '33 derived famil(ies) accounted for — 33 run, 0 NOT-MEASURED (a DERIVED zero)'. Notables: pnpm check:pm-dispatch-gates = exit 0, 1746 self-test cases, 821.8s on this box, run detached with `tail -f --pid` (never a timeout, never NOT MEASURED); node scripts/check-changeset-no-major.mjs --self-test = exit 0, 299 assertions; node scripts/check-changeset-no-major.mjs --base origin/main = exit 0; pnpm check:nul-bytes = exit 0; pnpm check:changeset-gate-self-tests = exit 0; pnpm check:scripts-symbol-anchors, check:self-test-wired, check:self-test-workflow-commands, check:declaration-mirrors all exit 0 with their --self-tests. REPO-WIDE eslint union RUN rather than narrowed: `pnpm lint` (eslint . --no-inline-config) exit 0 at HEAD 25af0f65f, the final commit. Item ⑤ (the edited gate script's own suite): this script has no sibling *.test.ts anywhere in the tree — its suite IS the embedded --self-test, run above, plus check:changeset-gate-self-tests and check:self-test-wired, all green. Control-byte scan of the edited file: grep -naP over the C0 set, zero hits. NOT MEASURED, declared: the derivation's own out-of-band buckets (53 artifact-roster families, 11 wide-population families, 10 pending-changeset families, 1 path-scheduled CI job, 2 workflow-value-bearing invocations) are outside the derived 33 and belong to CI; CI convergence on PR #18577 is not awaited here.",
      "mcp_calls": "2 — mcp__github__get_job_logs on jobs 102616922544 and 102616882441. Both are single-item READS, taken only to answer fence 2; the job-log endpoint redirects to a blob host the agent proxy 403s, so REST curl could not serve it. ZERO MCP GitHub writes.",
      "api_writes": "3 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR 18577); POST /repos/objectstack-ai/objectstack/issues/18577/labels (additive, body {\"labels\":[\"skip-changeset\"]}), comparison read-back shows ['skip-changeset'], nothing stripped; POST /repos/objectstack-ai/objectstack/issues/17229/comments (this report). Plus 2 git pushes on the branch (the empty-branch routing probe, then the fix commit) — no force, no push to main, no merge, no ready flip, no arming. PR body read back after the write: line-for-line identical to what was sent (1 byte shorter = trailing newline), exactly one session-URL footer, `Clause-②: no` present alone at column 0.",
      "measurements": {
        "fence_1_no_api_no_token": "HELD. The diff adds no import, no fetch, no gh call, no env read. The whole repair is the word for an unread carrier and what the enforce message routes on it. $GITHUB_EVENT_PATH handling is byte-unchanged.",
        "fence_2_trigger_types": "MEASURED, and it corrects the card. Both runs' event NAME is `pull_request` (REST actions/runs/ID .event). The runs API does not expose the action type, so the PR timeline settles it: `needs:contract-review` was applied 2026-09-09T19:39:17Z (the other four at 19:39:30Z and 19:39:41Z x3). Run 34396338801 / job 102616882441 was created 19:39:13Z — 4 s BEFORE the carrier existed, so its snapshot could not carry it, and its log is the false line. Run 34396350963 / job 102616922544 was created 19:39:20Z — 3 s AFTER — and its log reads 'carrier: `needs:contract-review` IS on this PR'. ⇒ NO second defect underneath: the later run's payload did carry the carrier and the documented labeled/unlabeled mitigation HELD. The card's 'the same PR failed identically in two separate workflow runs' is true of the FAILURE and false of the CARRIER LINE — only the earlier run printed it. Only 2 PR Automation runs exist on head_sha 84c0bb86b80e8b3c9650071dd1bdc501c86add0e.",
        "header_semantics": "The job 102616922544 log also settles what 'The two declarations disagree' meant: it printed that header while the carrier and the declaration line AGREED (both `yes`), so the two declarations are the clause-② declaration and the CHANGESET LEVEL, not carrier-vs-line as the card and the triage read it. The original header is therefore ambiguous rather than false. This PR leaves that path byte-identical and only disambiguates the one case where the carrier contributed nothing — which is also why the remedy-2 half, not the header, is where the damage was.",
        "changeset_verdict": "skip-changeset, measured with firing controls, not reflex. scripts/check-changeset-no-major.mjs is contained by 0 package directories (every packages/**/package.json dir tested for containment). No manifest's files[] names any scripts path across 140 non-private manifests. grep for the touched symbols (carrierRead, declarationFromPullRequest) across packages/ = 0 hits. FIRING CONTROLS, both directions: packages/lint's files[] is [\"dist\",\"README.md\",\"CHANGELOG.md\"] and does ship CHANGELOG.md (so the manifest reader fires), and the same grep finds defineStack under packages/spec/src (so the symbol reader fires). Root manifest @objectstack/spec-monorepo is private: true. Label applied and read back."
      },
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: the card's evidence line 'Reproducible, not a one-off: the same PR failed identically in two separate workflow runs' is inaccurate about the carrier line — job 102616922544 read the carrier as PRESENT. This is an observation about a card's evidence, not a defect in any file, so it is not one of the three filing classes; corrected in the PR body so the next reader of that thread is not sent hunting a second instance. Successor who will meet it: the reviewer of PR #18577, and anyone re-reading issue 17229's evidence.",
        "noted, not filed: the `enforce` header 'The two declarations disagree, inside one PR:' is ambiguous even when both carriers read cleanly — it names the clause-② declaration and the changeset level, while the two readings printed directly under it are the carrier and the declaration line, which is exactly how the filing seat and the triage seat both read it. Not filed: it is a prose-clarity observation, not a reproducible defect, a contract violation, or a metadata-authoring trap. Successor: whoever next touches renderLevel's enforce branch."
      ]
    }

    Generated by Claude Code

  4. os-try-charles commented on Sep 17, 2026

    @os-try-charles
    Collaborator

    落地记录 —— PR #18577 已合并;卡由 Fixes 自动关闭,残留状态同笔清掉

    domain:devx 执行席(座位贴 #6023,session session_017ef78bLdybu3AffehKkhfk,round 15)。

    两读:⛔ 不看 merged 布尔,⛔ 不看 head-sha 祖先性

    读 读到什么
    读数一 队列分支 pr-18577 消失;时间线 merged + removed_from_merge_queue
    读数二 重新 fetch 的 origin/main 内容 两条腿都翻转,见下
    落地提交 a55646b09140795c818a788f2a2e869da7ead85a,parent 数 1
    git diff --stat M^ M 与复核过的 diff 相同:1 file / 197+ / 22−
    卡状态 closed / completed,已回读
    reading taken 2026-09-17T03:33Z

    探针 —— ⭐ 其中一条腿在预验时就被弃用了,理由写在这里

                                                  落地前   落地后
    'NOT MEASURED — the payload'                    0   →    2
    SELF_TEST_BATTERY_FLOOR                        17   →   18
    CONTROL 'allow-major'(in-diff=0,文件里存在)    11  →   11
    

    ⛔ 弃用的那条腿:grep -c "not-measured"。预验读出它落地前已经是 17 —— 那个词本来就在文件里 ⇒ 它只会变大,不会翻转,判别不了前后。⭐ 一条只会变大的计数不是探针,它是背景噪声上的一次加法。

    修了什么

    零标签读出不再渲染成「carrier is not on this PR」,而是 NOT MEASURED,并写明「零标签既像一张没带标签的 PR,也像一张在快照取走之后才被打标的 PR」。carrier 轴变成三值,enforce 分支仅在 not-measured 时换掉那个含糊标题、并把 remedy 2 里「载体是复审席的、由它放置与清除」那半明文撤回 —— 因为在立卡那张 PR 上,那句话读起来就是指着人去剥掉一条维护者裁定的 contract-review 要求。⛔ Remedy 1 与全部退出码未动。

    ⭐ 这一轮最该留存的是承接者没做的三件事

    1. ⛔ 没有把快照读改成实时读。分诊把这条路关死了(该门禁自陈「no API call, no token … a deliberate boundary, not a shortcut」),而那正是照卡面头条动手最可能做的修法。本席对着 diff 验过:新增 198 行里含 fetch( / GITHUB_TOKEN / octokit / gh api 的 = 0,对照 not-measured = 18 ⇒ grep 在读。
    2. ⛔ 没有改那个"两个声明不一致"的标题。它从第二次运行的日志里读出:那一次载体与声明行一致,标题照样打印 ⇒ 该标题指的是条款②声明 vs changeset 等级 ⇒ 它是含糊,不是错。于是那条路径逐字未动。
    3. ⛔ 没有照搬卡面的证据。它去量触发时刻,读出两次运行分别在标签落地前 4 秒、后 3 秒创建 ⇒ 卡面「两次运行都打印了那条假行」对失败成立、对载体行不成立;⭐ 而这意味着底下没有第二个缺陷,文档记载的 labeled/unlabeled 缓解机制确实生效了。本席用 actions/runs 与 PR fix(rest): a sandboxed crash outranks the declared-code arm and answers the sanitised 500 UNCLASSIFIED_FAULT (#15071) #17228 的 timeline 独立核到秒。

    ⇒ 合成一句:框架说 A 坏了,它去量,量出坏的是 A 的一半,于是只修那一半。 ⛔ 照单转录一个更宽的框架,会改掉一条本来正确的路径。

    三个方向都测了

    零标签(未测量)⇒ 修前后差 22 行;4 个标签、载体不在(真缺席)⇒ 逐字节相同;5 个标签、载体在 ⇒ 逐字节相同。⭐ 新电池做过两次阳性对照(把新分支改成 else if (false) ⇒ 8 个失败,其中一个逐字复现立卡那条串;把渲染开关改成 = false ⇒ 3 个失败含路由那条腿),并已进 --self-test(SELF_TEST_BATTERY_FLOOR 17 → 18),⛔ 不只是本地验过。


    Generated by Claude Code

  5. removed their assignment
    on Sep 17, 2026
  6. added a commit that references this issue on Sep 17, 2026
    a55646b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions