Skip to content

fix(cli): os i18n check counts the coverage an app actually owns, so --strict / --threshold can gate an app package - #17223

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-16681-i18n-check-platform-bucket-and-dedup
Draft

fix(cli): os i18n check counts the coverage an app actually owns, so --strict / --threshold can gate an app package#17223
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-16681-i18n-check-platform-bucket-and-dedup

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #16681

Clause-②: yes

os i18n check is the one of three i18n-aware commands that publishes a percentage, and it carried the platform's metadataForms.* baseline in its denominator. An application with every string it owns translated read 38.9%, so --strict and --threshold — the two flags whose entire purpose is CI gating — could not gate an app package at all.

⚠️ Premise check first: half ② is already on main

The card's half ② (collectExpectedEntries emits object action keys twice) was fixed before the card was filed. dedupeByPath landed in 095df7fee3 — "fix(cli): count one translation key once in the i18n coverage population (#15368)", authored 2026-09-04, three days before the card (2026-09-07) and four before triage (2026-09-08). git merge-base --is-ancestor 095df7fee3 BASE exits 0.

Triage located pushEntry correctly and read it correctly — it still does not de-duplicate. What it did not check is the return statement of collectExpectedEntries, which is where the collapse now lives. Measured on this branch's base, for all three carrier shapes, zero repeated paths.

That does not make this card a no-op, and it does not relax the "⛔ 不要拆卡" ruling — it satisfies it. Triage's ordering condition is 「② 先于 ① 落地」; ② landed five days earlier, so ① is landing on a correct count, which is the state the ruling exists to guarantee. What ② still owed is its acceptance: conditions 2 and 3 ask for assertions that did not exist. They exist now, and the ablation below shows them catching exactly the 492-vs-482 defect the card reports.

验收备注

Triage's six conditions (5578569640), transcribed verbatim per 「承接 PR 请照抄进 ## 验收备注」, each with its answer.

  1. ② 先于 ①落地(或至少同一 PR 内先验 ②)。理由见上:先开闸门再修计数,会把一个无害的错误数字变成一个有害的错误闸门。

Met, by a stronger route than asked. ② is not landed in this PR — it landed in #15368 on 2026-09-04 and is in this branch's merge base. So the gate ① opens is opened onto a count that has been correct for five days, never onto a wrong one. Verified rather than assumed: the equality pin and the ablation below both run against this tree.

  1. ② 的验收用卡面那个天然对照:同一份 app 配置下,os i18n extract --json 的键数与 checkexpected.length 必须相等。卡面已给出今天的读数(482 vs 492)与差额构成(十个 objects.*._actions.*:ats_convert_inquiry 四个、ats_reject_inquiry 两个、ats_spam_inquiry 两个、ats_public_apply_link 两个)。⛔ 不要只断言"没有重复",要断言两个输出相等 —— 这才是 docblock 承诺的那件事。

Met. packages/cli/test/i18n-walk-output-parity.test.ts asserts set equality of the key paths, from both published faces, for all three carrier shapes — not merely "no duplicates", and stronger than count equality (the count assertion is there too, because the count is what an operator reads and what the percentage divides by). Neither side is read from the shared walker: extract is read through the extractor's own stackAuthoredSubtree, and check through the finding list it publishes for a declared locale that ships no bundle, where the expected set and the missing set coincide by construction.

  1. ② 的阴性对照:一个把 action 声明在对象上(走 :1197 那条)的 app,和一个声明在顶层并绑定对象(走 :1271 那条)的 app,两者的期望集必须各自完整。一个"把 :1271 那条走删掉"的修法会让第 2 条绿,同时让顶层声明的 action 完全不被要求翻译。

Met. Both shapes are fixtures — actionOnObject() and actionTopLevel() — plus the third shape the normalizer actually produces (actionOnBothCarriers(), one object by reference on both carriers). Each is asserted to reach the complete five-key action set, and one further case asserts the three shapes reach the same key set, so "where you declared it changes nothing about what you are asked to translate" is a single pinned fact. A repair by deletion turns the equality assertions green and reddens those.

  1. ① 取 lint 的默认,而不只是补一个旗标。 卡面把这条排在前面(「the same platform-bucket default lint has — hidden unless asked for — or at minimum the --no-metadata-forms switch」)。理由:lint 已经在默认隐藏并打印提示(「773 i18n issue(s) hidden — rerun with --include-platform」)。三个命令里两个已经知道这件事,⛔ 第三个不该要求作者去发现一个旗标。

Met. No flag is required on either side. The default is lint's principle rather than lint's mechanism, because the outputs differ: lint folds at the report seam and prints a hint; check publishes a percentage, so the same principle has to move the denominator. The flag name and meaning are lint's (--include-platform), and the console prints the same shape of hint under the coverage table.

  1. ① 的阴性对照:平台包自己check 时,metadataForms.* 必须仍然被计入 —— 它们是平台自己要翻译的。一个无条件排除该分组的实现会让 app 侧绿而把平台侧的闸门删掉。

Met, and it is what forbids the unconditional implementation. Ownership is observed, from the one place it is already written down: the stack's own translations bundles. platform-objects' extract config pins metadataForms into every locale bundle it declares, so it stays gated with no flag. A non-empty string leaf is the test, so an --fill=empty scaffold is not mistaken for a claim of ownership. Asserted at the seam (i18n-platform-bucket.test.ts) and end to end (--threshold=100 on a platform-shaped config must exit 1). Ablation A below installs exactly the forbidden implementation and reddens exactly this case.

  1. 三个命令的口径在 PR 里列一张表(extract / lint / check 各自的默认与旗标),⛔ 不要修完之后仍然要靠读三个文件才能知道它们是否一致。

Met — the table is below, and it is also carried in check.ts's module note so the next reader does not have to find this PR.

command what the baseline does to it default opt in / out
os lint adds findings to the report hidden, with a printed hint --include-platform
os i18n extract adds a companion FILE / JSON member emitted (metadataFormsCounts reports its size either way) --no-metadata-forms
os i18n check moves the coverage denominator counted only when this stack ships those translations --include-platform / --no-include-platform

⚠️ The three differ because the outputs differ, and reading the table as three dialects of one setting is the mistake it exists to prevent. Only check publishes a percentage, which is also why it is the one that can answer without a flag at all.

What changed

  • computeI18nCoverage gains platformMetadataForms?: 'include' | 'exclude' | 'auto', defaulting to 'include' — deliberately, because os lint is the other caller and folds the family one seam later off CoverageIssue['source'], counting what it folded for its hint line. Flipping that default would zero lint's hint silently; a pin holds the coupling.
  • stackAuthorsMetadataForms(config) is the observed-ownership predicate, exported and pinned separately.
  • CoverageReport gains platformMetadataForms: { mode, excludedKeys }; the console hint renders from those same two numbers, so --json and the console cannot disagree.
  • os i18n check gains --include-platform with allowNo, so absence is a third state rather than a false.
  • Docs: content/docs/ui/translations.mdx gains "What counts as your coverage" — the page that teaches --strict --threshold as the CI gate now says what is in the number, and that shipping your own metadataForms bundle to move it is the wrong repair. skills/objectstack-i18n/SKILL.md listed metadata forms among the surfaces check always reports on; that sentence is now false, so it is corrected in place.

Evidence

Ablation — directions predicted in the script header before any leg ran; every leg proved the mutation reached disk (anchor occurrence count and a blob hash off the HEAD blob) and proved the restore clean (git checkout HEAD -- ABSPATH, blob equal to HEAD's, empty git diff HEAD), whole script under trap ... EXIT INT TERM with absolute paths. Final git status --porcelain observed empty.

leg mutation predicted observed
A1 'auto' arm returns 'excluded' unconditionally — the forbidden fix red, selectively exit 1, 1 of 10 failed: "⛔ still gates the package that SHIPS the baseline". The nine app-side cases stayed green.
A2 restore green, tree byte-identical exit 0, 10/10; blob c254407… on both sides
B1 collectExpectedEntries returns walked, not dedupeByPath(walked) (pre-#15368) red, and only on the dual-carrier shape exit 1, 9 failed across two files. In the new file exactly the dual-carrier shape's three cases plus the cross-shape case; both single-carrier shapes stayed green.
B2 restore green, tree byte-identical exit 0, 20/20; blob 158aceff… on both sides

⚠️ One prediction was wrong and is reported as observed, not as written. A1 was predicted to redden the negative control and the explicit-request case. It reddened only the negative control — the mutation touches the 'auto' arm alone, and the explicit 'include' / 'exclude' requests route around it. The direction held; the count did not.

B1's failure text is the card's own defect at fixture scale: expected 18 to be 13, the five surplus entries being exactly the objects.inquiry._actions.ats_convert_inquiry.* keys — the same mechanism as the reported 492-against-482 over ten keys.

Tests (exit codes captured by redirect-then-$?, never through a pipe):

  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2exit 0, 192 files / 2658 tests passed.
  • pnpm --filter @objectstack/cli typecheckexit 0 (tsc --noEmit + check:test-typecheck, ledger held at 3 files / 28 errors / 6 pinned signatures).
  • pnpm --workspace-concurrency=2 --filter @objectstack/cli buildexit 0.
  • Integration tier: the new i18n-check-platform-bucket.e2e.test.ts carries the *.e2e.test.* name, so it is a nightly-tier file and the queue does not run it. Driven here under OS_TEST_TIERS=nightly ... --project integrationexit 0, 5/5. The three states are additionally proven per-PR, in-process, through oclif's own Parser against I18nCheck.flags.

Gates — derived on this head with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, reconciled with --ran:

✓ dispatch-gates --ran: 95 derived famil(ies) accounted for — 95 run, 0 NOT-MEASURED.

All 95 exited 0. One needs naming: pnpm check:i18n-coverage first exited 3 — PREREQUISITE-NOT-MET, an unbuilt @objectstack/connector-mcp, ⛔ not a pass. The closure it named was built and it was re-run: check-i18n-coverage: OK (13 config(s), 621 baselined untranslated string(s), none new). That number not moving is the regression reading that matters most here — it is os lint's ratchet over 13 example configs, and this change must not touch it. pnpm check:i18n → exit 0. pnpm check:i18n-walk-parity → exit 0.

⚠️ The reconciliation answers one link only. Outside it, by the tool's own accounting: 44 artifact-roster families, 11 declared-wide families, 6 path-scheduled CI jobs and the always-runs tail. CI owns those.

Lint — the whole repo, not a narrowed run: pnpm exec eslint --no-inline-config --format json . over 6443 filesexit 0, 0 errors, 0 warnings, at 9f9b579.

Published skill readings (skills/** is touched, so both are owed), measured with the gate's own countTokens:

reading before after delta
skills/objectstack-i18n/SKILL.md, lines 493 494 +1
skills/objectstack-i18n/SKILL.md, tokens 4685 4708 +23
package, all SKILL.md, lines 6853 6854 +1
package, all SKILL.md, tokens 79679 79702 +23

check-skills-token-ratchet reads the file at 4708 against a ceiling of 6338 (headroom 1630) and exits 0. The edit is a correction of a sentence this change makes false, not an expansion: one clause replaced by two, one line longer.

Single-writer, measured from the open PR list (21 PRs), each PR's own files against its own merge base via /pulls/N/files: no PR touches any face of this card. The Version Packages PR was paged to completeness (207 files, not the 100-row first page). Positive control: the same predicate reports 7 hits against this branch's own diff, and the coarser packages/cli/ counter fires non-zero on #17073 (src/commands/db/clean.ts) and #17076 (CHANGELOG.md, package.json) — so the zeros are readings, not a silent instrument.

Docs-drift round on this head. Anchor sweep over every symbol the diff touches, plus a prose-class sweep for pages naming none of them (firing control: i18n hits 63 pages under content/docs/). Rows judged and not edited, each for a stated reason: content/docs/protocol/kernel/i18n-standard.mdx names metadataForms only in its orphan-key paragraph — the reverse direction, answered by os validate / os lint / os compile — which this change does not touch, and which in fact states the same ownership fact; content/docs/deployment/cli.mdx documents os lint and never os i18n check, and no sentence on it moves; content/docs/deployment/validating-metadata.mdx names "i18n coverage" as one of lint's rules, and lint is unchanged. ⛔ content/docs/releases/** was not edited.

维护者速读(草稿)

改了什么。 os i18n check 现在只统计这个应用自己拥有的翻译。平台 Studio 表单那 ~773 条(metadataForms.*)由 @objectstack/platform-objects 翻译并由运行时直接下发,不再算进应用的覆盖率分母。归属不是拍脑袋定的,而是从配置自己的 translations 里读出来的:你自己发这套翻译,就还是要你补完;不发,就不是你的。三个命令的口径列成一张表,同时写进了 check.ts 的模块注释。

为什么改。 一个把自己声明的每一个字符串都翻完的应用,报出来是 38.9%,于是 --strict / --threshold 这两个专为 CI 闸门而生的旗标对应用包完全不可用。而唯一能把数字做上去的办法——自带一份 metadataForms 翻译——会覆盖平台那份并在下次升级时变陈旧,规避方案本身有害。三个命令里已经有两个知道这件事,check 是唯一的例外。

风险与代价(含回滚)。 这是放宽接受集:今天被拒绝的一次调用明天会被接受,而且默认的覆盖率数字会变。风险集中在一处——如果归属判定读错,平台侧的闸门就会被悄悄删掉;这正是分诊点名的阴性对照,已经用消融把那个错误实现装进去验证过它会变红。os lint 一行未动,它的棘轮读数(13 个示例配置、621 条,无新增)没有移动。回滚是单一 commit revert,无数据迁移、无生成物、无存量形状变更。判断留给复审的一条:归属判定读的是「有没有发这套翻译」,一个只翻了三条的应用会因此被要求补完剩下的 770 条——这是不是过重,写在 open_questions 里。

席位意见。 (留空,待契约复审档填写)

你要做的。 这是 clause-② 卡,按 SKILL.md:640 禁止入队,PR 保持 draft、挂 needs:contract-review、未挂 auto-merge。它等的是一次席内契约复审档 PASS,不是等 CI。另外这个 diff 触到了 skills/**(受管面),所以按 Prime Directive #14 它的落地本来也需要你亲自确认。


Generated by Claude Code

`collectExpectedEntries` walks the Studio metadata-form registries
unconditionally, so every stack's expected set carries ~773
`metadataForms.*` keys that `@objectstack/platform-objects` translates and
the runtime serves. `os lint` hides them and says so; `os i18n extract` has
`--no-metadata-forms`; `os i18n check` — the one command that publishes a
PERCENTAGE — carried them in its denominator, so an application with its own
surface fully translated read 38.9% and `--strict` / `--threshold` could not
gate it.

Ownership is now OBSERVED rather than assumed: the baseline counts when the
stack itself ships those translations, and does not when it does not. That
keeps the package which owns the family gated with no flag, which an
unconditional exclusion would not. `--include-platform` (`os lint`'s own
spelling) and `--no-include-platform` force either way.

`os lint` is unchanged: the shared seam still defaults to counting the
baseline, because lint folds it away one seam later and counts what it folded
for its own hint line.

Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
Co-authored-by: Claude <noreply@anthropic.com>
…is taught

`content/docs/ui/translations.mdx` recommends `os i18n check --strict
--threshold` as the CI gate; that advice is now reachable for an app package,
and the page has to say what is and is not in the number it produces —
including why shipping your own `metadataForms` bundle to move it is the wrong
repair.

The published i18n skill listed metadata forms among the surfaces `check`
always reports on. That sentence is false as of this change, so it is
corrected in place rather than left to read as a scope statement.

`content/docs/protocol/kernel/i18n-standard.mdx` names `metadataForms` only in
its ORPHAN-key paragraph (the reverse direction, which `os validate` / `os
lint` / `os compile` answer) — untouched by this change and left alone.

Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 11 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/protocol/kernel/i18n-standard.mdx (via os i18n check (command, read off packages/cli/src/commands/i18n/check.ts))
  • content/docs/ui/translations.mdx (via platformMetadataForms (symbol, a field of interface CoverageOptions; a field of interface CoverageReport), os i18n check (command, read off packages/cli/src/commands/i18n/check.ts))
What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bccf311100cad7affccd6cbfcddbd81fe734d97dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from c754029632e9ce2aa713a655b81cde5ba714106f — the merge of head 03ca4f4eba6c0400edaecc47fb12d31cd70eb4fc into base bccf311100cad7affccd6cbfcddbd81fe734d97d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c754029632e9ce2aa713a655b81cde5ba714106f && git checkout c754029632e9ce2aa713a655b81cde5ba714106f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bccf311100cad7affccd6cbfcddbd81fe734d97d 03ca4f4eba6c0400edaecc47fb12d31cd70eb4fc && git checkout -B drift-repro bccf311100cad7affccd6cbfcddbd81fe734d97d && git merge --no-ff 03ca4f4eba6c0400edaecc47fb12d31cd70eb4fc

node scripts/docs-audit/affected-docs.mjs --json bccf311100cad7affccd6cbfcddbd81fe734d97d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs bccf311100cad7affccd6cbfcddbd81fe734d97d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator

Contract review at CONTRACT_REVIEW_TIERVerdict: PASS (audit reading; director seat, summon #18 segment 6, session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-10T00xxZ)

PR #17223 · head 03ca4f4eba6c0400edaecc47fb12d31cd70eb4fc (re-read at posting 00:00:03Z; unchanged since 18:54Z) · card #16681.

  • Reviewed-by: isolated claude-fable-5-1 subagent, transcript-verified (68 harness model stamps, all claude-fable-5-1, zero residue; positive control 56 assistant / 35 user role tokens), adopted verbatim below.
  • Implemented-by: the domain:cli execution PM seat [PM seat] domain:cli — 🟢 os-project-manager · session_015QE8qk46e5CHJxyQEUjbf8 · R71 · (consolidated-seat takeover of 09-09T01:4xZ withdrawn 06:3xZ — see brief) #6024 (session_015QE8qk46e5CHJxyQEUjbf8)'s dev (mode:subagent), branch claude/issue-16681-i18n-check-platform-bucket-and-dedup (newest Claim: 5606003504). Distinct sessions ⇒ not a self-review.
  • Reading for the seat: PASS at tier: the widening (new --include-platform/--no-include-platform flag, platformMetadataForms on --json, relaxed acceptance) is declared, bounded by triage's negative control, and documented; Clause-② yes matches, --pair 17223 exit 0, CI 0 red. Governed: skills/objectstack-i18n/SKILL.md is in the diff ⇒ draft-only, maintainer hand-merge (Prime Directive feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14), no queue. Acceptance caveats, not defects: F1 the dev's local readings cite a SHA that does not exist (9f9b579) so the nightly-tier e2e leg is unpinned on head — obtain one head-pinned run or accept nightly; F2 the observed-included state is silent (follow-up card). Q1 ruled A as built. ⛔ This seat cleared no carrier and touched no PR state at posting.

Verdict

PASS (contract-review tier) — the widening is correctly declared, correctly shaped, correctly bounded by the negative control triage demanded, and its published face is documented. Two acceptance caveats below (F1 evidence pin; F2 follow-up card), neither a contract defect. Landing is draft-only, maintainer hand-merge — the diff touches skills/**.

Head reviewed

03ca4f4eba6c0400edaecc47fb12d31cd70eb4fc — matches the requested prefix; head did not move. Base bccf311100, merge-base a9550bf83b. Two commits: 028f271f9 (fix), 03ca4f4eb (docs). 8 files, +929/−4. Draft, targets main, body line 1 Fixes #16681.

Clause-② reading · claim match · --pair

  • Reading: yes, on both limbs. Mechanical floor alone decides it: new exported symbol stackAuthorsMetadataForms (packages/cli/src/utils/i18n-coverage.ts:262) and a new key platformMetadataForms on the --json payload (check.ts:176 spreads the whole report). Substantively: an invocation that exited 1 now exits 0 on the same argv — accepted set relaxed.
  • Claim match: PR body line 3 Clause-②: yes; newest Claim: (5606003504) Clause-②: yes. Agree.
  • node scripts/pm/check-clause2-carriers.mjs --pair 17223 → exit 0 ("readable in the fixed spelling and both carriers agree"; token present, 3 reads served).
  • Note for the record: i18n-coverage.ts is not in packages/cli's exports map (only ., ./console, ./hook-body) and src/index.ts does not re-export it — the TS additions (CoverageReport.platformMetadataForms required field, PlatformMetadataFormsOption) are internal. The published face is argv + --json + console text.

Governed surface / protocol label

  • Governed: yesskills/objectstack-i18n/SKILL.md (+4/−3). Prime Directive feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14 (AGENTS.md:255): maintainer confirms and merges by hand; no queue, no auto-merge. Governed Surface Queue Guard check is green (PR is not queued).
  • protocol:*: none applylabeler.yml binds them to packages/spec/src/{data,ui,system,ai}/**; no spec file touched. PR labels: documentation, size/l, tests, tooling, needs:contract-review.

CI on head

38 check runs on 03ca4f4eba, all completed; 0 failures. Lint & Repo Gates success (19:18:22Z — this job runs pnpm check:i18n-coverage, lint.yml:5962, so the lint ratchet reading is head-pinned via CI), TypeScript Type Check success, Test Core 1–6 success, Build Docs / Check Changeset / Docs Links / single-writer / Part-of guards all success. Skipped only: Console Pin Gate, Packed-tarball smoke (opt-in), and the duplicate Auto Label / Check PR Size runs. No log pull needed.

Findings

F1 — Dev's local readings are pinned to a SHA that does not exist; the e2e leg is INCONCLUSIVE on head (acceptance caveat, not a contract defect).
The report and PR body pin lint/tests/ablation to 9f9b579 ("= the final head"). git cat-file -t 9f9b579 → not a valid object; the branch contains only 028f271f9 and 03ca4f4eb. Per review-checklist, that is a dead-tree reading in both directions. What CI re-establishes on head: lint, typecheck, unit shards (the two new non-.e2e. test files are unit-project and ran), and the check:i18n-coverage ratchet. What nothing on head establishes: packages/cli/test/i18n-check-platform-bucket.e2e.test.ts (nightly tier per vitest.config.ts:566, not run per-PR) and ablation legs A1/B1. I could not reproduce here (no node_modules/spec/dist in the container). The composition that turns the seam decision into an exit code is pre-existing and unchanged (check.ts:170–176, :240: exit 1 iff totals.errors > 0 || thresholdViolations.length > 0), and the head-pinned unit tests prove coveragePercent === 100 under auto for the fully-translated app and > 0 metadataForm issues for the platform-shaped stack — so the published-command claim is derivable, but the direct reading is unpinned. Recommend the dispatching seat obtain one head-pinned run (OS_TEST_TIERS=nightly … --project integration on 03ca4f4eba) before landing, or explicitly accept nightly.

F2 — The observed-included state is silent; recommend a follow-up card (non-blocking).
resolvePlatformMetadataForms (i18n-coverage.ts:281–293) returns 'included' both for --include-platform and for auto + observed ownership; the report records { mode: 'included', excludedKeys: 0 } in both cases and the console hint prints only when excludedKeys > 0 (check.ts:200). So a stack that overrides a single platform label (a legitimate customization) silently inherits the ~773-key denominator with no console pointer to --no-include-platform — the only signpost is the new docs paragraph. This is the dev's Q1 "cliff", plus the discoverability gap nobody flagged. Ruling on Q1: Option A as built. It is the only reading that satisfies triage's conditions 4 and 5 simultaneously with no flag on either side; B is condition 5's forbidden implementation verbatim; C introduces a second percentage. Follow-up (additive, not this PR): print the same hint shape when included was reached by observation, and/or add reason: 'requested' | 'observed' to platformMetadataForms.

F3 — Correctness vs the card: verified, with controls.

  • Gate passes on a clean app package: unit lets an app with its own surface translated reach 100% (auto → 100%, include → <100%, errors === 0) — head-pinned via CI.
  • Gate still fails on the defect class: --include-platform on the same fixture reproduces the old number (e2e control; unit honours both explicit requests).
  • Negative control (triage [WIP] Fix error in step four of the action run #5): platform-shaped stack under auto{ mode: 'included', excludedKeys: 0 }, metadataForm issues present. platform-objects' real config pins metadataForms into all four locale bundles (scripts/i18n-extract.config.ts:336–339), so the observed predicate fires there without a flag. Empty-scaffold control (--fill=empty) reads as not-authored, with a firing control.
  • os lint untouched: lint.ts:736–742 calls computeI18nCoverage without the option → default 'include'foldCoverageIssues (lint.ts:53–70, keyed on source === 'metadataForm') still counts its hint; pinned by the shared seam keeps os lint whole.
  • Discriminator is exact: source: 'metadataForm' is what metadataType/metadataFormSection/metadataFormField map to (i18n-coverage.ts:361–363); the population filter at :633 and lint's fold use the same key.
  • Ownership and coverage read the same field: stackAuthorsMetadataForms and computeI18nCoverage both read config.translations as [{locale: data}] (:263, :582).

F4 — Scope vs ruling: satisfied; half ② premise verified dead.
git merge-base --is-ancestor 095df7fee3 origin/main → 0; dedupeByPath at i18n-extract.ts:1419/1469, landed 2026-09-04 (#15368), before the card (09-07). The PR lands ②'s acceptance (conditions 2 & 3 — i18n-walk-output-parity.test.ts asserts set and count equality across three carrier shapes, plus the "not repaired by deletion" control) rather than its repair, and says so in body and report (premise_still_valid: false). Triage's six conditions transcribed verbatim and each answered; the 「不要拆卡」 ordering holds because ② predates ①. Condition 4 taken as principle (observed ownership) rather than lint's mechanism (unconditional hide) — correct, since condition 5 forbids the mechanism. Fixes #16681 is right: both "What would close it" outcomes hold on head (PM seat already ruled; concur). No unrelated files.

F5 — Changeset: correct. .changeset/i18n-check-platform-bucket-and-app-gating.md@objectstack/cli: minor. The only touched published package (packages/cli, publishConfig.access: public, files: dist); skills/ has no package.json; content/docs unversioned. Minor is the right level for a new flag + new JSON member + relaxed acceptance with nothing removed. No content/docs/releases/ edits.

F6 — Tests pin the CLI contract, not the implementation. e2e pins exit 0/1, thresholdViolations, the JSON member, and the console strings platform built-ins: / --include-platform. Unit pins the flag declaration (allowNo: true, default undefined) and the three parse states through oclif's own Parser. Seam tests assert relations between two runs of one fixture, never an absolute 773 (so a registry growth cannot red them and a dropped family cannot green them). One wording nit: the parity file's header calls computeI18nCoverage/extractTranslations "published faces"; they are the functions the commands call, not argv — harmless.

F7 — Docs updated; stale-grep clean. content/docs/ui/translations.mdx:247–262 (the page that teaches --strict --threshold as the CI gate) now states what is in the number, both flags, and the JSON member — consistent with the code. skills/objectstack-i18n/SKILL.md:277–283 sentence that this change made false is corrected. Grep on head: no other page lists i18n check flags (deployment/cli.mdx never names the command); i18n-standard.mdx:872–875 is the reverse (orphan-key) direction and remains true; SKILL.md:171 lists metadataForms as a bundle group, not the check population; SKILL.md:386 lists what check "also demands" without metadata forms. Pre-existing, not this PR: i18n-standard.mdx:822–834 shows an example output block in an obsolete format.

Acceptance notes

  • Boundary flags (③): Q1 → A, ruled above (F2 records the follow-up). Q2 → keep Fixes (concur with seat). Q3 → nightly cadence stands as a maintainer cost ruling; the material gap is F1's missing head pin, not the cadence.
  • Pre-landing checks per contract-review.md: ① this PASS; ② --pair = 0; ③ all checks green on head. Then do not queueskills/** puts the merge in the maintainer's hands under Prime Directive feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14. The single-writer guard and both open-PR guards are green on head.

Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Clear-and-hold provenance — director seat, summon #18 segment 6 (session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-10T00:16:54Z). Clearing needs:contract-review on both carriers (#17223 + card #16681) on the strength of the contract-review-tier PASS at #17223 (comment) (head 03ca4f4eba, unchanged). ② --pair 17223 exit 0; ③ 34 check-runs on head, 0 red. ⛔ Governed surface: check-governed-merges.mjs --test exit 3 — the diff touches skills/** (published skill text) — on the governed register. Per Prime Directive #14 this PR stays draft-only for the maintainer's hand merge; ⛔ no ready flip, no queue entry from this seat. Listed under 待维护者 in the segment-6 brief on #12708.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants