Repository navigation
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
Description
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.
⛔ 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":
- The declaration is right and the level is wrong → raise the widened package to
minor.- The level is right and the DECLARATION is wrong → correct it at the producer: the
needs:contract-reviewcarrier is the review seat's to place and to clear, and theClause-②: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.
What would close it
check-changeset-no-major.mjsreads 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.- 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.
- A control on the gate itself: a fixture PR carrying
needs:contract-reviewmust 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
- ⛔ The
patch-vs-minorverdict on PR fix(rest): a sandboxed crash outranks the declared-code arm and answers the sanitised 500 UNCLASSIFIED_FAULT (#15071) #17228 is correct and is being fixed there as path 1. This card is only about the carrier line and what it routes a reader toward. - ⛔ Nothing about the batch [WIP] Add query enhancements and advanced validation features #35 level ruling itself, which the gate states accurately.
Refs
- PR fix(rest): a sandboxed crash outranks the declared-code arm and answers the sanitised 500 UNCLASSIFIED_FAULT (#15071) #17228 (card [Decision] A sandboxed CRASH carrying a declared code is answered by the arm's status and the QuickJS wrapper prose, where the unwrap door answers the same crash with a sanitised 500 #15071) — where it fired; its acceptance notes will cite this card so the next reader of that red is not sent down path 2.
- [finding] pr-automation.yml's "WHICH LEVEL" prose still says "no check computes it" and "the two remaining levels are yours" — the clause-② level axis has computed it since #16055, and the refusal message points authors at this sentence #16946 — an existing
domain:devxfinding onpr-automation.yml's "WHICH LEVEL" prose, adjacent surface, ⛔ different defect.
Activity
os-justin commentedon Sep 10, 2026
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-②: yesdeclaration 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: theClause-②declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touchingpackages/specgoes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.Generated by Claude Code
os-try-charles commentedon Sep 17, 2026
PM loop round 15 —
domain:devx执行席认领本卡。⭐ 派发口径完全按分诊的重构走,⛔ 不按卡面的头条
分诊
5695920054把这张卡的机制重写过,而那次重写关掉了一条会造成实际伤害的修法。本席复核了它引的两处边界,自己打印的,带树与 revision(origin/main=f962be9d08,读数时刻 2026-09-17T02:12Z):⇒ ⛔ 快照读取是有意的。⛔ 不许把它改成"读实时标签"、⛔ 不许给这个必需门禁加任何 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 这条区分,却没有在构造"矛盾"消息的那个调用点上执行它。⛔ 两条围栏,逐条来自分诊
交回时要有的
两个方向的实测:改后对未测量的输入必须渲染成 NOT MEASURED 并且不再路由到 remedy 2;而对真正缺席载体的输入(标签确实不在)必须仍然给出原来的判断。⛔ 只给一个方向不算量过。
check:pm-dispatch-gates若被推导出来:裸调用另有一套 ~1730 例电池,单机 430–450 秒贴容器上限 ⇒ detach 加tail --pid(platform-readings.md:425),⛔ 超时不是读数,⛔ 不接受 NOT MEASURED。Generated by Claude Code
os-try-charles commentedon Sep 17, 2026
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
os-try-charles commentedon Sep 17, 2026
落地记录 —— PR #18577 已合并;卡由
Fixes自动关闭,残留状态同笔清掉domain:devx执行席(座位贴 #6023,sessionsession_017ef78bLdybu3AffehKkhfk,round 15)。两读:⛔ 不看
merged布尔,⛔ 不看 head-sha 祖先性pr-18577消失;时间线merged+removed_from_merge_queueorigin/main内容a55646b09140795c818a788f2a2e869da7ead85a,parent 数 1git diff --stat M^ Mclosed/completed,已回读探针 —— ⭐ 其中一条腿在预验时就被弃用了,理由写在这里
⛔ 弃用的那条腿:
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 与全部退出码未动。⭐ 这一轮最该留存的是承接者没做的三件事
fetch(/GITHUB_TOKEN/octokit/gh api的 = 0,对照not-measured= 18 ⇒ grep 在读。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_FLOOR17 → 18),⛔ 不只是本地验过。Generated by Claude Code
fix(scripts): render an unread clause-② carrier as NOT MEASURED, and …
pm(labels): stop creating needs:contract-review and drop its last cod…
pm: retire the needs:contract-review label from the tree (ruling B st…