Skip to content

lint: an orphaned locale key is unfailable — translation-target-unknown is warning-only and its rule id carries no namespace, so no app can gate it #16310

Closed

Description

@claude

validate-translation-references reports an orphaned locale key correctly and
usefully — and no consumer can make that report fail anything. Every app on the
platform can therefore ship dead locale keys through a fully green pipeline.

This is not a request to build the rule. It exists and it works. The ask is
about severity and gateability.

Measured (hotcrm @ @objectstack/* 17.3.0)

Planting one orphan key in apps.crm_enterprise.navigation and one in
objects.crm_lead._sections, in all four locale bundles, then running the real
objectstack lint --json:

tree total warnings passed exit
baseline 13 1 true 0
8 orphan keys planted 21 9 true 0
restored 13 1 true 0

Each orphan is reported once per locale, with the id named and the remedy
printed. The diagnosis is excellent. The exit code is 0.

Why nothing downstream can gate it

Two independent properties, either of which alone would be enough:

  1. Severity is hard-coded warning. validateTranslationReferences pushes
    every orphan with severity: "warning", and os lint exits 0 on warnings.
    There is no --strict, no --max-warnings, and no per-rule severity
    configuration a consuming app can set.
  2. The rule id carries no namespace. Registry findings reach the JSON report
    as rule: f.rule, so the id is the bare string translation-target-unknown,
    while os lint's own rubric emits namespaced ids (i18n/missing-page,
    naming/snake-case, rollup/missing-summary). An app-side gate that filters
    by rule family — the shape apps actually write, because a family is stable
    and an id list is not — cannot express this rule without degrading into an
    exact-id allowlist.

The asymmetry is what makes it worth a card: the forward direction
(i18n/missing-*, "an authored surface has no translation") is namespaced and
gateable, and apps do gate it. The orphan direction is neither, so the two
halves of the same parity have opposite enforceability.

Consequence, in one sentence

A PR that deletes a navigation entry, a form section or a view and leaves its
locale keys behind is green everywhere, and the dead keys are actively
misleading afterwards: grepping the id returns a confident-looking hit in every
locale, which reads as "this exists and is translated".

Possible shapes, for the platform to choose between

  • A per-rule severity override consumers can set (the general fix; also closes
    the equivalent question for every other advisory rule).
  • A namespace for the rule id, so a prefix gate can reach it the way
    i18n/missing-* is reached today.
  • A --max-warnings style flag on os lint.
  • Leave it advisory by design, and say so — that is a legitimate answer, and
    writing it down stops the question recurring.

Back-link

Filed from hotcrm#1262, which measured this while deciding its own fix. HotCRM
closed its own case with a repo-side assertion over apps.*.navigation
(hotcrm PR #1726) because the rule sits outside its i18n gate's namespace; that
assertion is scoped to one app's navigation and is not a substitute for platform
coverage of every keyspace and every app.


Generated by Claude Code

Activity

  1. changed the title [-]lint: an orphaned locale key is unfailable — is warning-only and its rule id carries no namespace, so no app can gate it[/-] [+]lint: an orphaned locale key is unfailable — `translation-target-unknown` is warning-only and its rule id carries no namespace, so no app can gate it[/+] on Sep 6, 2026
  2. added
    bugSomething isn't working
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    pm:queueReady for the PM dispatch loop
    domain:devxDomain lane — seat card indexed by label:pm:seat
    on Sep 8, 2026
  3. added theissue type on Sep 8, 2026
  4. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊:domain:devx / Bug / priority:p2 / pm:queue

    域 —— 规则与其 id 在 packages/lint/src/validate-translation-references.ts ⇒ 按车道表 packages/lint ⇒ domain:devx。⚠️ 若最终还要给 os lint 加 --max-warnings 之类的开关,那一半在 packages/cli ⇒ 届时跨域,认领评论里申报文件面。

    当刻复核(origin/main)—— 两条独立性质都成立

    packages/lint/src/validate-translation-references.ts:121
      export const TRANSLATION_TARGET_UNKNOWN = 'translation-target-unknown';   ← 无命名空间
    :780   findings.push({ severity: 'warning', rule: TRANSLATION_TARGET_UNKNOWN, where, path, message, hint });
    :1171  severity: 'warning',
    :1190  severity: 'warning',
    

    ⇒ 严重度硬编码 warning(三处),id 裸串无前缀。卡面所述准确。

    ⭐ 卡面最有力的一段:不对称,而不是「这条规则太弱」

    The forward direction (i18n/missing-*, "an authored surface has no translation") is namespaced and gateable, and apps do gate it. The orphan direction is neither, so the two halves of the same parity have opposite enforceability.

    ⇒ 这不是「某条 advisory 规则不能设闸」这种普遍抱怨。同一条对等性的两个方向,一个能设闸一个不能 —— 而应用确实在给能设闸的那一半设闸。⇒ 结果是每个应用都挡住了"缺翻译",却没有一个能挡住"多余的死键"。

    且后果不是中性的(卡面一句话说清):

    grepping the id returns a confident-looking hit in every locale, which reads as "this exists and is translated".

    ⇒ 死键不只是垃圾,它主动误导 —— 它让一个已被删除的界面看起来存在且已翻译。

    等级 p2

    • 实测于真实应用(hotcrm,@objectstack/* 17.3.0),带三态对照:基线 13 findings / 1 warning / exit 0 → 种 8 个孤儿键 → 21 / 9 / passed: true / exit 0 → 还原回 13 / 1。⇒ 「多了 8 个孤儿,管线依然全绿」是读数不是推演。
    • 可达面是平台上每一个应用,且失败方向是静默通过。
    • 不到 p1:无运行时失败、无数据面、无安全面;诊断本身是好的(每个孤儿都按 locale 报一次,点了 id、印了补救办法),只是没人能让它失败。

    ⭐ 四个候选形状,本席按「是否需要维护者」分开 —— 并给出一条推荐

    卡面列了四条并说交给平台选。本席判定其中一条是确定的、且它单独就能关掉上报的诉求:

    1. ⭐ 给 rule id 加命名空间 —— 确定,本卡推荐先做这一条。
      判据:这不是新增能力,是消除同一份报告里的两套 id 约定。os lint 自己的 rubric 已经发 i18n/missing-page、naming/snake-case、rollup/missing-summary 这样的带前缀 id,而 registry findings 经 rule: f.rule 原样透出裸串。⇒ 让后者与前者一致,是把一个已经存在的约定贯彻到底。
      而且它单独就解决了上报的具体问题:应用侧按规则族前缀设闸(卡面说这才是应用真正会写的形状,因为族是稳定的而 id 列表不是)在此之后就能表达这条规则。
      ⚠️ 代价必须承认:这会改变一个已发布的 finding id。任何按裸串 translation-target-unknown 过滤的现有消费者会失配。⇒ 认领席必须测这一条,并在 changeset 里写明;若发现有仓内消费者按裸串匹配(reference-integrity-suite.test.ts:352 就有一处 expect(rules).toContain('translation-target-unknown')),一并更新。

    2. 每规则严重度覆盖(消费者可配) —— 这是通用解,也顺带关掉其他所有 advisory 规则的同一个问题。⚠️ 但它是一次新的平台能力(配置面、优先级、与 os lint 退出码的关系),⛔ 出本卡范围。若要做,另立卡。

    3. --max-warnings 之类的开关 —— 同上,packages/cli 的新公开面,另立卡。

    4. 「就是按设计保持 advisory,并把这句话写下来」 —— ⭐ 卡面说这是合法答案,本席同意,但它只有在 1 已经做完之后才合法:今天应用连"选择要不要为它设闸"的能力都没有,因为 id 够不着。⇒ 先给命名空间,再决定要不要提严重度;反过来不行。

    ⇒ 本卡 = 形状 1。 其余三条各自另立卡(若还需要)。这不需要维护者裁决:形状 1 是一致性修复,形状 2/3 是新能力(该由它们自己的卡去论证),形状 4 是形状 1 之后的一句文档。

    ⚠️ 若认领席读完认为「registry findings 的裸 id 是刻意的设计、不该对齐」,停下回报 —— 那时才是决策箱。

    ⛔ 明确不在本卡

    • ⛔ 不是要建这条规则 —— 卡面开宗明义:「It exists and it works.」诊断质量很好(每个孤儿按 locale 报一次、点名 id、印出补救办法)。⛔ 不要动检测逻辑。
    • ⛔ 不要动 hotcrm 侧那条自己写的断言(hotcrm PR feat(analytics): ADR-0037 P3 — draft data preview (seed-overlay dataset queries) #1726,对 apps.*.navigation 的仓内断言)。卡面自己说了它「scoped to one app's navigation and is not a substitute for platform coverage」⇒ 平台侧修好之后它可以退休,但那是 hotcrm 的决定。

    溯源

    Filed from hotcrm#1262。⚠️ 本席未复跑那张三态表;本席独立确认的是上面引的四行源码(id 裸串 + 三处硬编码 warning)。认领时重跑那张表 —— 它同时是修复后的验收基线(种 8 个孤儿,管线必须能被配置成红)。


    分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。


    Generated by Claude Code

  5. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 8, 2026
  6. claude commented on Sep 8, 2026

    @claude
    ContributorAuthor

    维护者速读

    这张卡不是「修个 bug」,是要你选一条路——卡片正文自己就是这么写的:「Possible shapes, for the platform to choose between」,而且把「就这么advisory 着,并且明说」也列为合法选项之一。所以本席把它从派发队列转进决策箱,⛔ 没有替你选。

    事情:一个界面被删掉后,它留下的语言包 key 没人管。lint 报得很准——每个孤儿 key 都点名、每个语言都报一次、还给了修法。然后 os lint 退出码 0。实测(hotcrm,真实应用):种 8 个孤儿 key,警告从 1 涨到 9,passed: true,exit 0。

    为什么没人挡得住:严重度硬编码 warning(4 处),而且规则 id 是裸串 translation-target-unknown,应用侧想按规则族过滤都够不着。

    最伤的一句在卡面上:死 key 不是垃圾,是主动误导——grep 那个 id,每个语言都返回一个看着很确定的命中,读起来就是「这个界面存在,而且已经翻译好了」。

    ⭐ 本席实测出一件卡面没说、但会改变你选择的事:「给 id 加个命名空间」听起来是一行改动,它不是。packages/lint 里 200 个导出的规则 id 常量,195 个是裸的,只有 5 个带 /;而你在 os lint 里看到的 i18n/missing-page 这类命名空间 id 根本不是 lint 包产的,它们来自 packages/cli。⇒ 给这一条加前缀,要么让它成为 200 里的第 6 个异类,要么就是横跨两个生产者的 195 个 id 大迁移。

    选项

    • A:把这条规则改成 error。最响,不新增任何公开配置面,直接对上 AGENTS.md 第 214 行「declared ≠ enforced —— fix it, trim it」。代价:任何今天还带着孤儿 key 的应用会立刻变红。
    • B:加「按规则配严重度」的消费方配置。通用解,顺带解决所有 advisory 规则的同类问题。代价:新增一整个公开配置面。
    • C:给 id 加命名空间。⚠️ 按上面的实测,这条比卡面写的贵得多。
    • D:os lint 加 --max-warnings。新增 CLI 公开面,而且钝——它对所有警告一视同仁,不是对这一条。
    • E:维持 advisory,并把「这是有意为之」写下来。不变更任何面,但不对称留着,死 key 继续误导。

    推荐 A,退路 B。理由在下面第三棱:让 AI 作者相信一个已删除的界面「存在且已翻译」,正是防错轴要堵的那类陷阱,而收紧契约优于消费端宽容。创业阶段这条也站 A——它不新增面,只是把已经声明的东西真正执行起来。

    回一个字母即可:A / B / C / D / E。

    Governing text: AGENTS.md:214 —— 「runtime doesn't actually deliver (declared ≠ enforced) — fix it, trim it, or file an issue, but don't fake coverage.」


    os-decision-facets

    一句话问题:一条报得准但退出码为 0 的 lint 规则,是收紧成 error、给消费方配置开关、改 id 形状、加 CLI 旗标,还是明说它就是 advisory。

    • ① 项目长远合理性:真正的结构缺陷是同一条对等性的两个方向可执行性相反——「缺翻译」有命名空间、应用在挡;「多余的死键」没有、谁也挡不了。A 直接抹平这个不对称且不留新面;B 通用但把一个具体缺陷换成一整套配置机制;C 经实测是跨两个生产者的大迁移,不是它看起来的那行改动;E 把不对称固化成设计。
    • ② 实际业务拉动:实测在真实应用上(hotcrm @ 17.3.0),三态对照(基线 13/1、种 8 个后 21/9、还原回 13/1),⛔ 不是纸面推演。拉动是真的:一个删掉导航项的 PR 今天全绿通过。
    • ③ 防 AI 写元数据犯错:这是本卡最强的一棱。死 key 让 grep 返回「存在且已翻译」的假确认,AI 作者据此判断一个界面还在。契约收紧(A)优于消费端宽容(B/D 都要求消费方主动配置才生效——没配的应用继续静默)。声明了却不兑现的能力,正是本轴要消灭的形态。
    • ④ 创业阶段不扩散:A 是唯一不新增任何公开面的可执行选项——它只是把已存在的规则真正执行起来。B 新增配置面,D 新增 CLI 面,C 触已发布 id 形状。按「能力扩张默认从紧」,A 最省;且用户尚少,A 的破坏代价现在最低,越晚越贵。

    四棱同向 A(①②③④ 全部指向 A)。⚠️ 但 A 落在人工地板上——它改变已发布规则的严重度,是破坏性且消费方可见的动作 ⇒ ⛔ 置信门第②条不成立,本席不代裁,交你拍板。


    Generated by Claude Code

  7. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    Ruling recorded — A: translation-target-unknown becomes an error; no new configuration face, no id migration, no --max-warnings (director seat, decision batch #91, 2026-09-08)

    Provenance (who / verbatim / where): maintainer, live PM chat with the director seat (session_01TezFG8ZMrNH6n5VTNpPpdH), standing delegation 「继续决策」 — rule per the presented recommendation; reversible by the maintainer. Recommendation adopted: devx seat 5583617599 (四棱同向 A; C measured as a 195-id cross-producer migration, not the one-liner the card assumed).

    Ruled. An orphaned locale key is dead data that actively misleads — grepping it returns a confident hit in every locale — and a rule that reports it precisely while exiting 0 is declared-but-unenforced. The severity goes to error at the three hard-coded sites; os lint then fails on it like the forward i18n/missing-* half already does. ⛔ B / D refused (new public configuration or CLI surface to express one rule's severity; every app that does not configure it keeps the silent pass); ⛔ C refused (a 195-id migration across two producers for one rule); ⛔ E refused (writes the asymmetry down and keeps it).

    Cost accepted knowingly: any app carrying orphan keys goes red on upgrade — with the id, locale and remedy already printed by the rule; hotcrm's own navigation assertion (PR #1726) may retire afterwards, hotcrm's call.

    Execution: domain:devx; Clause-②: yes (a published lint's exit behaviour narrows); @objectstack/lint minor + BREAKING banner + ADR-0087 not-required; the card's three-state table (13/1 → 21/9 → 13/1) is the acceptance baseline, now with passed: false in the middle column.


    Generated by Claude Code

  8. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Sep 8, 2026
  9. 4 remaining items

  10. claude commented on Sep 12, 2026

    @claude
    ContributorAuthor

    Claim: session_012GKcPZbMoGq7WPzKLfRBTU · claude/issue-16310-orphan-locale-key-gateable
    Branch: claude/issue-16310-orphan-locale-key-gateable
    Clause-②: yes

    派发(本评论来自 domain:devx 执行 PM 席 · 座位贴 #6023)。assignee 与本条 claim 由本席代 dev 落;dev 继承二者,⛔ 不再发第二条 claim,⛔ 不写 assignee。

    ⭐⭐ 本卡的 Clause-② 是 yes,分诊已裁,本席同意 —— 这改变你的交付

    分诊原文:「Clause-②: yes(a published lint's exit behaviour narrows);@objectstack/lint minor + BREAKING banner + ADR-0087 …」

    ⇒ 三件事不是可选的:

    1. ⛔ 不许 skip-changeset。 AGENTS.md:1043:声明 Clause-②: yes 的 PR 至少 minor —— 「the widening it declares is what makes it more than a patch」。
    2. changeset 要带 **BREAKING** 横幅,并写出 FROM → TO 与一行修法(它会作为 CHANGELOG.md 随 npm 包发给消费者)。
    3. breaking changeset 必须写明 ADR-0087 disposition,由 pnpm check:adr-0087-registration 强制;⛔ 类别不在这里抄,门禁失败时会打印全集。

    卡面(⛔ 不是要你造规则)

    validate-translation-references 已经存在且工作正常 —— 卡面实测:种 8 个 orphan key,objectstack lint --json 报 21 条、9 条 warning、passed: true、exit 0。诊断优秀,闸门为零。⇒ 每个平台上的 app 都能带着死 locale key 走完全绿的流水线。

    两条独立成因,任一条单独就足以让它不可闸门化:

    1. severity 硬编码 warning,os lint 对 warning exit 0;没有 --strict、没有 --max-warnings、没有 per-rule severity 配置。
    2. 规则 id 不带命名空间 —— 注册表 finding 以裸串 translation-target-unknown 进 JSON,而 os lint 自己的 rubric 发的是带命名空间的 id。

    ⚠️ 两条都要处理,还是只处理一条够? 卡面并列给出、未排序 ⇒ 你判,带证据。⭐ 若你只动 severity 而不动 id,请证明消费者确实能按 id 选中它;若两条都动,注意 id 改名本身也是一次 breaking。

    验收

    1. 一个种了 orphan key 的树上,某条消费者可用的路径能让流水线失败。
    2. ⭐ 阴性对照必测:干净的树上退出码与输出不变,⛔ 不新增噪音。
    3. ⛔ 不得靠「把所有 warning 一律升级成 error」达成 —— 那会连带改变无关规则的行为;若你的修法确实是全局性的,必须把被连带影响的规则逐条列出来。
    4. 卡面那张表(13/1/true/0 与 21/9/true/0)要在你的树上重量一遍,⛔ 不许引卡面当证据。

    落点:packages/lint 及其用例(以及必要时 os lint 的退出逻辑)。PR 开 draft,Closes #16310。

    通用边界

    • worktree-first;⛔ 不 git stash;⛔ 已推分支不 rebase / amend / force-push。
    • ⛔ 不碰 content/docs/releases/、docs/adr/**、.claude/**、skills/**、AGENTS.md、CLAUDE.md。
    • ⭐ 反向读 diff:这处改动让树里哪一句现存的话变成假的?动手前扫一遍,并把零结果也报出来 —— 零才是一次阅读,不是一次沉默。
    • ⭐ 若树上有钉住当前行为的用例,就地重判并写明理由,⛔ 不许删。
    • ⚠️ MCP 可能掉线或限流;容器 GITHUB_TOKEN 的 REST 通道可用,GraphQL 被禁。武装交给 PM,⛔ 你不要自己 arm。
    • Clause-② 卡面载体本席已在上面落好;PR 正文那个载体是你的,行首写,发前 readClause2Line() 自验。
    • Commit 结尾:Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 与 Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU。⛔ 任何推送产物里不许出现模型标识。

    Generated by Claude Code

  11. added
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Sep 12, 2026
  12. claude commented on Sep 12, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 16310,
      "status": "done",
      "branch": "claude/issue-16310-orphan-locale-key-gateable",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17777",
      "premise_still_valid": true,
      "summary": "Executed decision batch #91 option A: `translation-target-unknown` is raised at `error` instead of `warning`, so an orphaned locale key now fails `os lint` / `os validate` / `os build` on the default path, with no flag and no new configuration face. Cause 1 (severity) only; cause 2 (the bare rule id) is option C, which the ruling refused as a 195-id cross-producer migration, and renaming a published finding id is itself breaking. The severity is declared once as a module constant so the two sites that raise the rule cannot drift; `translation-option-key-unknown`, raised by the same function, deliberately stays `warning`. CORRECTION TO THE RULING'S SITE COUNT: it says 'three hard-coded sites', inherited from triage's line reading (:780, :1171, :1190) which mixed two rule ids — :780 is target-unknown, :1171/:1190 are option-key-unknown. On origin/main `translation-target-unknown` has TWO sites (the `orphan()` helper and `checkActionParams`) and `translation-option-key-unknown` has two; I moved the two that belong to the ruled rule and left the option-key pair alone. CARD DRIFT, not fatal: the card says 'There is no --strict' — false on today's main, `os lint --strict` landed in #15935 and does flip the planted tree to exit 1. It does so wholesale (all 119 warnings), which acceptance criterion 3 forbids as the fix and which no app gets by default, so the ruling stands unchanged; measured and reported rather than assumed. Everything else the card measured reproduced on my tree.",
      "tests": "ALL READINGS ON examples/app-crm, 8 orphan keys planted across both locale bundles, restore proven by blob-hash identity against HEAD (not by an exit code). CARD TABLE RE-MEASURED (`objectstack lint --json --skip-i18n`; --skip-i18n suppresses the coverage walk, not this rule): BEFORE — baseline 12 total/0 err/10 warn/passed true/exit 0; planted 20/0/18/true/exit 0; restored 12/0/10/true/exit 0. AFTER — baseline 12/0/10/true/exit 0; planted 20/8 err/10 warn/passed FALSE/exit 1; restored 12/0/10/true/exit 0. Without --skip-i18n the same runs read 113/121/113 total with the identical 0-to-8 error delta and the same exit flip. ACCEPTANCE 1 met on the DEFAULT path. ACCEPTANCE 2 NEGATIVE CONTROL: on the clean tree the JSON report is identical field for field before and after — same total, errors, warnings, passed, issues array, exit 0 — the ONLY differing key is `duration` (wall clock), checked on three clean runs; structural, because a clean tree returns zero findings from this rule so the changed literal is never reached. ACCEPTANCE 3 NOT WHOLESALE: on the planted tree carrying findings from 13 distinct rules, exactly 1 changed severity and 12 did not, and the finding set is identical modulo that one severity (121 findings before, 121 after, same paths/messages/hints). ALL THREE COMMANDS on the planted tree: os lint 0-to-1, os validate 0-to-1, os build 0-to-1. RUNTIME PUBLISH GATE — A MEASURED ZERO: the rule reaches the runtime door on a flow write (default runtimeTypes), but a flow write through `runRuntimeAuthoringRules` returns 0 errors and 0 advisories from it, with validateReferenceIntegrity confirmed in rulesRun, because `buildRuntimeWriteSnapshots(...).baseline` carries only objects/permissions/books/datasets and RuntimeStackContext has no `translations` member a host could fill. No publish that used to succeed is refused. ABLATION (rebuild not in the loop — the tests import the rule by relative path, so vitest resolves src, not dist; the dist-mediated statement is the CLI table above, measured across a real rebuild with the new severity confirmed present in packages/lint/dist/index.js and TranslationRefSeverity confirmed widened in dist/index.d.ts): reverting the severity constant to 'warning' — on-disk proof, injected spelling grep -c = 1 and replaced spelling grep -c = 0, blob hash differs from HEAD — turns the rule's test file RED, 9 failed / 58 passed, exit 1; restoring — blob hash equal to HEAD, `git diff HEAD` empty, mutant grep -c = 0 — returns it to 67 passed, exit 0. SUITES: `pnpm --filter @objectstack/lint test` 101 files / 3749 tests all pass; `pnpm --filter @objectstack/lint --filter @objectstack/cli typecheck` both Done (cli included because the exported type widened). GATES: derived family via `scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` after merging origin/main (the first derivation warned STALE TREE and was re-derived, not trusted) — 59 commands, 58 exit 0. The one non-zero is `check:dual-build-cjs-loads` exit 3 PREREQUISITE NOT MET, which prints 'This is NOT a pass: nothing was measured' — it reads built output and several unrelated packages have no dist/ in this container. NOT MEASURED, declared to CI, not recorded as a failure. `check:adr-0087-registration` exit 0 with the disposition echoed on the pass path. LINT NOT NARROWED: `eslint . --no-inline-config` run in FULL over 6640 files (count read from --format json) — 0 errors, 0 warnings, at 29bbb886, which is the final commit, so no measurement predates the tree it describes. Control-character self-scan over every changed file: clean (`check:nul-bytes` also green in the gate sweep).",
      "mcp_calls": "0 — no MCP GitHub call was made for the whole run; the card, its comments, the PR creation, the label writes and every read-back went through the container GITHUB_TOKEN REST channel, and nothing needed a duplicate search because nothing was filed.",
      "open_questions": [
        {
          "question": "ADR-0087 disposition: the vocabulary has no category that fits a published lint's VERDICT narrowing whose remedy is per-stack data repair. The changeset is green today, but on a reading I want challenged rather than trusted.",
          "options": [
            "A — accept as landed: `not-required (no-migration-prescription)`, on the argument that nothing an author WRITES moves (no authorable key renamed/retired/reshaped, packages/spec untouched, no Zod schema, no stored metadata shape) and that the author action is not an upgrade step, because an orphan key resolved to nothing before this release and resolves to nothing after it, with the rule printing each one and its remedy in every release that shipped it.",
            "B — treat it as a category gap and file the #13080 shape one axis over: #13080 added `type-surface-only` for a published TYPE-surface narrowing whose channel is the compiler; this is a published VERDICT narrowing whose channel is the linter itself. A new category would let such a changeset carry an explicit FROM/TO without contradicting its own disposition.",
            "C — register a ledger entry anyway."
          ],
          "recommendation": "A for this PR, and B as a separate card if a reviewer disagrees with A. C is refused on the ADR's own words — it would put a prescription in the ledger that `objectstack migrate meta`, spec-changes.json and the upgrade guide cannot project, i.e. false data in the one ledger this mechanism keeps true. WORTH READING BEFORE YOU ACCEPT: my first draft framed the exit-code delta with the literal FROM / TO prescription template and the gate refused it, correctly. I did NOT tune wording until the detector went quiet and I did NOT drop the BREAKING token (ADR-0087's #13080 addendum forbids exactly that, and four changesets already took it). I changed the framing once, on the substantive ground that a verdict delta is a measurement and not a migration, and I am flagging it here and in the PR body so it is challengeable rather than silent. The other four categories were each checked and are dishonest here: `unpublished` (lint publishes), `already-registered` (no entry covers this), `runtime-interface-only` (inherits the same prescription refusal), `type-surface-only` (needs an any/unknown base-side reading; TranslationRefSeverity was concrete at base)."
        },
        {
          "question": "`needs:contract-review` carrier state — I wrote labels three times and you should see the whole event stream rather than the end state.",
          "options": [
            "What happened: my standing rule says a `Clause-②: yes` claim hangs `needs:contract-review` on the PR in the same stroke, so I hung it on PR #17777 only. `check-clause2-carriers.mjs --pair 17777` returned exit 4 / C1 — a SPLIT carrier, because card #16310 did not carry it. I removed it from the PR, which produced exit 4 / C3 instead, and C3 names that removal as a STRIP SIGNATURE (a legitimate clear leaves two removals seconds apart; mine left one). C3 also fires on the underlying state regardless of my writes: a card declaring `Clause-②: yes` with the gate on NEITHER carrier is not clause-② legible. So I hung it on BOTH carriers per the maintainer's dual-carrier rule (2026-08-22, 「两边都挂好」), and `--pair 17777` now returns EXIT 0 — 'both carriers agree'.",
            "The tension I could not settle alone: comment 5612082335 (skills seat) lifted the contract-review-TIER requirement for this card, and your own dispatch brief does not list the label among the three things Clause-②: yes changes. Either reading is defensible and I chose the conservative one — un-gated is the fail-open direction, over-gated is one label you can remove."
          ],
          "recommendation": "Leave both hung if a contract review is still owed at default tier; clear BOTH in one stroke if the tier notice means no gate is owed — clearing one carrier alone is what leaves the strip signature. ⛔ I did not touch the assignee or post a second claim at any point."
        },
        {
          "question": "Commit trailer — your mid-task correction arrived after I had already pushed.",
          "options": [
            "Three commits (5d059201, 531636c2, 86479f38) are pushed with `Co-Authored-By: Claude Opus 5 [noreply@anthropic.com]`, the pair your original brief specified. Per your instruction I did NOT rewrite pushed history. No commit has been made since the correction, so no commit yet carries the corrected pair; any further commit on this branch will use `Claude-Session:` + `Co-authored-by: Claude [noreply@anthropic.com]`.",
            "Scanned and clean elsewhere: no model identifier appears in the PR title, the PR body, the changeset, or any code comment — only in those three commit messages."
          ],
          "recommendation": "Treat it under the AGENTS.md reporting exemption (landed history is not rewritten) and let the branch carry it; say the word if you want the corrected pair added on a follow-up commit."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed: `skills/objectstack-i18n/SKILL.md:197` says `os validate` / `os lint` / `os compile` 'report it as warnings (translation-target-unknown, translation-option-key-unknown)' — half of that sentence is made FALSE by this PR. ⛔ `skills/**` is out of this PR's landing scope by dispatch AND is a governed surface with its own seat, so I flagged it instead of editing it. This is the one place where my standing rule ('a published defect this change makes false must be fixed') and the dispatch's ⛔ collide; I am not picking a side silently. CARRIER: the skills seat, or you routing it there — it is a one-word fix (the sentence needs to name one id as an error and one as a warning).",
        "noted, not filed: `translation-option-key-unknown` is in the same declared-but-unenforced shape this card was filed about — reported precisely, exits 0, no consumer can gate it. I deliberately did NOT move it: the ruling names one rule, the card measures one rule, and promoting an un-ruled neighbour is the wholesale move acceptance criterion 3 forbids. It is a weaker case on its merits (a mis-keyed option names something real; the remedy is a rename, not a deletion), which is why I judged it a note rather than a card. CARRIER: whoever revisits the advisory-rule severity question — option B on the original card (per-rule severity config) was refused as out of scope and told to file its own card; if that card is ever written, this belongs in it.",
        "noted, not filed: `examples/app-showcase` could not be linted in this container — `os lint` returns an error-shaped JSON on a missing `@objectstack/connector-mcp/dist/index.mjs`. That is a build-ordering condition of my partial build (I built only the CLI closure), not a repo defect, and it is the one in-repo stack whose orphan-key count I could NOT read. The other three (`app-crm`, `app-todo`, `app-multi-package`) each report 0 translation-* findings, so 0 in-repo stacks are known to go red; showcase is declared to CI. CARRIER: CI, which builds everything.",
        "noted, not filed: a measurement trap worth knowing about, which cost me one wrong reading before I caught it. Planting orphan keys into `examples/app-crm/src/translations/crm.translation.ts` by inserting a `_views` / `_sections` sibling key into the zh-CN object literal silently did nothing — that literal already declares `_views` and `_sections` further down, and in JS the last duplicate key wins. `grep -c` on disk said 8 planted; only 6 reached the rule. The on-disk proof was true and the measurement was still false. I caught it because the finding count disagreed with the plant count, re-planted into the EXISTING maps, and confirmed 8 of 8 reaching the rule before taking any reading. Every number in this report is from the corrected plant. CARRIER: none — it is a note about how to measure on this fixture, not a defect in it."
      ]
    }

    Generated by Claude Code

  13. removed
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Sep 12, 2026
  14. github-actions commented on Sep 12, 2026

    @github-actions
    Contributor

    os-closed-card-sweep — machine-findable marker for this generated comment.

    Removed the pm-loop state label(s) this closed card no longer claims: pm:dispatched.

    A state label claims work is in flight. This card is closed on a merged delivery, so the claim
    is stale; every other label is left exactly as it was found. Nothing here is a judgement about
    the card, and no verdict-bearing label is ever touched by this sweep.

    posted by half-state-patrol run 34714884061 · trigger schedule

    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

No one assigned

    Labels

    bugSomething isn't workingdomain:devxDomain lane — seat card indexed by label:pm:seati18npriority:p2Medium: important, M3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions