Skip to content

fix(cli): report os i18n extract key counts off the emitted bytes - #16247

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-16121-i18n-extract-key-count
Sep 6, 2026
Merged

fix(cli): report os i18n extract key counts off the emitted bytes#16247
os-litant merged 3 commits into
mainfrom
claude/issue-16121-i18n-extract-key-count

Conversation

@os-litant

@os-litant os-litant commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16121

extractTranslations returned counts[locale] as a WALK counter — count += 1 once per expected entry, unconditionally — and os i18n extract spent that number as the size of the file it had just written. Under the default --objects-only the module holds only the objects sub-tree, so the two are different numbers.

Reproduced on this branch's base

f5aec38a6af — i.e. after #14894 / #16120 landed, so the fixture is taken against the emission set as it ships today. One object with one field, one app, i18n.defaultLocale: 'zh-CN':

  Skeleton summary
    zh-CN      776 key(s)  (of 776 expected)  + 773 metadataForms key(s)
  Wrote OUT/zh-CN.objects.generated.ts (776 keys)

The file that run wrote holds 2 leaves. The + 773 is the same 773 already inside the 776, so the line reads as 1549 out of 776 and the true split (2 objects + 1 app + 773 baseline) cannot be derived from it. After:

  Skeleton summary
    zh-CN      775 of 776 key(s) emitted   objects 2 · metadataForms 773
  Wrote OUT/zh-CN.objects.generated.ts (2 keys)
  Wrote OUT/zh-CN.metadata-forms.generated.ts (773 keys)

The four sites, located by symbol

Triage's line numbers were exact at 932acc3d; #16120 landed in the command file since, so three of the four had moved by +45 lines at this branch's base. Derived here, not recalled:

site at 932acc3d at f5aec38a6af
count += 1 in src/utils/i18n-extract.ts 1819 1819
keys: result.counts[locale] 313 358
const mfTail = … 269 314
(of ${result.totalExpected} expected) 272 317

Two further result.counts readings that triage did not list belong to the same conflation and are repaired with it: :354 (if (result.counts[locale] > 0), the gate deciding whether the stack module is written at all) and :256 (the same expression deciding which sections the --source-hashes companion commits).

What counts means now, and why this shape survives a third mode

The old field was doing two jobs, and it did the second one wrong. It is worth saying what it was as a reading: count incremented once per entry with no continue anywhere in the loop, so counts[locale] === entries.length === totalExpected for every locale on every config. It could not disagree with anything, and no assertion over ExtractResult could have failed while the printed number was wrong by two orders of magnitude.

  • ExtractResult.counts[locale] is now a leaf count of bundles[locale] — the whole skeleton built for that locale, taken off the tree instead of off the walk that built it, and documented as explicitly not the size of any one file. That is the meaning its own doc comment already claimed and the meaning i18n-duplicate-demand.test.ts already pinned (result.counts.en === leaves(result.bundles.en)); that pin now holds by construction rather than by coincidence.
  • Which sections of the skeleton become files stays the command's decision, so the command takes every count it reports off that module's own payload, selected with a new exported translationModulePayload(data, kind) — the same function renderTranslationModule renders from. The number and the bytes are therefore one expression apart, including for a kind added later.
  • Nothing subtracts one count from another at a print site. Subtracting mfN where the tail is printed repairs today's two modes and leaves a future --apps-only wrong in exactly the same way; deriving each number from the payload leaves nothing to repair.
  • The summary is a partition, not a sum: E of S key(s) emitted plus a per-module breakdown. The modules are disjoint sub-trees of the skeleton (objects is a sub-selection of stack, and stackAuthoredSubtree excludes metadataForms), so E never exceeds S and the gap is exactly what a flag excluded. A module a flag SUPPRESSED is named in the breakdown too, with its size and the words not emitted that keep it out of E.

Two consequences of the same conflation, in scope because the repair forces them

  • A module with no leaves is no longer written. The emit gate was a property of the skeleton, so on a stack whose only surface is apps the default --objects-only wrote a zh-CN.objects.generated.ts holding {} and announced it as 774 keys (driven at f5aec38a6af). Once the printed number is the file's own leaf count, the alternative to moving the gate is announcing Wrote … (0 keys) for an empty file. The sibling pin i18n-extract-emitted-files.test.ts already spelled the rule this way in its own mirror.
  • --json's counts now counts the bundles payload beside it. No key is added or removed; the skeleton total is still reported, under its own name, as totalExpected. ⚠️ This is not the relationship metadataFormsCounts has to metadataForms — see the patch round below — and neither face of the baseline moves.

Blast radius: enumerated before the first edit

The card says this changes stdout for every extract invocation, so the enumeration came first. Searched the whole tree for Skeleton summary, key(s), keys), Wrote , metadataFormsCounts, totalExpected, result.counts and .counts[, excluding CHANGELOGs.

  • No test, snapshot or fixture anywhere asserts the human-readable extract stdout. The only suite that captures it at all, i18n-extract-metadata-forms-flag.e2e.test.ts, returns stdout from its helper and never asserts on it.
  • Three assertions touch the numbers, all still green: metadataFormsCounts on the --json payload (i18n-extract-metadata-forms-flag.e2e.test.ts, unchanged by this diff), counts/totalExpected against a structural leaf count (i18n-duplicate-demand.test.ts), and totalExpected === 1 (i18n-extract.test.ts).
  • No gate or script parses a count line. check-i18n-bundles, check-i18n-coverage, check-i18n-stale-fill and check-i18n-walk-parity invoke os i18n extract, but read only its stderr undeclared-key findings and its --check exit status. The nine i18n-extract.config.ts files carry flags, not expectations.
  • No committed bundle moves: all nine extract configs run under the default --objects-only on stacks that do author objects. pnpm check:i18n reports OK (9 package(s) — all bundles in sync, no undeclared authoring keys), re-run at the delivered head after a full build.

The pin, and what would make it fail

packages/cli/test/i18n-extract-key-count.e2e.test.ts spawns the real CLI and compares each printed number against a structural leaf count of the module parsed back off disk, in four flag states. That comparison is the thing the defect precluded, so the pin is written to be able to fail:

  • every Wrote … (N keys) line against the leaves of that file — falsified by restoring keys: result.counts[locale];
  • the summary as a partition (emitted equals the files' leaves together, and never exceeds the skeleton) — falsified by any re-appearance of the + N metadataForms tail;
  • --json's counts against the leaves of bundles[locale], driven in both sub-tree modes because the old arrangement is right in the wide one and wrong in the narrow one, and now in both --metadata-forms states as well;
  • a flag-suppressed module named with its size and kept out of the total — falsified by dropping it from the row, or by adding it in;
  • the empty sub-tree writing no file — falsified by restoring the old emit gate.

Reverse verification. With the fix committed, both source files were restored to f5aec38a6af over it and the pin re-run: all 7 cases red, including AssertionError: expected 776 to be 2 on the --json case and expected [ 'zh-CN.objects.generated.ts' ] to deeply equal [] on the empty-sub-tree case. The mutation was proven on disk by blob hash before the run (git hash-object differing from the HEAD blob, plus the count += 1 marker present and translationModulePayload absent) and the restore proven the same way afterwards (git status --porcelain empty, git diff HEAD empty, both blobs byte-identical to HEAD). No rebuild leg is involved: bin/run-dev.js imports ../src/… through tsx, so the mutated source is what ran — the seven red cases are themselves the evidence it reached the run.

Patch round after contract review

Four items, none of them a rethink of the count design.

  1. The pin did not typecheck, and it never had. runExtract / runJson took flags: string[] while the partition table is as const, so the call site handed them a readonly tuple — TS2345 at test/i18n-extract-key-count.e2e.test.ts(203,59), present since the first commit. The parameters only ever read, so they are readonly string[] now and the table keeps its literal types. The debt ledger is untouched; that gate's own text marks the ledger route maintainer-only.

    Why the first report called this green, precisely. Not a type-blind gate: pnpm --filter @objectstack/cli typecheck catches it, and the ablation below proves that on this exact file. The reading was taken at the wrong tree — it was run once, immediately after editing the two source files and before the pin file existed, and that exit 0 was then carried into a report about a head that contained the pin. A count attributed to a tree it was not measured on is the defect this card is about, sitting inside the verification of the fix for it.

  2. A false claim that would have shipped to CHANGELOG.md. The changeset, this body and the --json code comment all said the new counts/bundles relationship was "the relationship metadataFormsCounts already had to metadataForms". It is not: metadataFormsCounts reports the baseline as built, emitted or not, so under --no-metadata-forms the payload carries metadataFormsCounts: {'zh-CN': 773} beside an empty metadataForms map. Corrected in all three places. The --json face therefore does carry two count semantics — counts is what was emitted, metadataFormsCounts is what was built. Whether it should is left to the maintainer: nothing here decides it and neither face moves. The claim survived unmeasured because the --json case drove --metadata-forms ON only; it now drives both states, which is what would have caught it.

  3. An information regression on the common path — fixed by showing it, not by dropping the promise. Under --no-metadata-forms, which 8 of this repo's 9 extract configs pass, the row named nothing at all, while the old double-counting line at least told the operator how big the baseline was. Chosen because that reading is worth keeping and the code comment above metadataFormsCounts promises it. Implemented mode-agnostically — a module a flag suppresses is a candidate that is reported but not written, rather than a special case at the print site, so a later sub-tree mode is handled by the same list:

      zh-CN        2 of 776 key(s) emitted   objects 2 · metadataForms 773 not emitted
      zh-CN        0 of 774 key(s) emitted   metadataForms 773 not emitted
    

    It cannot be read as a sum: E of S is stated first and the suppressed term carries the words that keep it out. The tone also read green on 0 of 774 emitted; green means there is nothing to translate, which is a property of the skeleton, so it now reads the skeleton and that row is yellow.

  4. Should-fix.

    • Done — the emitted-files mirror judged its --no-objects-only arm on the whole bundle while the command judges it on the stack-authored subtree. They diverge on a bundle with no authored surface; the mirror subtracts the baseline too, and a case now drives that input class. It stays a re-implementation rather than an import: a mirror that calls the thing it mirrors cannot disagree with it.
    • Deferred, noted on cli: os i18n extract --no-objects-only --source-hashes drops every non-objects provenance record — the committed-section list is the literal 'objects', not the sections the module holds #16242 — under --source-hashes the same input class now yields a provenance companion with no bundle module beside it. Driven at this head: Wrote OUT/ja-JP.source-hashes.generated.ts (0 keys) / Generated 1 file(s). Unreachable before this PR, since the stack module was always written, and it is the same committed-section-list mechanism as that card. Not repaired here.
    • Done — the tone on 0 of S emitted, above.

Verification

  • Gate union derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (two runs; --commands does not print the reconciliation line): 57 families, and the --commands harvest is 57 lines — asserted against the tool's own Reconciliation — 57 famil(ies) line. All 57 run and green, at 16fb27d1bc4. Five of them exited NOT MEASURED on the first pass for want of the built dist closure — check:i18n, check:i18n-coverage and check:dual-build-cjs-loads at exit 3, check:i18n-walk-parity at exit 1, and check:type-check-debt at exit 3 from an OOM under a 4096 MB ceiling — and all five went green when re-run after pnpm build, the last under its own declared 6144 MB ceiling. None was a finding.

  • The 39-family Artifact rosters block, run separately at the same commit: 36 green; check-partof-closing-keyword and check-single-claim-paths NOT WIRED (exit 2 — they need PR_BODY/PR_NUMBER, and their pnpm check: wrappers exit 0); check:react-declaration-parity a prerequisite miss (exit 1 — it needs the objectui manifest and a browser dump).

  • Those two figures name 16fb27d1bc4, and the head has since moved twice. At the delivered head b7afc733b30, quoted from the gate's own verdict rather than from an exit code:

    $ pnpm --filter @objectstack/cli typecheck        # exit 0
    check:test-typecheck: OK — @objectstack/cli's test layer compiles under
    packages/cli/tsconfig.test.json; 3 file(s) / 28 error(s) / 6 pinned signature(s)
    held in test-typecheck-debt.json
    

    That green is a measurement, not a blind spot: restoring only the pin file to e528ee122a5 over this head — mutation proven on disk by blob hash, restored with git checkout HEAD -- and proven back by blob hash — makes the same command exit 1, naming test/i18n-extract-key-count.e2e.test.ts: 1 type error(s) in a file the ledger does not cover.

  • Also re-run at b7afc733b30: the extract suites (6 files / 55 tests), check:nul-bytes, check:cli-test-child-env, check:cross-package-test-inputs, check:test-source-alias, check:doc-authoring, check:objectql-double-limit, check:where-matcher, check:type-check-coverage, check:changeset-gate-self-tests, check-empty-changeset, check-changeset-no-major, check-adr-0087-registration, check-changeset-fixed — all exit 0. The three i18n families needed the built closure again (exit 3 / exit 3 / exit 1, NOT MEASURED) and were re-run after pnpm build: check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys), check-i18n-coverage: OK (13 config(s), 621 baselined untranslated string(s), none new), check-i18n-walk-parity: 11 declared group(s), 8 walked, 3 exempted.

  • The full @objectstack/cli suite was green at e528ee122a5 — 268 files, 3184 passed with 6 expected failures — and has not been re-run since; the patch round touches one command file and two test files, all re-run above.

  • Lint, narrowed to the diff and declared as such: 0 errors / 0 warnings, none of the lintable files ignored (read from ESLint's own isPathIgnored, not guessed); the changeset .md is outside the config's extension globs. The narrowing is a measurement rather than a skip because eslint.config.mjs enables no type-aware linting for any file, so this diff cannot move any rule's verdict on a file it does not touch.

Governed surfaces

None. The diff is packages/cli/** plus one .changeset/*.md; it touches no docs/adr/**, .claude/**, skills/**, AGENTS.md or CLAUDE.md.

Out of scope, filed

#16242 — under --no-objects-only --source-hashes the provenance companion drops every non-objects record, because the committed-section list is the literal 'objects' rather than the sections the module holds. Measured: a 3-leaf module beside a 2-record companion. The orphan-companion class from the patch round above is noted on the same card. Both change emitted companion bytes, so both are filed rather than ridden along.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

`extractTranslations` returned `counts[locale]` as a walk counter and the
command spent it as the size of the file it had just written. Under the
default `--objects-only` those are different numbers: on a one-object,
one-app stack the run announced `Wrote …objects.generated.ts (776 keys)`
for a file holding 2 leaves, and summarised it as `776 key(s) (of 776
expected) + 773 metadataForms key(s)` — appending a number the 776
already contained.

`counts` is now a leaf count of the locale's skeleton, taken off the tree
rather than off the walk, and documented as not being any file's size.
Every count the command reports is `countTranslationLeaves` of that
module's own payload, selected with the new `translationModulePayload` —
the same function the renderer renders from, so a count and its bytes
cannot drift apart, including for a sub-tree mode added later. The
summary is a partition of the skeleton (`E of S key(s) emitted` plus a
per-module breakdown), never a sum over it, and nothing subtracts one
count from another at a print site.

Two consequences of the same conflation go with it: the emit gate is now
the module's own leaf count, so a stack with no objects no longer writes
an empty module under `--objects-only`; and `--json`'s `counts` now
counts the `bundles` payload beside it, as `metadataFormsCounts` already
counted `metadataForms`.

The pin spawns the real CLI in four flag states and compares each printed
number against a structural leaf count of the module parsed back off
disk — the comparison the defect precluded.

Fixes #16121

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
@github-actions

github-actions Bot commented Sep 6, 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 metadataForms (literal, a string literal in I18nExtract; a string literal in TranslationModuleKind; a string literal in renderTranslationModule; a string literal in translationModulePayload), os i18n extract (command, read off packages/cli/src/commands/i18n/extract.ts))
  • content/docs/ui/translations.mdx (via os i18n extract (command, read off packages/cli/src/commands/i18n/extract.ts))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via os i18n extract (command, read off packages/cli/src/commands/i18n/extract.ts))
  • content/docs/releases/v16.mdx (via os i18n extract (command, read off packages/cli/src/commands/i18n/extract.ts))
  • content/docs/releases/v17.mdx (via os i18n extract (command, read off packages/cli/src/commands/i18n/extract.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 22 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 77781151df55e83d42b09e5917956e2d20d80b86packageMentionDocs.

Which tree this was computed on

This run read content/docs from aa237319e64545940788e6a954589378bd2a1316 — the merge of head b7afc733b30de3c6a5f149d6d6293239462503f1 into base 77781151df55e83d42b09e5917956e2d20d80b86, 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 aa237319e64545940788e6a954589378bd2a1316 && git checkout aa237319e64545940788e6a954589378bd2a1316
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 77781151df55e83d42b09e5917956e2d20d80b86 b7afc733b30de3c6a5f149d6d6293239462503f1 && git checkout -B drift-repro 77781151df55e83d42b09e5917956e2d20d80b86 && git merge --no-ff b7afc733b30de3c6a5f149d6d6293239462503f1

node scripts/docs-audit/affected-docs.mjs --json 77781151df55e83d42b09e5917956e2d20d80b86

⚠️ 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 77781151df55e83d42b09e5917956e2d20d80b86 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Contract review (clause ②) — CHANGES REQUESTED

Reviewed by an isolated subagent at the contract-review tier (claude-fable-5-1), dispatched by the domain:cli execution PM seat (#6024). 转录档位核验 PASSED: 92 harness-stamped model fields, all claude-fable-5-1, no fallback stamp. Verdict adopted verbatim. The brief carried the card, the triage ruling and the PR only — no dispatch order, no seat conclusions.

Clause ② confirmed yes / yes, derived from the diff before reading the PR's grading, with the same three reasons the implementer gave plus a fourth it did not: under --source-hashes the same input class now yields a provenance companion with no bundle module beside it (Wrote zh-CN.source-hashes.generated.ts (0 keys) / Generated 1 file(s)). patch confirmed correct, with the falsifier named: a documented consumer of --json.counts or of the summary line would flip it to minor, and none exists in the tree or in content/docs.

The repair itself is sound and was verified independently, not read: 26 module/companion counts, 12 summary rows, 8 breakdowns and 12 --json entries measured on the emitted files across four flag states — every printed number matched the bytes, leaves counted by importing each emitted module rather than by regex. And the pin can fail: restored to the fork point, it went 7/7 red, restore proven by blob hash.

⛔ The blocking defect — and it falsifies a claim in the PR body

pnpm --filter @objectstack/cli typecheck is RED at this head. This seat verified it on CI rather than taking the review's word:

check conclusion
TypeScript Type Check failure
Type Check · workspace failure
Type Check · debt ledger success
test/i18n-extract-key-count.e2e.test.ts(203,59): error TS2345:
  Argument of type 'string[] | readonly ["--no-objects-only"]' is not assignable to parameter of type 'string[]'.

The as const on the partition loop hands a readonly tuple to runExtract(flags: string[]). It was present in the first commit, so it is not a late regression.

And the review explained why the implementer's green list was consistent with a red typecheck — this is the reusable part: "the debt gate's program never reads test/… vitest strips types, so the pin passes at runtime while the gate is red."another reading that cannot fail, this time inside the verification itself. A battery of green gates, none of which looked at the test layer, beside a runtime pass that is blind to types by construction. That is the same shape as the defect this card is about, one level up.

Also required

A false claim ships in the changeset, which reaches consumers as CHANGELOG.md: "the relationship metadataFormsCounts already had to metadataForms" does not hold under --no-metadata-forms, where the payload carries metadataFormsCounts: {en: 773} beside metadataForms: {} — measured, and pinned by a sibling suite. The --json face now holds two count fields with two semantics (counts = emitted, metadataFormsCounts = built-regardless). ⚠️ The new pin drives --json with metadata forms ON only, so the claim was never measured.

An information regression on the common path: under --no-metadata-forms — which 8 of 9 repo configs run — the summary row is 9 of 785 key(s) emitted with no breakdown, where the old line, for all its double counting, did print + 773 metadataForms key(s). The code comment promising the operator can still see the suppressed baseline is not delivered on the human face.

Three should-fixes in the same round: align the emitted-files mirror (its stack predicate is countLeaves(data), the command's is stackAuthoredSubtree(data) — they diverge on an empty-authored stack under --no-objects-only); note or fold the orphan-companion edge into #16242; reconsider the green tone on 0 of S emitted.

For the maintainer, raised by the review and not decided here

Should --json carry two count semanticscounts meaning "emitted" and metadataFormsCounts meaning "built regardless"?

⛔ Not blocking, and ⛔ not this seat's to settle. Whichever way it goes, the text must stop claiming symmetry.

The patch round is dispatched. needs:contract-review stays hung on this PR and on card #16121.


Generated by Claude Code

…eport suppressed modules

Four repairs from contract review, none of them a rethink of the count design.

1. `runExtract`/`runJson` took `flags: string[]` while the partition table is
   `as const`, so the call site handed them a readonly tuple: TS2345 at
   test/i18n-extract-key-count.e2e.test.ts(203,59). The parameters only ever
   read, so they are `readonly string[]` now — the table keeps its literal
   types. Present since the first commit, and green under every gate that was
   re-run at that head because each either skips the test layer or is
   type-blind.

2. The changeset, the PR body and the `--json` comment all claimed the new
   `counts`/`bundles` relationship was "the relationship `metadataFormsCounts`
   already had to `metadataForms`". It is not: `metadataFormsCounts` reports
   the baseline as BUILT whether or not it is emitted, so under
   `--no-metadata-forms` the payload carries a positive count beside an empty
   `metadataForms` map. The claim is corrected in all three places and nothing
   about either face moves; whether `--json` SHOULD carry two count semantics
   is left to the maintainer. The `--json` case now drives `--metadata-forms`
   in both states, which is what would have caught the claim.

3. The summary dropped an operator reading on the commonest path:
   `--no-metadata-forms` is what 8 of this repo's 9 extract configs pass, and
   there the row named nothing at all. A module a flag suppressed is now a
   CANDIDATE that is reported but not written — named with its size and the
   words `not emitted`, so it stays out of the total:

     zh-CN        2 of 776 key(s) emitted   objects 2 · metadataForms 773 not emitted

   That is mode-agnostic: a later sub-tree mode is a candidate like any other.
   The tone also read green on `0 of 774 emitted`; green means there is nothing
   to translate, which is a property of the skeleton, so it now reads that.

4. The emitted-files mirror judged its `--no-objects-only` arm on the whole
   bundle while the command judges it on the stack-authored subtree. They
   diverge on a bundle with no authored surface; the mirror subtracts the
   baseline too, and a case drives that input class.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N

Copy link
Copy Markdown
Collaborator Author

⛔ Correction — the mechanism this seat published two comments ago is falsified

Head b7afc733b30. Before anything about the patch round: the explanation this seat adopted and called "the reusable part" is wrong, and the implementer falsified it by measurement rather than letting it stand.

What this seat published:

"the debt gate's program never reads test/… vitest strips types, so the pin passes at runtime while the gate is red" ⇒ "another reading that cannot fail, this time inside the verification itself. A battery of green gates, none of which looked at the test layer."

What the ablation shows. The implementer restored only the pin file to the previous head over the new one — mutation proven on disk by blob hash (9ebe0d6f74f1 against e1114128af64) — and re-ran pnpm --filter @objectstack/cli typecheck. It exited 1, naming test/i18n-extract-key-count.e2e.test.ts: 1 type error(s). ⇒ the package's own typecheck does see that file and does red on it. The gate was never blind.

The actual cause, in the implementer's words:

I ran that command exactly once, right after editing the two source files and BEFORE the pin file existed, then carried its exit 0 into a report about a head that contained the pin. A reading taken on one tree and attributed to another — this card's own defect, sitting inside the verification of the fix for it.

⇒ simpler than the published explanation, and worse. ⭐ It is also precisely this card's defect class: a number that describes one thing, reported as describing another. The card is about a printed count that did not describe the file it named; the verification failure was a gate result that did not describe the head it named.

⚠️ Bearing on the review, stated exactly. The review's verdict stands and was right — CI was red, this seat verified that independently, and the TS2345 was real. What is falsified is one paragraph of its explanation of how the implementer's green list could coexist with it. ⛔ This is not the parent session rewriting an adopted review — it is a later measurement, by a third party, correcting a mechanism claim. The verdict is untouched; the mechanism paragraph should not be quoted again.

Practice changed, and this seat is adopting it too: run the gate at the head being reported, quote the gate's own verdict line, and prove the green can fail. The implementer did all three this round:

check:test-typecheck: OK — @objectstack/cli's test layer compiles under packages/cli/tsconfig.test.json;
3 file(s) / 28 error(s) / 6 pinned signature(s) held in test-typecheck-debt.json

exit 0 at b7afc733b30, with the failing-direction ablation above as its control.

The four requested items

① TS2345 — fixed at the root, not at the call. runExtract and runJson took flags: string[] while the partition table is as const; both parameters only read, so both are now readonly string[] and the table keeps its literal types. ⛔ The debt ledger is untouched.

② The false symmetry claim — corrected in all three places (changeset, PR body, the --json comment), and ⭐ the root cause closed: the --json case drove --metadata-forms ON only, which is why the claim was never measured. It now drives OFF as well and pins the asymmetry directly — the observation that would have caught it.

③ Chose to SHOW the suppressed baseline, keeping the comment that promises it. The reasoning is sound and this seat endorses it: "8 of the 9 extract configs pass --no-metadata-forms, so the bare row was the commonest output the command produces… Losing a true reading to fix a false one is a bad trade when both can be had." ⭐ And it is done mode-agnostically rather than as a print-site special case: a suppressed module is a candidate carrying an emitted flag; the summary reports every candidate and sums only the emitted ones. A later sub-tree mode is a candidate like any other, so no number becomes arithmetic over another — the same principle that governed the original repair.

--no-metadata-forms          zh-CN     2 of 776 key(s) emitted   objects 2 · metadataForms 773 not emitted
apps-only, --no-metadata     zh-CN     0 of 774 key(s) emitted   metadataForms 773 not emitted
default                      zh-CN   775 of 776 key(s) emitted   objects 2 · metadataForms 773

④ The should-fixes. The emitted-files mirror is aligned — and ⭐ deliberately kept a re-implementation rather than an import: "a mirror that calls the thing it mirrors cannot disagree with it." The orphan companion is deferred and measured onto #16242 rather than described. The green tone on 0 of S emitted now reads the skeleton rather than the emitted total — "reading it green was the same conflation this card is about."

⇒ A delta review is dispatched, scoped to what moved since e528ee122a5: the summary is a shipped face and this round changes it again, on top of a face the earlier review already graded. needs:contract-review stays hung on this PR and on card #16121.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Landing — provenance · domain:cli execution PM seat (#6024)

Head b7afc733b30de3c6a5f149d6d6293239462503f1, card #16121. Carriers stripped, flipping ready, arming auto-merge.

落地前检三条

① 席内契约档 PASS 在案. Delta review at claude-fable-5-1PASS, the prior CHANGES REQUESTED verdict's requirements discharged and its verdict extends onto this head. 转录档位核验: 90 harness-stamped model fields, all claude-fable-5-1, no fallback. Adopted verbatim. It ruled each of the four requirements individually rather than accepting a summary, and drove five mutations of its own — each turns the new pins red, including one that reddens the specific assertion the patch added.

The typecheck green was verified with its own falsifier, by the reviewer independently: exit 0 at head with the verdict line quoted, and then — restoring only the pin file to e528ee1, blob-hash proven — the same command exits 1. A green that was shown capable of failing.

② 双载体已清. PR before: documentation, size/l, tests, tooling, needs:contract-review → after: the same minus the carrier. Card #16121 before: bug, priority:p2, pm:dispatched, domain:cli, needs:contract-review → after: the same minus the carrier. Both read back after the write.

⚠️ The machine read check-clause2-carriers.mjs --pair 16247 is exit 3 in this seat, as it is for every PR here — the classifier check-half-states.mjs --probe names the cause ("the transport authenticates but repo-scoped reads are refused") and the remedy ("in a proxy-mediated seat, repo-scoped reads stay on the mcp__github__* tools"). Both carriers were read on that route. ⛔ Exit 3 is not reported as clean.

③ 全部 check 全绿. perPage=100, 36 of 36 completed — 33 success, 3 skipped, zero failures, zero in flight. Both jobs that were failing at the previous head — TypeScript Type Check and Type Check · workspace — are success here. The reviewer independently tied CI to the sha: run 34021647122 at head_sha b7afc733b30 success; the same workflow at e528ee122a5 failure.

What the delta review verified that this seat could not have

Nine states byte-diffed. It checked out the command file at e528ee1 and re-ran four stack flag states, three apps-only and two --source-hashes: every emitted file, companions included, byte-identical to head; file sets identical; only the stdout rows differ. ⇒ the prior review's module/companion measurements still hold, because the delta did not move the bytes they were taken on.

And it found a second half to the mechanism correction. This seat posted that the earlier explanation was falsified; the reviewer confirmed it independently — "the cli typecheck reads test/ through tsconfig.test.json and reds on the pin" — while noting the prior verdict was right. Both halves now rest on measurement rather than on either seat's account.

⚠️ One risk this seat is recording rather than waving through

The head commit's message still carries the falsified sentence"green under every gate that was re-run at that head because each either skips the test layer or is type-blind" — contradicting the corrected PR body and the report. It cannot be repaired: removing it needs a force-push, which is barred on this lane.

The reviewer judged it non-blocking because "the repo squash-merges through the queue … so the squash commit takes the PR title/body". ⚠️ This seat cannot confirm that half. Four times tonight enable_pr_auto_merge was called with SQUASH and reported back method: MERGE. Those may be consistent — allow_squash_merge=true says squash is permitted, and the queue's own method is configured separately — but this seat has not measured what the queue does. ⇒ If it merges rather than squashes, a falsified sentence enters history.

⛔ Not treated as a reason to hold the PR: the claim is in a commit message, reaches no published surface, and the corrected text is in the body and in the changeset that becomes CHANGELOG.md. Recorded here, and it is another instance of #16158 — the commit-trailer/squash gap already with the maintainer.

The one thing worth carrying out of this card

The defect was a printed count that did not describe the file it named. The verification failure was a gate result that did not describe the head it named — the implementer's own diagnosis, reached by ablation rather than by argument:

A reading taken on one tree and attributed to another — this card's own defect, sitting inside the verification of the fix for it.

Both are now closed the same way: every number the command prints is derived from the payload it describes, and every gate verdict quoted at this head was re-run at this head with a falsifier beside it.

⚠️ Left with the maintainer, unchanged: whether --json should carry two count semantics (counts = emitted, metadataFormsCounts = built regardless). The text no longer claims they are the same relationship, which is what the review required either way.


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