fix(platform-objects): nine es-ES and ja-JP metadata-form leaves say what their source says - #16819
Conversation
…what their source says The English source for these nine leaves moved after es-ES and ja-JP were translated from it. Bundle merge fills gaps only, so a present-but-stale translation is never refreshed, and the recorded-source-hash table drops a leaf's entry as soon as a translator writes real text -- which makes every one of these nine LEGACY-TRUSTED and invisible to the staleness instrument. Each leaf is rewritten from what the source says now, not from the zh-CN repair: the two have different causes and their correct target text differs. `action.fields.body.helpText` adopts the rendering the sibling leaf `hook.fields.body.helpText` already carries for the same source sentence. Key sets are unchanged (773 leaves, identical across all four bundles). Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg Co-authored-by: Claude <noreply@anthropic.com>
…and the changeset `email_template.fields.variables.helpText` is a JSON shape example — language-neutral, so the faithful es-ES and ja-JP rendering is the source verbatim. That makes each leaf a byte copy of its source, which is exactly the condition under which `os i18n extract` records a source digest, so both tables gain one entry. Generator output, not a hand-edit: the entry records a TRUE fact (the leaf is a copy of the CURRENT source), which is the opposite of the false claim the file header warns against. Its effect is to move this one leaf OUT of LEGACY-TRUSTED and INTO the staleness instrument's coverage — if the JSON example is ever revised, this leaf now reports stale instead of silently serving the superseded revision. Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c17e6753bdf050d55d9ef605d8735b51c9e9c554 && git checkout c17e6753bdf050d55d9ef605d8735b51c9e9c554
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d4401f75bbe2e9e1b6f52f9bad83e377c5c33277 2af79e7a7b9bffa32330cb360d860d833d2416fb && git checkout -B drift-repro d4401f75bbe2e9e1b6f52f9bad83e377c5c33277 && git merge --no-ff 2af79e7a7b9bffa32330cb360d860d833d2416fb
node scripts/docs-audit/affected-docs.mjs --json d4401f75bbe2e9e1b6f52f9bad83e377c5c33277
|
Fixes #16227
Nine
metadataFormsleaves served a superseded English source revision in both es-ES and ja-JP. Each was a faithful translation of the sentence the source carried when it was extracted; the English moved afterwards, and bundle merge fills gaps only — a present-but-stale leaf is not a gap, so no amount of re-extraction corrects it.Clause-②: no
Values only: no exported symbol, no key added to a published payload, and the key set does not move. Measured, not assumed — all four bundles flatten to 773 leaves with identical key sets before and after this change.
Deliverable 1 — the nine-row table, re-measured
The card measured at
origin/main9f3989771; this branch bases on56ee7aef1d. The comparison was re-run rather than inherited, by the card's own method: the bundles underpackages/platform-objects/src/apps/translations/flattened to dotted leaf paths and each locale's value read against itsencounterpart.The re-measured table is identical to the card's at all nine leaves — same paths, same strings, both locales. The es-ES and ja-JP metadata-forms bundles moved exactly once between the two commits, at
dashboard.fields.gap.helpText(PR #16190), which is none of the nine.en)object.fields.fields.trackHistory.helpTextResume este campo en la cronología de actividad del registro/ 「レコードのアクティビティタイムラインでこのフィールドを要約する」object.fields.isSystem.helpTextdefaults sharing to public…; el uso compartido predeterminado es público)/ 「…。共有の既定は公開)」view.fields.filter.helpTextaction.fields.body.helpTextUna expresión L1 o un body JS L2 en sandbox/ 「L1 式または L2 サンドボックス JS body」action.sections.advanced.descriptionExposición a IA y forma de la solicitud API./ 「AI 公開と API リクエスト形状。」page.fields.type.helpTextreport.sections.basics.descriptionIdentidad y tipo de informe./ 「ID とレポートタイプ。」report.fields.columns.helpTextNombres de dimensión en horizontal (solo matrix)/ 「横方向のディメンション名(matrix のみ)」email_template.fields.variables.helpTextAn independent instrument agrees with the card
Rather than only re-reading the nine, every leaf's value history was reconstructed across all 38 commits that ever touched the three bundles, and each leaf asked: did
enmove after this locale's own value last changed? That yields 18 candidate paths mechanically, with no judgement involved. All nine of the card's leaves are in it, and so are both leaves the card names as borderline. The card's floor holds.The two borderline leaves — deliberately left alone
app.fields.defaultAgent.helpTextandemail_template.sections.identity.descriptionwere both re-read and both still show what the card describes. They are not touched here: the card names them without counting them, and this branch repairs the counted nine. Repairing them is a separate wording judgement.Written from the source, not copied from the zh-CN repair
Per the dispatch: 「zh-CN 那一半的成因是坏导入,这九个的成因是源漂移;两者的正确目标文本可能不同。」 Two places where the zh-CN target text was deliberately not copied:
object.fields.fields.trackHistory.helpText— the zh-CN repair reads 「…概述该字段的变更」, adding changes, which the source does not say. es-ES and ja-JP summarize the field.email_template.fields.variables.helpText— the zh-CN repair prefixes 「示例:」. The source is a bare JSON array, so both locales carry it verbatim.For
action.fields.body.helpTextthe rendering is not invented: the sibling leafhook.fields.body.helpTextalready translates the identical source sentence correctly in both locales, and this leaf now matches it.The digest entry this adds, and why it is the generator's and not a hand-edit
Carrying the JSON example verbatim makes those two leaves byte copies of their source, which is exactly the condition under which
os i18n extractrecords a source digest — soes-ES.source-hashes.generated.tsandja-JP.source-hashes.generated.tseach gain one entry, written bynode scripts/check-i18n-bundles.mjs --write.That file's header warns against editing it to silence a staleness report. This is the opposite: the entry records a true fact — the leaf is a copy of the current source — and its effect is to move this one leaf out of LEGACY-TRUSTED and into the instrument's coverage. If the JSON example is ever revised, the leaf now reports stale instead of silently serving the superseded revision.
Why no gate, and why these nine were invisible
Verified mechanically rather than assumed: all nine leaves carry zero entries in either locale's
*.source-hashes.generated.ts(which holds 190 of the 773metadataFormspaths). The table records a digest only while a leaf is still a byte copy of its source, and drops the entry as soon as a translator writes real text — so a hand-written translation is LEGACY-TRUSTED and never reported stale, by construction. That is the coverage hole, and it is documented insource-hash.ts's own header.No gate is proposed or extended here. #14931 already ruled that a second stale-detection instrument is debt rather than fix, and that whether a gate should exist is a maintainer call.
Changeset — route and level, on a measurement
.github/workflows/pr-automation.yml's WHICH LEVEL block gives two routes, chosen by what the PR actually releases.Route 2 (
skip-changeset) is rejected. Its stated scope is a diff that publishes nothing — ".github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like". Measured false here:@objectstack/platform-objectscarries noprivateflag, is version17.3.0, publishesfiles: ["dist"], and the repaired strings are present in the built output —npm pack --dry-runreports 69 files including sixmetadata-translationsentries, and the repaired text appears indist/metadata-translations/index.mjs,dist/index.jsanddist/plugin.*(asNombres de dimensi\xF3n en horizontal (solo matrix)— the bundler hex-escapes non-ASCII, which is why a raw grep for the accented form finds nothing).Route 1, level
patch. The block says a purely additive widening of a published surface takes at leastminor, and that "afix(that changes no public surface stayspatch". This adds no exported symbol and no accepted key or value; the key set is unchanged at 773.AGENTS.md's floor againstnoneis satisfied rather than exceeded.Verification
Dependency closure built before the first gate. Gate coverage reconciliation, verbatim:
That line accounts for coverage, not verdicts. The verdicts, stated separately: 50 of 50 exit 0. Two needed a prerequisite build first and were re-run —
check:i18nandcheck:dual-build-cjs-loadsboth returned exit 3 (PREREQUISITE NOT MET, which is NOT MEASURED, never a pass) against a partialdist, and both exit 0 once their named closure was built.check:i18n-coverageis run beyond the derived union on purpose: making a leaf a byte copy of its source is the extractor's definition of an untranslated fill, so the ratchet is the thing that could have moved. It reports none new.Every exit code was captured before any pipe, and each gate's own verdict line is quoted rather than a bare
$?.验收备注
Out-of-scope observations from the re-measurement, recorded here and not repaired in this PR:
object.fields.fields.reference.helpTextis stale in all three translated locales, and is not part of this card's frame. The census read it againsten=Target object nameand reasonably placed it below its threshold; on 2026-09-05 the source grew a normative sentence (atreefield'sreferenceis optional and must name the declaring object), and all three locales still say only "target object, for lookup/master_detail". Fresh drift on a leaf the census only ever judged against the shorter source, so it is a threshold-independent finding rather than a reversal of one.report.fields.blocks.helpText,permission.sections.tab_and_row_level_security.descriptionandagent.sections.capabilities.description. That threshold call is deliberate and reasoned, so it is reported rather than reversed. Worth a maintainer's eye that the last two now add a concept the source omits (custom context variables; tools) rather than merely paraphrasing, and thatagent.tools[]is a retired key.report.sections.joined_blocks.descriptionis in the card's 26-leaf frame and the card judged it faithful in both locales. On re-reading it drops only thedataset-boundqualifier; the card's judgement is upheld.object.sections.fields.descriptionrenderseach entryas "each row" in both locales. It is in neither the 26-leaf count nor the 18-leaf paraphrase list; the concept survives.Card numbers appearing above — #14931, #14587, #16165, #11671, #15979 and PRs #16226 / #16190 — are context only and are not addressed here.
Generated by Claude Code