Skip to content

fix(platform-objects): decide the dataset panel en-echoes per leaf, and derive the panel pin - #19460

Merged
huangyiirene merged 3 commits into
mainfrom
claude/issue-19403-round3-en-echoes
Sep 21, 2026
Merged

huangyiirene merged 3 commits into
mainfrom
claude/issue-19403-round3-en-echoes

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Part of #19403

Clause-②: no

What this is

Round 3 of this card. One panel family, decided leaf by leaf. The card's deliverable is a per-leaf judgement with its reason recorded, not 24 translations; every leaf below was decided on its own evidence, and the verdicts with their reasons live in a ledger the suite reads (dataset-panel-echo-decisions.test.ts), not only in this body.

The family: the whole dataset metadata form — the ADR-0021 analytics semantic-layer editor. Its type display pair, its four section headings, and both string leaves of its seven non-repeater fields. It is the largest family the round-2 table left (12 of the 50 keys), and round 2's stated rule was to take that one.

Not Fixes — this is one family of the card's population, and the card outlives this round. Six families remain (38 label keys), tabled at the foot of this body.

The reading, re-taken on this base — the numbers are NOT inherited

Census instrument: flatten every *.metadata-forms.generated.ts bundle and compare .label leaves against the en bundle. Population fixed here: the .label leaves of the metadata-form catalogs (the card's population, 538 leaves), plus the sibling helpText leaf of every key this round decides, plus the label + description pair of every section heading this family sits under, plus the type's own display pair — a panel whose field name is Chinese and whose tooltip is English is the same defect half fixed.

Measured on worktree base 2951c0f8f:

reading result
en string leaves in the metadata-form catalogs 893
of which .label leaves 538
POSITIVE CONTROL — labels genuinely translated 488 (zh-CN) · 472 (ja-JP) · 472 (es-ES)
labels echoing the en source, per locale 50 · 66 · 66
label keys echoing in ALL THREE locales 50 ⇒ 150 leaves

The control is what makes the echo count mean something: 488 translated against 50 echoing is a discrimination, not a parser that matches everything.

This is 50 / 150, the same reading round 2's LANDED comment recorded on the trunk and the same the round-3 claim re-took at 23:01Z — but that is a measurement here, not an inheritance: four commits landed on origin/main between my base and my first push (04d639c65, fc1a79067, be488cef3, 8e368dc3b) and git diff 2951c0f8f..origin/main -- packages/platform-objects/src/apps/translations/ is empty, so nothing moved this population under me. The card's own banner exists because a sibling PR once did exactly that; the way to know is to look.

After this change: 38 keys / 114 leaves echo in all three locales; control 500 · 484 · 484 — echoes down 12 and the control up 12, same population, same run.

The re-derived family table — the round-2 table is NOT stale

The claim said to treat PR #19428's seven-family breakdown as a starting hypothesis and re-derive it. Re-derived from the census output on base 2951c0f8f, grouping the 50 keys by metadata type: the two tables agree key for key, including its decision to collect the six bare type.label keys with no panel (seed · mapping · api · doc · book · capability) into one row. So the table is a live reading, not a stale one, and the largest family it leaves is this one.

One thing the re-derivation exposes that a .label census cannot: 12 label keys is 24 decidable leaves here, because every one of them has a sibling helpText or description that echoes with it and that no .label census counts. That ratio is what the remaining-count table at the foot is worth reading against.

The verdicts — 12 keys × 2 string leaves × 3 locales = 72 decisions

Every one is translate, and not one of them is "translate because it is English". The echo direction was tested per leaf by asking what this catalog's own authored corpus answers for that term.

  • The authored twin INSIDE the same key. dataset.fields.measures was already authored in all three locales (度量 / メジャー / Medidas, helpText included) while dataset.sections.measures — the heading directly over it — read English; dataset.fields.dimensions read English while its own five row properties were authored; dataset.fields.measures.filter was 筛选 / フィルター / Filtro while dataset.fields.filter echoed; dataset.fields.measures.name / .label were authored while dataset.fields.name / .label echoed. The identical string, on the same panel, rendered one way and echoed the other. An echo cannot be the deliberate rendering when the same word is rendered beside it.
  • The sibling editor. datasetForm's own docblock says it "Mirrors reportForm — the sibling analytics editor", and report is authored in all three locales for the same identity fields (snake_case unique identifier is an exact-string precedent, byte for byte), the same section shape, and the same semantic-layer vocabulary (semantic layer, measures/dimensions).
  • Decided AGAINST its precedents, per locale — dataset.sections.source.label. This catalog answers the bare word Source in three directions: hook.fields.body.source is 源码 / ソース / Código fuente (source CODE), sys_metadata.fields.source is 来源 / ソース / Origen (provenance), and page.fields.interfaceConfig.source is 数据来源 / ソース / Origen de datos (a data binding — decided one round earlier on this same card). This section holds the base object, the joins and the intrinsic scope, so it is the third sense and neither of the first two. zh-CN and es-ES depart from two of the three precedents; ja-JP keeps ソース, which all three precedents already use and which carries the data-source sense unambiguously in Japanese. Per locale, not per string.
  • The one leaf with NO precedent at all — dataset.fields.include.label. Nothing in either catalog answers the bare word Include, so it is decided from its own helpText and the decision is recorded as an expansion: 包含关联 / 含める関係 / Relaciones a incluir, using this catalog's own authored words for the concept (report.sections.joined_blocks.label is 关联对象 / 結合ブロック / Bloques unidos; field.fields.summaryOperations.helpText renders parent-child relationships as 父子关系 / 親子関係 / relaciones padre-hijo). A bare 包含 / 含める / Incluir answers "include what?" with nothing on a panel whose other labels all name their content.
  • Strict-enum values kept English, schema checked BEFORE translating — dataset.sections.measures.description names sum/avg/count/… and ratio/sum/difference/product. Those are AggregationFunction (packages/spec/src/data/query.zod.ts:159) and DerivedMeasureOp (packages/spec/src/ui/dataset.zod.ts:136), both z.enum inside a strictObject. Rendering them as words would tell an author in their own language to write a token the schema refuses — the inverse defect round 2 found on view.fields.userFilters.helpText and recorded as A metadata-form tooltip spells a strict enum's legal values in translated locales, so the value an author is shown is one the schema refuses #19430. Kept English on this catalog's own authored precedent for the shape: report.fields.type.helpText keeps tabular/summary/matrix/joined verbatim in all three locales.
  • Machine tokens and SQL keywords kept English inside translated prose — lookup / master_detail (FieldType enum values), relationship.field and its worked example account.region, the literal example value account, and the FROM / ON clauses the prose names. Precedents: object.fields.fields.reference.helpText keeps tree and lookup verbatim in all three locales, field.fields.multiple.helpText keeps select/lookup, and view.fields.appearance.helpText keeps allowedVisualizations.
  • A house style measured rather than assumed — of the 67 genuinely-translated section descriptions in this catalog, zh-CN drops the trailing full stop in 56 while ja-JP and es-ES keep it in all 67. The four new section descriptions follow that.
  • Panel-internal control for every prose leaf: each of the 24 carried an entry in this package's per-locale source-hashes.generated.ts provenance table in all three locales — exactly 24 rows under metadataForms.dataset per locale, no more and no fewer — while all 15 authored neighbours in the same dataset block (measures.label, measures.helpText and the thirteen repeater row properties) carried none in any of them. That is what an unauthored extractor fill looks like here.

Per-leaf reasons for all 24 leaves are in the ledger; this list is the summary, not the record.

The instrument, extended not reinvented

dataset-panel-echo-decisions.test.ts is the #19355 shape as rounds 1 and 2 left it: one row per leaf, verdict per locale, reason, and the en source pinned EQUAL to the live bundle so rewording the English reds the row instead of leaving a decision standing over text nobody judged. All three of round 2's deliberate properties are carried over:

  1. The echo-reason rule is a predicate with a dark control. Every verdict here is translate, so asserting the rule over the ledger alone evaluates nothing — the suite feeds it a synthetic echo row, asserts it is refused, then asserts it is accepted once a departure reason is added.
  2. The panel population is DERIVED from the en catalog, not hand-listed, so the pin cannot drift into a maintained list. It reaches all 39 string leaves of the dataset entry (the 24 decided plus the 15 already authored), and the dark control feeds it the en catalog and requires every leaf to come back flagged.
  3. A SECOND, independent witness: the provenance table. The ledger asserts that no decided leaf is still recorded as an extractor fill in any of the three source-hashes.generated.ts companions. It fires on a different trigger than the catalog assertion (a re-fill followed by an extract, rather than the re-fill itself) and it is per-locale, so it is not blind in the case check:i18n-stale-fill's cross-locale witness is blind. Its own dark control proves the lookup finds a key the table does hold, matched by pattern rather than pinned to a family, so a later round cannot red it by repairing one — and it additionally pins both spellings the composer builds, since this family is the first to need a type-level key (metadataForms.dataset.label) beside the fields/sections form.

The blind spot this family adds — a third shape, measured from both sides. Round 1 measured the record blind spot and round 2 the composite one. Here dataset is one of the types repeater-row-properties.test.ts already derives: its two enumerated repeaters yield 13 row properties (dimensions.* ×5, measures.* ×8), and every one of those 13 was already translated in all three locales. None of the 24 leaves decided here is inside that derivation, because the pin walks a repeater's CHILDREN and never the repeater itself — so dataset.fields.dimensions.label and .helpText sat directly above thirteen covered row properties and no pin saw them. The suite asserts both halves: what the existing pin covers (and that it was fine), and what it structurally cannot.

Regeneration — the silent-loss hazard on this path

Translated-locale values are hand-written; the provenance tables are not. Regenerated with pnpm i18n:extract (the command this package's extract config documents), never by hand, and no key was added or removed — so the merge=os-regen gap on *.metadata-forms.generated.ts (git check-attr merge reads unspecified on these paths) has nothing to lose even in principle.

Measured rather than asserted: the bundle diff is 24 insertions / 24 deletions in each of the three translated bundles — a pure value replacement — and the key lines of all four bundles are byte-identical lists before and after (1465 in each, same order, diff clean). The extract's own diff is 0 insertions / 24 deletions per provenance table: the 24 rows per locale that now record these leaves as authored (table sizes 455 → 431, 509 → 485, 518 → 494, exactly −24 each; 0 rows remain under metadataForms.dataset in any locale, while the tables still hold rows for other families). The en bundle and all four *.objects.generated.ts bundles are untouched.

pnpm check:i18n green is the proof that the hand-written values are byte-identical to what a fresh extract writes, across all nine bundle sets.

Reverse verification — the new pin can fail, in three independent places

Committed first, then ablated through scripts/ablation-replace.mjs, which makes the anchor count and the blob hash its own verdict and performs the mutation, the run and the restore inside one process — so no restore can be lost to a shell that exited between the two. No rebuild is needed for these: the ledger imports the bundles by relative path inside its own package, so the ablated files do not resolve through a package exports to dist/; this package's one vitest alias is for @objectstack/lint and touches none of it. (The ledger's one cross-package import, datasetForm from @objectstack/spec/ui, does resolve to spec/dist — it is not an ablation subject, and that dist was rebuilt on this base before any reading was taken.)

Ablation A — one decided leaf reverted to its English source in zh-CN (dataset.fields.include.label):

  • anchor label: "包含关联", x1 → x0 · replacement label: "Include", x0 → x1 · blob 6cadd2c0231a → a0f356e5270f
  • suite under the mutation: 2 failed | 17 passed — the enumerated verdict (zh-CN: every decided leaf matches its verdict) AND the derived panel pin (no leaf on this panel reads its en source unless the ledger decided it is an echo), independently
  • restored: blob back to 6cadd2c0231a == HEAD, git diff HEAD empty

Ablation B — one provenance row re-pointed at a decided leaf (a row's key swapped in zh-CN.source-hashes.generated.ts, so the table claims the leaf is still a byte copy of its source):

  • anchor x1 → x0 · replacement x0 → x1 · blob d279b182827b → bbe9b563ba54
  • suite under the mutation: 1 failed | 18 passed — and it is a different assertion than A's two (zh-CN: no decided leaf is still recorded as an extractor fill), so the second witness is not a restatement of the first
  • restored: blob back to d279b182827b == HEAD, git diff HEAD empty

Suite restored and re-run: 19 passed, working tree clean.

Verification

  • pnpm --filter @objectstack/platform-objects test — 46 files, 647 passed (628 before; +19 from the new ledger)
  • pnpm --filter @objectstack/platform-objects typecheck — exit 0. Its tsconfig.test.json program reaches the new file (--listFiles names it; 46 of 46 of this package's test files are in that program) and it contributes 0 errors — the only test-layer errors are the three pre-existing signatures test-typecheck-debt.json already records for src/feature-gate-guard.test.ts. The build config tsconfig.json excludes *.test.ts by design, so it names the new file 0 times; that is the shape, not a gap
  • pnpm check:i18n — green, 9 packages in sync. Its first run exited 3 (PREREQUISITE NOT MET), which is not a reading; cleared by building the closure the gate itself names, then re-run green
  • pnpm lint — repo-wide, eslint . --no-inline-config, exit 0, at edfef935d (the head of this branch; no commit has landed on it since that run)
  • Gate families derived with scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack and reconciled with --ran carrying every exit code, each captured BEFORE any pipe: 60 derived · 59 run green · 0 unrun · 1 NOT MEASURED — pnpm check:dual-build-cjs-loads exits 3 (PREREQUISITE NOT MET: it reads built output for every package in the repo and names twelve with no dist/). That prerequisite is a whole-repo build, which is CI's Build Core, and this diff adds no package and changes no export
  • The first two derivations printed STALE TREE (4 commits behind origin/main, 2 of the files the families are derived from changed across that range). Discharged by merging origin/main and re-deriving from the merged tree, not by waving it off; the re-derived list is identical and the merge brought no change to packages/platform-objects/src/apps/translations/ and no lockfile movement

Changeset, measured

@objectstack/platform-objects declares no private and ships files: ["dist", …], and src/metadata-translations/index.ts imports all three translated bundles. Probed on the built output: four new leaves are present in dist/metadata-translations/index.js, dist/index.js and dist/plugin.js — 数据集 as 数据集, 含める関係 as 含める関係, Capa sem\xE1ntica de anal\xEDtica, and the plain-ASCII Relaciones a incluir — with two pre-existing translated leaves as positive controls (度量, メジャー, 15 hits each) and two negative controls: an invented string (0) and the raw un-escaped spelling of the same CJK strings (0, including for the positive controls). ⇒ the diff publishes, skip-changeset would be wrong, and a patch changeset is included.

That probe is written to round 2's sharpened lesson and confirms both halves of it in one run: esbuild emits UPPER-CASE hex in \uXXXX, and \xNN for U+0080–U+00FF — Capa semántica de analítica is only findable as Capa sem\xE1ntica de anal\xEDtica. The raw negative control is in the probe so the next reader can see that a zero means "escaped", not "absent".

The six families that remain (38 label keys, all-three-locale echoes, this base)

family keys
object.fields.enable.* (+ validations) — the object capabilities panel 9
field.fields.* — the standalone field panel (several keys have a precedent from round 1) 9
action.fields.body.* + action.fields.ai 6
metadata-type display names with no panel: seed · mapping · api · doc · book · capability 6
hook.fields.retryPolicy.* + hook.fields.body.memoryMb + hook.fields.timeoutMs 5
report.fields.drilldown + report.fields.runtimeFilter + report.sections.dataset_binding 3

The dataset type is now absent from the echo census and from the metadataForms.dataset.* half of all three provenance tables — 0 rows remain there, where 24 per locale stood on this base.

⚠️ For whoever sizes the rest: the .label census under-counts the decidable population by roughly half, because each label key carries a sibling helpText or description that echoes with it. This family was 12 keys and 24 leaves; the largest row left (9 keys) is plausibly 18, and the six bare type.label keys carry a type.description each.

Acceptance notes

No class (a), (b) or (c) finding was produced by this family, and that is a reading rather than a silence — the one place this family could have carried the #19430 trap was checked directly. dataset.sections.measures.description names two strict enums' legal values in prose; both schemas were read before any word was translated (AggregationFunction, DerivedMeasureOp), and the tokens were kept English, so no author is told to write a value the schema refuses. dataset.fields.measures.helpText (authored before this round) already keeps count the same way. Nothing else on this panel names an enum's values in prose.

Noted, not filed:

  • The repeater blind spot, third shape. repeater-row-properties.test.ts filters on spec.type === 'repeater' and walks a repeater's declared CHILDREN. Round 1 measured the record blind spot and round 2 the composite one; here the repeater itself is covered — 13 row properties derived for dataset, all already translated — while the repeater's own label and helpText are structurally outside it, as are every section heading and the type display pair. Closed for THIS panel by the derived pin this PR adds; the shape remains for any other repeater parent. Successor: the remaining-family rounds, each of which will meet it.
  • report.sections.* descriptions. The population the card records as having no home. report.sections.dataset_binding.label IS counted by the .label census (it is in the remaining table's last row), but the description half of every report.sections.* pair is counted by no census and seen by no pin — the same asymmetry this round's 12-keys-to-24-leaves ratio measures generally. Successor: the report family row above, if the seat dispatches it.
  • field.fields.summaryOperations.object.label echoes Object while five authored precedents in this catalog answer 对象 / オブジェクト / Objeto. It was read while judging this family's own dataset.fields.object.label and deliberately NOT used as evidence for it; it belongs to the field family row above and is left there untouched. Successor: the field family round.
  • The eight bundle sets other than this one still do not opt into the provenance table (--source-hashes), so their generated leaves stay legacy-trusted and no per-leaf control like this round's exists for them. Documented in the extract config as deliberate; not this card's. 承接者:无.

Generated by Claude Code

The `dataset` metadata-form panel — the analytics semantic-layer editor —
shipped its English source in zh-CN, ja-JP and es-ES on 24 string leaves:
the type display pair, all four section headings, and both string leaves
of its seven non-repeater fields.

Each leaf decided on its own evidence against this catalog's authored
corpus, verdicts and reasons recorded in the ledger this commit's sibling
adds. The strict-enum tokens the measures section names (sum/avg/count,
ratio/sum/difference/product) and the relationship type tokens
(lookup / master_detail) stay English on an authored precedent.

Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE
Co-authored-by: Claude <noreply@anthropic.com>
…ived pin

One row per decided leaf with its per-locale verdict, the reason it was
reached and the `en` source pinned equal to the live bundle. Carries the
three deliberate properties the earlier rounds of #19403 established — the
echo-reason rule as a predicate with a dark control, a population derived
from the `en` catalog, and the provenance tables as a second independent
witness with its own pattern-matched dark control — and adds a third
blind-spot measurement: `repeater-row-properties.test.ts` covers this
panel's thirteen row properties and cannot see the repeaters' own leaves.

Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see

Coarse fallback — 3 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 50bae37f3df71e759f1730b906d5af35baa7b0f7 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 50bae37f3df71e759f1730b906d5af35baa7b0f7

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 20, 2026
@huangyiirene
huangyiirene marked this pull request as ready for review September 21, 2026 00:11
@huangyiirene
huangyiirene added this pull request to the merge queue Sep 21, 2026
Merged via the queue into main with commit 72eeabd Sep 21, 2026
36 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-19403-round3-en-echoes branch September 21, 2026 00:40
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

Development

Successfully merging this pull request may close these issues.

2 participants