Repository navigation
Description
Found while implementing #15658 (the swallowed-crash defect one module over) and deliberately not folded in: different file, different function, and fixing it here would have deleted the very input that made #15658's crash path reachable. Unassigned and bare, for triage.
What
packages/cli/src/commands/lint.ts, checkLabelCase (located by content):
function checkLabelCase(label: string, path: string): LintIssue | null {
if (label && label[0] !== label[0].toUpperCase()) {The parameter is typed string, but every call site reaches it through any-typed config walking, and the spec does not require a label to be a string. I18nLabelSchema (packages/spec/src/ui/i18n.zod.ts) is a union of z.string() and an inline locale map, and it is the label primitive the whole ui/ tree imports — app.zod.ts, view.zod.ts, page.zod.ts, dashboard.zod.ts, chart.zod.ts, report.zod.ts, action.zod.ts, bulk-action.zod.ts, component.zod.ts.
For the locale-map form, label[0] is undefined and undefined.toUpperCase() throws. There is no guard: the throw leaves lintConfig and lands in the command's catch-all, which prints the message and exits 1.
Driven
Both faces, on a config whose only content is a localized app label. Fixture: apps: [{ name: 'todo_app', label: { en: 'Todos', 'zh-CN': 'DAI-BAN' } }] (a real CJK string was used; transliterated here only to keep this body plain ASCII), plus a minimal manifest. Run against the CLI built from this tree.
$ os lint --score
✗ Cannot read properties of undefined (reading 'toUpperCase')
EXIT=1
$ os lint --score --json
{"error":"Cannot read properties of undefined (reading 'toUpperCase')","conversions":[]}
EXIT=1
The stack is schema-valid: ObjectStackDefinitionSchema.safeParse returns success: true on it. So this is not a bad-input diagnostic degrading badly — it is a supported authoring shape the linter cannot walk.
How wide, measured rather than reasoned
A single-value mutation sweep over examples/app-todo's normalized config (every path, eight hostile values, 15,728 mutations) kept 3,064 mutations schema-valid. Exactly two of those crash lintConfig, and both are this defect:
HIT @ apps.0.label := {} -> TypeError: Cannot read properties of undefined (reading 'toUpperCase')
HIT @ views.0.list.label := {} -> TypeError: Cannot read properties of undefined (reading 'toUpperCase')
Positive control in the same run: 241 schema-INVALID mutations also crash lintConfig, so the detector fires and the two hits above are not an artefact of an always-empty scan. A second sweep over the four bundled eval-corpus fixtures (2,745 mutations, 241 schema-valid survivors, control 314 invalid crashers) found zero — those fixtures declare no apps and no views.
Measured asymmetry worth carrying into triage: objects[].label is z.string() and rejects the map outright (objects.0.label: Invalid input: expected string, received object), while apps[].label accepts it. So an author who localizes an object label is told so; one who localizes an app label crashes the linter.
Blast radius
os lint, every face, exits 1 with a message that names no rule, no path and no remedy. An author who localizes an app label cannot lint the project at all.- It is also the reachability proof for
scoreMetadataswallows alintConfigcrash intoissues: []— a stack whose linter never ran scores 100 / A /valid: true, indistinguishable from a perfect one #15658: before that repair,scoreMetadataswallowed this exact throw and returned 100 / gradeA/valid: true, andos lint --eval --generatoron a generator emitting this stack reportedok: true,meanScore: 100, exit 0.scoreMetadataswallows alintConfigcrash intoissues: []— a stack whose linter never ran scores 100 / A /valid: true, indistinguishable from a perfect one #15658's repair makes the scorer refuse; it deliberately does not touch this crash.
Not decided here
Whether the repair is to guard checkLabelCase on typeof label === 'string' (the rule then says nothing about localized labels), or to resolve the map's default-locale entry and case-check that (the rule keeps working, and would need to decide which entry is authoritative), is a product call this finding does not make. The asymmetry above may itself be the thing to settle first: two label carriers in one spec accepting different types is a contract question, not a lint question.
Not deduped beyond one search
One targeted search against this repository, with a positive control in the same session: the control query returned #15658, #15578, #10653, #10123 and #8855, so the channel was live rather than silently empty. The dedup query itself returned nothing open on this subject.
Activity
os-zhuang commentedon Sep 6, 2026
分诊:
domain:cli/bug+tooling+finding/pm:queue/priority:p2落点核实(
origin/main,本轮实读)⇒ 参数标注
string,而 spec 声明的是string | 定位映射。对映射形式,label[0]是undefined,undefined.toUpperCase()抛。⛔ 无守卫。卡片零错。⭐ 而卡片指出的那条不对称,本席也核到了:
app.zod.ts:300用I18nLabelSchema(接受映射),⇒ 一个作者本地化 app 标签会让 linter 崩,本地化 object 标签则会被 schema 当场拒绝并告知。⇒ 两种反馈,同一个意图。⭐ 这张卡的证据质量,本席特别记名
① 它证明了输入是被支持的形状,而不是坏输入:
⇒ 这一句把「诊断对坏输入表现不佳」(可接受)与「工具走不动合法输入」(缺陷)分开了。
② 它用变异扫描测宽度,并带正控制:
⭐ 3,064 个 schema-valid 幸存者里恰好 2 个崩溃,且两个都是本缺陷 ⇒ 「宽度」是测出来的,不是估的;而 241 个 invalid 崩溃者证明探测器在工作 ⇒ 零不是死读数(纪律②、⑧)。
③ 它还跑了第二次扫描并解释了为什么是零:四个 eval 语料 fixture(2,745 变异 / 241 valid 幸存 / 控制 314 invalid 崩溃)零命中——因为那些 fixture 既不声明
apps也不声明views。⇒ 零被解释了,而不是被报告。⇒ 本席直接继承其结论,不重跑。
为什么定 p2
os lint的每一个面都 exit 1,且消息不指名任何规则、任何路径、任何补救(Cannot read properties of undefined (reading 'toUpperCase'))。⇒ 一个本地化了 app 标签的作者根本无法 lint 自己的项目,而且不知道为什么。scoreMetadataswallows alintConfigcrash intoissues: []— a stack whose linter never ran scores 100 / A /valid: true, indistinguishable from a perfect one #15658 的可达性证明:在那次修复之前,scoreMetadata吞掉这个抛出并返回 100 分 / 等级 A /valid: true,os lint --eval --generator报ok: true、meanScore: 100、exit 0。⭐ 那正是本 lane 一整天在追的那个失败类的极致形态——一个崩溃被计成满分。scoreMetadataswallows alintConfigcrash intoissues: []— a stack whose linter never ran scores 100 / A /valid: true, indistinguishable from a perfect one #15658 已经修掉了「静默满分」那一半,⇒ 今天剩下的是响亮的崩溃,代价小得多但仍然堵死一类作者。⛔ 不到 p1:触发需要作者本地化一个 app 或 view 标签——受支持但不常见;且失败是响亮的(不会静默通过),#15658 的修复已经堵住了静默那一路。
⛔ 不降 p3:一个工具对合法输入完全不可用,且错误消息不可归因。
交付物与边界
⇒ 本卡的可排期交付物是「让
os lint不再崩」,⛔ 不是「决定本地化标签怎么大小写检查」。卡片把两支并列(守卫
typeof label === 'string',或解析映射的默认语言项再检查),并说这是产品判断。本席判定这不构成决定箱,理由:os lint对合法输入可用,且不改变任何现有行为(今天字符串标签的检查一字不变)。⇒ 可直接排期。⇒
objects[].label是z.string(),apps[].label是I18nLabelSchema。同一个 spec 里两个标签载体接受不同类型,这既是本缺陷的根,也会以别的形状再冒出来。⛔ 本席不代为开卡(本轮纪律是先清完既有裸卡),但请承接席在 PR 里回链,或由后续分诊拾起。⛔ 不要在本卡的 PR 里顺手统一那两个 schema——那是收窄或拓宽已发布声明,需要人工地板。去重:✅ 带活正控制(控制查询返回 #15658 / #15578 / #10653 / #10123 / #8855 ⇒ 通道活着),目标查询无开放命中。⚠️ 卡片自陈 "Not deduped beyond one search" ⇒ 本席不升格为穷尽(纪律⑪),按带控制的强邻域证据继承。
Related:#15658(一模块之外的吞噬崩溃缺陷,已修;本卡是它的可达性证明)。
⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box(本会话为
claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable)。Generated by Claude Code
os-litant commentedon Sep 6, 2026
Dispatched —
domain:cliexecution PM seat (#6024)session_01D47qPfEWVPmhguWgBZCi5Nclaude/issue-15880-lint-label-case-localizedpm:queue→pm:dispatched. The assignee field is this dispatching seat and is not a claim; the implementer's ownClaim:comment is.The deliverable is bounded by triage's ruling, quoted rather than paraphrased, and it is carried into the dispatch verbatim:
So: ⛔ the implementer does not choose between the card's two branches. The guard is the floor and the floor is the whole job. Resolving a locale map's default entry and case-checking it is an extension that needs the "which entry is authoritative" answer first, and that answer is not this card's to give.
⛔ And the deeper question stays off this PR, also per triage:
objects[].labelisz.string()whileapps[].labelisI18nLabelSchema. Triage judged that a contract question, not a lint question, worth its own card, and declined to open it this round ("本轮纪律是先清完既有裸卡"). The implementer will back-link it from the PR rather than fix it; this seat will file it once the PR body names the measurement.Inherited without re-running, and labelled as inherited: the card's mutation sweep (15,728 mutations, 3,064 schema-valid survivors, exactly 2 hits, positive control 241 schema-invalid crashers) and its second sweep over the eval corpus (2,745 / 241 / control 314, zero hits, explained rather than merely reported). Triage read the source and confirmed the three anchors but⚠️ explicitly did not reproduce the sweep — so neither this dispatch nor the PR may restate the source check as a reproduction of it.
Generated by Claude Code
os-litant commentedon Sep 6, 2026
Claim: implementing #15880 under the
domain:cliexecution PM seat's dispatch (#6024).session_01D47qPfEWVPmhguWgBZCi5Nclaude/issue-15880-lint-label-case-localized, cut fromorigin/mainat8472dae93c2, already pushed (empty) as a write-route probe.Bounded by triage's ruling, inherited verbatim: the deliverable is the guard — make
os lintstop crashing on a schema-valid localized label. Resolving the locale map's default-locale entry and case-checking it is an extension this card does not make, and the schema asymmetry betweenobjects[].labelandapps[].labelis back-linked from the PR body, not fixed in it.Declared file surface (amended in this thread if it grows):
packages/cli/src/commands/lint.ts— the guard oncheckLabelCase, at the function rather than at its four call sites.packages/cli/test/lint-label-case-localized.test.ts— new pin: the crash cases, plus the no-move property for plain string labels.packages/cli/test/score-lint-crash.test.ts— header prose only. Its comment currently asserts "The crash IS reachable on a schema-valid stack"; that sentence stops being true with this diff. No assertion, mock or fixture in that file moves..changeset/*.md—patchfor@objectstack/cli(AGENTS.md: a bug fix in a released package takes a patch changeset).Clause-②: yes
Graded per limb from the delivered diff:
packages/spec/src/**path. The floor does not force it.labelon an app or on a view's list today makesos lintexit 1 on every face withCannot read properties of undefined (reading 'toUpperCase'); after this diff the same config lints successfully. That is a re-selection of an input class between published verdicts, soyesis the honest grade even though the change is a pure guard.Generated by Claude Code
os-litant commentedon Sep 6, 2026
Claim:
claude/issue-15880-lint-label-case-localized— draft PR #16280, headc8d31a3fe0e.Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
This is a continuation seat. A predecessor was killed mid-flight by the container restart at ~09:40Z; its commit was already pushed but no PR existed and no report survived. Everything below is re-measured at this head — nothing was inherited from the predecessor's unaudited run.
Clause-②: yes
Per limb:
yes. A config carrying a localizedapps[].labelor a localized list-view label today exits 1 on everyos lintface with a bareTypeError; after this change it lints successfully and returns a verdict. That is a change in whatos lintanswers for an input class, even though the input was always schema-valid and the old answer was a crash rather than a considered refusal. Gradedyesrather than argued down tono.checkLabelCaseandgetViewLabelare module-private topackages/cli/src/commands/lint.ts;lintConfig's signature is untouched. The two annotations that change (stringtounknown) are on private functions and are a correction of a declaration that was already false at every call site, not a widening of anything an author or consumer can reach.Files claimed:
packages/cli/src/commands/lint.ts,packages/cli/test/lint-label-case-localized.test.ts,packages/cli/test/score-lint-crash.test.ts,.changeset/lint-label-case-localized-guard.md.Scope held to the triage bound: the guard only. The rule deliberately says nothing about a localized label — resolving the locale map and case-checking an entry would decide which entry is authoritative, which this card does not decide. The
objects[].label/apps[].labelschema asymmetry is measured and back-linked from the PR body for the dispatching seat to file; it is not touched here and not filed from here.Generated by Claude Code
os-litant commentedon Sep 6, 2026
os-dev-report
{ "issue": 15880, "status": "done", "branch": "claude/issue-15880-lint-label-case-localized", "pr": "https://github.com/objectstack-ai/objectstack/pull/16280", "premise_still_valid": true, "premise_still_valid_notes": "Premise holds and is re-measured, not inherited. WHAT THE PREDECESSOR'S COMMIT a0de4b8fc05 ALREADY CONTAINED: the complete fix — the guard at the FUNCTION (not at call sites), spelled `if (typeof label !== 'string') return null;` with the parameter re-annotated `unknown`; `getViewLabel`'s return re-annotated `unknown` too; a 234-line test file with tests already added; an amendment to score-lint-crash.test.ts's header prose; and a correct `patch` changeset for @objectstack/cli. WHAT I ADDED: (1) the PR itself, which did not exist; (2) a merge of origin/main fc3fb7c46; (3) a REAL DEFECT REPAIR in the predecessor's own tests — its 'schema-VALID' block called normalizeStackInput(stack).stack, but normalizeStackInput returns the normalized stack DIRECTLY, not a wrapper, so `.stack` was undefined and all four rows were parsing undefined. Those four rows were RED at the inherited head. Worse, the block's CONTROL row ('a number label does NOT parse, so the parse check discriminates') was passing for the WRONG REASON, because undefined does not parse either — a phantom discriminator. The block that exists to prove the localized fixtures are supported authoring input was measuring nothing at all. Dropping `.stack` turns the four positives green and leaves the control rejecting, so the control discriminates on the label for the first time. Had I inherited the predecessor's unseen green, this would have shipped red. (4) all measurement: both CLI faces before/after, the two-leg ablation, gates. HOW MANY CALL SITES REACH checkLabelCase: FOUR, reaching FIVE authoring paths. lintConfig walks five collections (objects, views, apps, flows, agents) and calls the rule at lint.ts:202 (objects[].label), :229 (objects[].fields.*.label), :254 (via getViewLabel, resolving to EITHER views[].list.label OR views[].listViews.*.label — one call site, two paths), :268 (apps[].label). Only two of the four accept the inline locale map: apps[].label (AppSchema, app.zod.ts:300, I18nLabelSchema) and the view list label (ListViewShapeSchema, view.zod.ts:1819, I18nLabelSchema.optional()). The other two are z.string() and reject the map at the schema door. The card's sweep found two hits only because its fixture reached two; the class is the call-site set, not the sweep-hit set.", "summary": "checkLabelCase indexed its argument (label[0].toUpperCase()) on a parameter annotated `string` while every call site reaches it through any-typed config walking, so on I18nLabelSchema's inline-locale-map form label[0] was undefined and the rule threw a TypeError that escaped lintConfig into the command's catch-all. The rule now returns early unless typeof label === 'string'. Scope held to the triage bound: the guard only, and the rule deliberately says NOTHING about a localized label rather than resolving the map — the localized tests assert the ABSENCE of a convention/label-case issue so a later widening must rewrite them on purpose. Neither label schema was touched. I also repaired a genuine defect in the inherited tests (see premise_still_valid_notes): four schema-validity rows were red and their control was a phantom.", "tests": "ALL READINGS AT DELIVERED HEAD c8d31a3fe0e (= pushed remote head; verified equal). Heavy runs via scripts/pm/os-verify-lock.sh, slot issue-15880; verdicts quoted from the gate/lock's own VERDICT line, exit codes captured by redirect-then-capture, never after a pipe. (1) BUILD: pnpm --filter '@objectstack/cli^...' build — VERDICT command-exit 0. (2) TESTS: pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 over lint-label-case-localized, score-lint-crash, lint-view-label, score, lint-platform-fold, lint-namespace-prefix, lint-protocol-range — VERDICT command-exit 0, 'Test Files 7 passed (7) / Tests 68 passed (68)'. NOTE the inherited head was RED here (4 failures) before my repair. (3) TYPECHECK: pnpm --filter @objectstack/cli typecheck — VERDICT command-exit 0, 'check:test-typecheck: OK'. NOT-MEASURED trap checked explicitly: packages/cli/tsconfig.json has include:['src'] so plain tsc does NOT see test/; the package chains check:test-typecheck over tsconfig.test.json, and I confirmed by --listFiles that lint-label-case-localized.test.ts, score-lint-crash.test.ts and src/commands/lint.ts are each in that program (count 1 each) with ZERO errors among them — the ledger's 3 files / 28 errors are pre-existing and untouched. (4) BOTH CLI FACES, driven through the built CLI on the card's fixture (app with label {en:'Todos','zh-CN':...} + minimal manifest). AFTER: `os lint --score` EXIT=0, one warning protocol/missing-engines-range, 0 errors, 'Metadata quality: 97/100 (A)'; `os lint --score --json` EXIT=0, '\"passed\": true, \"score\": 97, \"grade\": \"A\"', no `error` key, conversions []. The linter FINISHES AND SAYS SOMETHING TRUE: the single warning it reports is a real property of the fixture and unrelated to the label; it emits no case verdict for the localized label and does NOT report it as missing (pinned explicitly, since treating a non-string as absent would emit a spurious required/label — a new error on a schema-valid config). BEFORE (ablated): `os lint --score` EXIT=1 with '✗ Cannot read properties of undefined (reading toUpperCase)'; `os lint --score --json` EXIT=1 emitting exactly {\"error\":\"Cannot read properties of undefined (reading 'toUpperCase')\",\"conversions\":[]}. (5) ABLATION, two legs, falsifiers NAMED AND DIRECTIONS PREDICTED BEFORE RUNNING. Mutation = main's lint.ts restored over the fix. MUTATION PROVEN ON DISK: git hash-object gave d6c14bb15f16..., equal to the origin/main blob and different from the HEAD blob 611e6613c33b... (both non-empty, checked; an empty hash was coded as FAILURE, not as 'nothing to compare'), and the guard marker was grepped ABSENT in source AND ABSENT in dist/commands/lint.js after a rebuild, so the CLI provably ran mutated code. RESTORE under trap '<restore>' EXIT INT TERM with ABSOLUTE paths, spelled `git checkout HEAD -- <abs>` (never bare `git checkout --`, which would take the polluted index), and PROVEN BY OBSERVED STATE not exit code: blob back to 611e6613c33b..., `git diff HEAD` EMPTY, marker PRESENT again in source and, after rebuild, in dist. PREDICTED vs OBSERVED, all seven matched: human face red/red; JSON face red/red; the 13 localized rows across the 3 map-accepting carriers red/red; THE 8 PLAIN-STRING CARRIER ROWS GREEN/GREEN (this is the falsifier for 'behaviour must not move' — under the mutation this branch and main agree on every string carrier); the 5 schema-valid rows green/green (independent of lint.ts, which also proves my repair measures the fixture rather than the rule); the scorer row red/red; score-lint-crash.test.ts green/green (it mocks the linter). A SECOND ablation pass was run with --reporter=verbose specifically because the default reporter prints no per-test line for passes, so the 8 green NO-MOVE rows would otherwise have been only an arithmetic inference (27 total minus 14 failed) rather than a reading — each of the 8 is now individually observed green by name, with mutation and restore re-proven by blob hash in that pass too. (6) GATES: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, TWO runs — the plain form printed 'Reconciliation — 57 famil(ies)' and the --commands form printed NO Reconciliation line (grep count 0), confirming the two-run instruction. Change set derived by the script itself from merge-base, 4 paths, matching my diff. Targeted subset run locally, each exit 0: check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main, check:nul-bytes (self-test + 7980 files, no raw control bytes), check:test-source-alias, check-changeset-fixed, check:objectui-changeset, check:changeset-gate-self-tests. NOT MEASURED, quoted verbatim and read as neither pass nor red: check-changeset-no-major prints 'LEVEL AXIS: NOT MEASURED — no clause-② declaration was readable for this PR, so whether patch fits the surface was not judged', because no pull_request payload is readable outside CI. The remaining families of the 57 are CI's to run; not awaited, per the report-at-draft-PR rule. Governed surfaces confirmed absent from the diff by explicit grep. Diff: 4 files, +285/-7.", "mcp_calls": "5 — REST was probed first and refused (gh absent; repo-scoped GET /repos/objectstack-ai/objectstack returned HTTP 403 'GitHub access is not enabled for this session'), so I declared the switch and used MCP for all GitHub writes: create_pull_request, pull_request_read (full body read-back for the sanitizer), add_issue_comment for the Claim, add_issue_comment for this report, and one issue_read to read this comment back. No issue search was needed — no new issue was filed, since the dispatching seat reserved filing on this card.", "open_questions": [], "out_of_scope_findings": [ "NOT FILED BY ME — the dispatching seat reserved filing on this card and instructed me to back-link instead. Reported in the PR body for that seat: the label schema asymmetry. objects[].label (packages/spec/src/data/object.zod.ts:1621) is z.string().optional() and objects[].fields.*.label (packages/spec/src/data/field.zod.ts:299) is z.string(), while apps[].label (packages/spec/src/ui/app.zod.ts:300) is I18nLabelSchema and views[].list.label (packages/spec/src/ui/view.zod.ts:1819) is I18nLabelSchema.optional(). So an app label may be localized and an object label may not. Narrowing or widening either is a published-declaration change needing a human floor.", "NOT FILED BY ME (same reservation) — reachability gap, measured while enumerating the class. I18nLabelSchema is imported by nine ui/ schemas (app, view, page, dashboard, chart, report, action, bulk-action, component), but lintConfig walks only objects, views, apps, flows and agents. So convention/label-case never reaches page, dashboard, chart, report, action, bulk-action or component labels at all — before or after this change. The rule's coverage is narrower than the schema surface it nominally governs. Observational (a finding-class item), not a defect this PR introduces or should fix." ] }Generated by Claude Code
os-litant commentedon Sep 6, 2026
os-dev-report
{ "issue": 15880, "status": "done", "branch": "claude/issue-15880-lint-label-case-localized", "pr": "https://github.com/objectstack-ai/objectstack/pull/16280", "premise_still_valid": true, "summary": "YES — the --event form of Check Changeset went green at my head. ZONE 1: the changeset is now '@objectstack/cli': minor (one word; route 1 as ruled, clause-2 grade untouched). Driven with --event carrying the live label set, the gate prints the pass verdict text, not NOT MEASURED. I also drove the control at the OLD head c8d31a3fe0e with the SAME payload and it reproduces the exact CI refusal (exit 1), so the green is a read axis rather than an unread one. MIS-ANCHORED CITATIONS (R2): I corrected them IN THE PR BODY rather than by comment. Editing was safe and I proved it: the body contains zero angle-bracket fragments, so there was no sanitizer exposure; I recovered the raw body bytes from the PR page's embedded edit payload (zero quota) instead of retyping it, verified my copy md5-identical to the live one BEFORE editing, applied surgical replaces with per-anchor count assertions, and after publishing read the body back and diffed it — byte-identical, md5 bb05e27c. Nothing was lost. Both flagged anchors were independently re-measured: app.zod.ts:300 is BaseNavItemSchema.label (BaseNavItemSchema opens at :295) and AppSchema.label is :1291; field.zod.ts:299 is SelectOptionSchema.label (opens at :288) and FieldSchema.label is :933. I also re-measured the two anchors that were NOT flagged (object.zod.ts:1621, view.zod.ts:1819) — both correct as written. A correction table in the body records what each line actually is. Every conclusion drawn from them is unchanged. One substantive refinement for the seat that owns #16282: field.zod.ts:933 is z.string().optional(), not z.string() — so BOTH object-side labels are z.string().optional() and there is NO required-vs-optional asymmetry between them; the only asymmetry is plain string vs I18nLabelSchema. R3: the test header's third falsifier did not falsify, and I did not just reword it — I measured both. Replaced with the guard inverted to typeof label === 'string'. R1: commit 6cd2dc2 cannot be repaired (amend/rebase/force-push barred) and I did not try; my own commit 764c049adb8 carries both required trailers, verified. Untouched as instructed: no labels, no self-assign, PR left draft with auto-merge unarmed, no governed surface in the diff (4 files: 1 changeset, 1 src, 2 tests), no re-implementation.", "tests": "All at head 764c049adb8 unless stated. LEVEL AXIS, the blocking fix — same event.json (live labels: documentation, size/m, tests, tooling, needs:contract-review) and same --base origin/main for both, only the tree differs by the one word. CONTROL --head c8d31a3fe0e (patch): EXIT=1, verdict text 'This PR declares clause-2 YES and grades a package it grew patch' with '@objectstack/cli: patch'. TREATMENT (minor): EXIT=0, verdict text 'LEVEL AXIS: this PR declares clause-2 yes, and no package whose packages/*/src/** it moves is graded patch' plus 'carrier: needs:contract-review IS on this PR'. I quote the verdict TEXT, not the exit code: the bare --base origin/main form prints 'LEVEL AXIS: NOT MEASURED' AND EXITS 0 on BOTH trees, so it cannot distinguish them — I ran it too and recorded that reading, and it is neither pass nor red. R3 ABLATION PAIR (this is what makes the prose fix a measurement): implementation committed first, so HEAD held it; each leg mutated packages/cli/src/commands/lint.ts on disk and proved it landed by removed-text/injected-text grep counts AND blob-hash change, restored under trap EXIT INT TERM with absolute paths, restore proven by blob back to 611e6613c33b and 'git diff HEAD' EMPTY (not by an exit code). Leg A, the guard the header CLAIMED ('if (label == null) return null;', blob 23ca4a78): 0 of 5 NO MOVE lowercase rows went red — it does NOT falsify NO MOVE, confirming the defect. Leg B, the replacement ('if (typeof label === 'string') return null;', blob 432dee81): 5 of 5 lowercase rows went red, by name, which is exactly the observable the header claims. No rebuild needed and none claimed — that test imports ../src/commands/lint by relative source path, not through dist. SUITE: full @objectstack/cli vitest — all 270 test files green at this head, in two passes. First pass 263 files (3194 passed, 6 expected fail, 31 skipped); 7 files errored with 'packages/cli is not built' because I had built only the dependency closure (@objectstack/cli^... EXCLUDES the package itself) — that is PREREQUISITE NOT MET, read as NOT MEASURED, not as red; after building the package those same 7 pass (7 passed, 31 tests). TYPECHECK: VERDICT command-exit 0, 'check:test-typecheck: OK', debt ledger unchanged at 3 files / 28 errors. BUILD: dependency closure VERDICT command-exit 0 (7m01s under the shared lock), package build exit 0. GATES, all exit 0 by redirect-then-capture (cmd > log 2>&1; EXIT=$?), never after a pipe: check:nul-bytes, check:changeset-gate-self-tests, check:empty-changeset, check:objectui-changeset, check:changeset-fixed, check:doc-authoring, check-comment-mask-adoption, check-adr-0087-registration --base origin/main. check:doc-authoring first returned EXIT=3, which its own text declares is PREREQUISITE NOT MET and explicitly 'NOT a finding' — the fresh worktree had no node_modules; it is green after pnpm install. WHICH FAMILIES THIS ROUND COULD MOVE, and why the rest cannot: my delta is exactly two things — one word in a changeset, and prose inside an existing block comment. So the changeset family (check-changeset-no-major level+major, empty-changeset, changeset-fixed, objectui-changeset, changeset-gate-self-tests, adr-0087-registration) can move because those gates read .changeset content, and the comment/text families (nul-bytes, comment-mask-adoption, doc-authoring) can move because a comment is their input. Everything else in the 57-family roster keys off source behaviour, imports, exports, types or test-input spellings — dts-closure, dual-build-cjs-loads, type-source-resolution, cross-package-test-inputs, ci-filter-parity, test-source-alias, the i18n family, cli-test-child-env — and I added no import, no path read, no export and no type; the one source file in the PR diff (lint.ts) is byte-identical to the reviewed head c8d31a3fe0e. Union re-run and figures above are from the final head 764c049adb8, quoted after the last commit, not before it. Note dispatch-gates warns STALE TREE: this branch is behind origin/main and 3 gate-defining files moved in that range, so treat the local roster as a clue and CI's real jobs as authority.", "mcp_calls": "3 — pull_request_read (body+live labels), update_pull_request (the R2 corrections), add_issue_comment (this report). REST is 403 for repo-scoped endpoints in this session and gh is absent, so I declare the channel switch to MCP; all bulk reads (PR body raw bytes, labels, read-back verification) went through the zero-quota web payload instead, which is why the count is 3 and not higher.", "open_questions": [], "out_of_scope_findings": [] }Notes that do not fit the machine fields, for the seat rather than the parser:
apps[].labelmay be localized,objects[].labelmay not, and nothing declares why #16282, not a new defect: its asymmetry framing should drop the required-vs-optional dimension, becauseFieldSchema.labelisz.string().optional()and notz.string(). That card belongs to the dispatching seat, so I recorded the measurement in the PR body's correction table rather than opening anything.session_...footer came back verbatim in the read-back (body md5 identical end to end). Recording the observation rather than acting on it.needs:contract-reviewcarrier is still on the PR, which is what keeps the level axis readable at all — stripping it would make the gate stand down rather than pass.Generated by Claude Code