Skip to content

fix(platform-objects): decide the page Interface panel en-echoes per leaf, and derive the panel pin - #19428

Merged
huangyiirene merged 2 commits into
mainfrom
claude/issue-19403-round2-en-echoes
Sep 20, 2026
Merged

huangyiirene merged 2 commits into
mainfrom
claude/issue-19403-round2-en-echoes

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Part of #19403

Clause-②: no

What this is

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

The family: the page Interface panel — page.fields['interfaceConfig*'] plus the page.sections.interface heading it sits under. This is where a list page is authored: which object it reads, which columns it shows, how end users filter it, what the toolbar offers. It is the largest family the round-1 table left (15 of the 65 keys).

Not Fixes — this is one family of the card's population, and the card outlives this round. Seven families remain (50 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 description leaf of the section — a panel whose field name is Chinese and whose tooltip is English is the same defect half fixed.

Measured on worktree base 576d5df66:

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

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

This is 65 / 195, the same reading round 1's LANDED comment recorded on the trunk — this round inherited no drift, and that is itself a measurement rather than an assumption: four commits landed on origin/main between my base and this push (50bfe137c, 7ffddfa49, f9e16d856, 488f4f546) and git diff 576d5df66..origin/main -- packages/platform-objects/src/apps/translations/ is empty. The card's own banner exists because a sibling PR moved this population under a previous round; it did not happen here, and the way to know is to look.

After this change: 50 keys / 150 leaves echo in all three locales; control 488 · 472 · 472 — echoes down 15 and the control up 15, same population, same run.

The verdicts — 15 keys × 2 string leaves × 3 locales = 90 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. interfaceConfig inlines the view's own surface onto the page — the form's own helpText says "The page IS the view" — and view.fields.<same key> is authored in all three locales for most of the family: columns (列 / 列 / Columnas), sort (排序 / 並び替え / Orden), appearance (外观 / 外観 / Apariencia), userFilters (用户筛选器 / ユーザーフィルター / Filtros de usuario), userActions (用户操作 / ユーザーアクション / Acciones de usuario), addRecord (添加记录 / レコードを追加 / Agregar registro), showRecordCount (显示记录数 / レコード数を表示 / Mostrar recuento de registros). A translator who judged the English right here would have had to judge it wrong one panel away.
  • Decided AGAINST both of its same-string precedents — interfaceConfig.source.label. This catalog answers the bare word Source twice, in opposite directions: hook.fields.body.source is 源码 / ソース / Código fuente (source CODE) and sys_metadata.fields.source is 来源 / ソース / Origen (provenance). This leaf is neither — its own helpText says "Object this page reads from". So zh-CN and es-ES depart from both (数据来源 / Origen de datos) while ja-JP keeps ソース, the word both precedents already use and the one that carries the data-source sense unambiguously in Japanese. Per locale, not per string.
  • Decided AGAINST a same-string precedent, the other way — interfaceConfig.userFilters.helpText. view.fields.userFilters.helpText translates its dropdown / tabs / toggle (下拉 / 标签页 / 开关), but those are the stored element VALUES. The None / Tabs / Dropdown here are the visible option labels of the filter-mode widget, which objectui renders and this catalog holds no key for — translating them would assert a rendering this package cannot deliver. Kept English, the way this same panel's authored page.fields.type.helpText keeps the quoted option labels "List / Interface". (The precedent it departs from turns out to be a defect in its own right — see Acceptance notes.)
  • Machine tokens and product terms kept English inside translated prose — the five literal key names in interfaceConfig.helpText (source, columns, filterBy, appearance.allowedVisualizations, userActions), on the authored precedent of view.fields.appearance.helpText, which keeps allowedVisualizations verbatim in all three locales; the renderer tokens Grid / Kanban / Calendar / …, on this same panel's authored page.fields.type.helpText, which keeps grid / kanban / calendar / …; and Interface and Airtable, on page.fields.type.helpText and view.sections.end_user_controls.description respectively.
  • A distinction the source draws, preserved — interfaceConfig.levels.helpText says "tree-like sources". object.fields.fields.reference.helpText is authored on both sides of that line in all three locales: it keeps the machine token (tree 字段 / tree フィールド / un campo tree) and translates the shape in the same sentence (树 / ツリー / árbol). This one is the shape, so it is translated.
  • Panel-internal control for the prose leaves: each carried an entry in this package's per-locale source-hashes.generated.ts provenance table in all three locales, while all 43 authored neighbours on the same page panel carried none in any of them — which is what an unauthored extractor fill looks like here.

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

The instrument, extended not reinvented

page-interface-panel-echo-decisions.test.ts is the #19355 shape as round 1 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. Both of round 1's deliberate additions 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.

Three things this panel let the instrument gain that the earlier rounds could not:

  • An authored control INSIDE the family. interfaceConfig.sort is type: 'repeater', so its two row properties (interfaceConfig.sort.field / .order) are the one slice of this panel that repeater-row-properties.test.ts already derives — and they are the only two leaves under interfaceConfig that were already translated (字段 / 排序方向, フィールド / 並び方向, Campo / Dirección). Everything the existing pin could see was fine; everything it could not see was an echo. That is the card's 「0 of the keys sit inside it」 measured on this panel, with the covered slice as its own control, and the suite asserts both halves.
  • A SECOND, independent witness: the provenance table. The ledger now also 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. It carries its own dark control: the lookup is proved to find a key the table does hold, matched by pattern rather than pinned to a family, so a later round of this card cannot red it by fixing one.
  • The population covers the section heading too. page.sections.interface is a .label + .description pair that no .label-only census counts in full; the derived walk takes both.

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 30 insertions / 30 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). The extract's own diff is 0 insertions / 30 deletions per provenance table: the 30 rows per locale that now record these leaves as authored. 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 (anchor must hit; on-disk counts and blob hashes are the tool's own verdict). No rebuild is needed for these: the ledger imports the bundles by relative path inside its own package, so nothing here resolves through a package exports to dist/ — this package's one vitest alias is for @objectstack/lint and touches none of it.

Ablation A — one decided leaf reverted to its English source in zh-CN:

  • anchor label: "数据来源", x1 → x0 · replacement label: "Source", x2 → x3 · blob 71cc7c4c47c0 → 8a8147b989ed
  • suite under the mutation: 2 failed | 16 passed — the enumerated verdict (zh-CN page.fields.interfaceConfig.source.label reads its en source again) AND the derived panel pin, independently
  • restored: blob back to 71cc7c4c47c0 == HEAD, git diff HEAD empty

Ablation B — one provenance row re-added for a decided leaf (a row 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 4746249617fb → 5a5f08eea895
  • suite under the mutation: 1 failed | 17 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 4746249617fb == HEAD, git diff HEAD empty

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

Verification

  • pnpm --filter @objectstack/platform-objects test — 45 files, 628 passed (610 before; +18 from the new ledger)
  • pnpm --filter @objectstack/platform-objects typecheck — green; its tsconfig.test.json program reaches the new file (--listFiles names it) and it contributes no errors
  • 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 9adb9f774 (the head of this branch; no commit has landed since that run)
  • Gate families derived with scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack and reconciled with --ran carrying every exit code: 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). That prerequisite is a whole-repo build, which is CI's Build Core, and this diff adds no package and changes no export.

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: three new leaves (Origen de datos, and 数据来源 / 階層レベル in their esbuild-escaped form) are present in dist/metadata-translations/index.js, dist/index.js and dist/plugin.js, with three pre-existing translated leaves as positive controls and two negative controls (an invented string, and the raw un-escaped CJK). ⇒ the diff publishes, skip-changeset would be wrong, and a patch changeset is included.

One line for whoever probes this dist next, refining the lesson round 1 recorded. Round 1 wrote that "tsup emits non-ASCII as \uXXXX escapes", which is right but not enough to write a working probe: esbuild emits upper-case hex, and uses \xNN for U+0080–U+00FF. My first probe spelled the escape in lower case and came back 0 hits including its own positive control — the same failure round 1 warned about, caught the same way. The raw-CJK negative control above is in the probe precisely so the next reader can see that the zero means "escaped", not "absent".

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

family keys
dataset.fields.* + dataset.sections.* + dataset.label 12
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 page type is now absent from the echo census and from the metadataForms.page.* half of all three provenance tables — 0 rows remain there, where 30 per locale stood on this base.

Acceptance notes

To file (class (c), a metadata-authoring trap — dedupe words: helpText translates enum values · userFilters element dropdown tabs toggle · translated locale names illegal value · metadata form tooltip enum spelling · strict enum rejects translated token):

  • view.fields.userFilters.helpText names the legal values of UserFiltersSchema.element — a z.enum(['dropdown', 'tabs', 'toggle']) inside a strictObject — and all three translated locales render those three tokens as words: 控件样式(下拉 / 标签页 / 开关), 要素スタイル(ドロップダウン / タブ / トグル), estilo de elemento (desplegable / pestañas / interruptor). An author working in one of those locales is told the element styles are 下拉 / ドロップダウン / desplegable; writing that is refused by the enum, and the tooltip is the only place the panel names them. This is the inverse of this card's defect and it is what made this round keep None / Tabs / Dropdown English. Seam: spec:UserFiltersSchema.element → renderer: objectui plugin-list/src/UserFilters.tsx. Not fixed here: the bounded-fix exemption needs the finding to be the same defect class as the card, and this is the opposite one.

Noted, not filed:

  • repeater-row-properties.test.ts filters on spec.type === 'repeater'. Round 1 measured the record blind spot; this round measures the composite one: page.fields.interfaceConfig is declared { field: 'interfaceConfig', type: 'composite' } with 15 children, and that derivation therefore yielded exactly the two interfaceConfig.sort.* row properties and none of the 30 leaves decided here. Closed for THIS panel by the derived pin this PR adds; the same blind spot remains for any other composite or record child. Successor: the remaining-family rounds, each of which will meet it.
  • report.sections.* (label and description) — the population the card records as having no home. Still uncounted by any .label census and unseen by any pin. Successor: the report family row above, if the seat dispatches it. Note the same shape now has one fewer instance: page.sections.interface was exactly such a pair and is decided here, so the class is reachable, not structural.
  • page.fields.interfaceConfig.appearance.helpText writes the renderer tokens capitalised (Grid / Kanban / Calendar) while page.fields.type.helpText on the same panel writes them lower-case (grid / kanban / calendar) and VisualizationTypeSchema's values are lower-case. Checked because a capitalisation split looked like the same trap as the finding above: it is softer — the sentence reads as display names of the renderers rather than as the literal whitelist entries, and the neighbouring helpText spells the values correctly one row up. Recorded as a boundary of that class, not a second instance. Successor: none.
  • 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

@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 488f4f546556527d03e9afe92b7ffe466f342a2a → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 488f4f546556527d03e9afe92b7ffe466f342a2a

⚠️ 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 20, 2026 20:26
@huangyiirene
huangyiirene added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 9529989 Sep 20, 2026
36 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-19403-round2-en-echoes branch September 20, 2026 20:54
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…nd derive the panel pin (objectstack-ai#19460)

Part of objectstack-ai#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
2951c0f..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 objectstack-ai#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
objectstack-ai#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 objectstack-ai#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 objectstack-ai#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](https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… translated tooltips (objectstack-ai#19520)

Fixes objectstack-ai#19430

Clause-②: no

The accept set does not move: no key joins a published payload, no
member joins an enum, no symbol is exported, nothing is registered. The
diff is three translated string leaves, one unit test file that exports
nothing, and a changeset. `packages/spec` is untouched — the schema is
not changing in this round.

## What was wrong

`metadataForms.view.fields.userFilters.helpText` names the legal values
of a **strict** enum, and it is the only place the `view` panel names
them at all. All three translated catalogs rendered those tokens as
ordinary words, so an author — or an AI — working in a translated locale
was shown a value the schema refuses.

This is the inverse of objectstack-ai#19403's defect: there English leaked into
translated UI; here translation leaked into a machine token that has to
stay English to be writable. The prose around the values is translated
correctly and stays translated.

## Per-locale before/after, beside the enum it must agree with

The enum, unchanged by this PR, read on this branch at
`packages/spec/src/ui/view.zod.ts:1506` (inside `UserFiltersSchema`,
declared at `:1497`):

```ts
element: z.enum(['dropdown', 'tabs', 'toggle']).default('dropdown')
```

| locale | before | after |
|:--|:--|:--|
| `en` (control, ⛔ not edited) | `Quick-filter bar: element style
(dropdown / tabs / toggle) + exposed fields or tab presets` | unchanged
|
| `zh-CN` | `快速筛选栏:控件样式(下拉 / 标签页 / 开关)+ 暴露的字段或标签页预设` |
`快速筛选栏:控件样式(dropdown / tabs / toggle)+ 暴露的字段或标签页预设` |
| `ja-JP` | `クイックフィルターバー:要素スタイル(ドロップダウン / タブ / トグル)+ 公開フィールドまたはタブプリセット`
| `クイックフィルターバー:要素スタイル(dropdown / tabs / toggle)+ 公開フィールドまたはタブプリセット` |
| `es-ES` | `Barra de filtros rápidos: estilo de elemento (desplegable /
pestañas / interruptor) + campos expuestos o preajustes de pestañas` |
`Barra de filtros rápidos: estilo de elemento (dropdown / tabs / toggle)
+ campos expuestos o preajustes de pestañas` |

(The table renders the CJK brackets as ASCII for legibility; the files
keep each locale's own full-width punctuation, unchanged by this PR —
read the diff for the bytes.)

⇒ after: every token the tooltip names is a value `UserFiltersSchema`
accepts, in all four locales. The prose keeps its own language: 快速筛选栏 ·
クイックフィルターバー · Barra de filtros rápidos, and the trailing "tab presets"
prose (标签页预设 · タブプリセット · preajustes de pestañas) stays translated — it
is prose, not a token.

Only leaf string values changed. No key was added or dropped, so the
bundle structure is untouched and no `*.source-hashes*.ts` file moves:
`metadataForms.view.fields.userFilters.helpText` carries no provenance
entry in any of the three locales (it is LEGACY-TRUSTED — already
translator-written before this round), and `pnpm check:i18n` reports all
9 packages in sync.

## The two `element` enums — which tooltip describes which, and the
verdict on the second

Both readings taken on this branch at `1b2abe57`, where
`packages/spec/src/ui/view.zod.ts` is byte-identical to `origin/main` at
`48c39e00`:

| line | enum | owning schema |
|:--|:--|:--|
| `view.zod.ts:1506` | `z.enum(['dropdown', 'tabs', 'toggle'])` |
`UserFiltersSchema` (`:1497`) |
| `view.zod.ts:4205` | `z.enum(['dropdown', 'toggle'])` — ⛔ no `tabs` |
`ObjectUserFiltersSchema` (`:4195`) |

The card measured the second one at `:4193`; on this tree it is `:4205`.
Same declaration, re-taken here.

**The repaired tooltip describes the FIRST enum.** The chain, each link
read on this tree:

1. The tooltip's source is `packages/spec/src/ui/view.form.ts:119`,
inside `defineForm({ schemaId: 'view' })` (`view.form.ts:18-19`).
2. `schemaId: 'view'` resolves through `METADATA_FORM_REGISTRY` to
`ViewMetadataSchema` (`view.zod.ts:5586`), a union over four wire
members (`VIEW_METADATA_MEMBERS`, `:5181`).
3. The form's `userFilters` is a **top-level** field. The member that
carries a top-level `userFilters` is `ListViewOverlayWireSchema` —
`ListViewShapeSchema.extend(...)` — and
`ListViewShapeSchema.userFilters` is `UserFiltersSchema.optional()` at
`view.zod.ts:2410`. The `viewItem` member carries the same schema one
level down, under `config`.
4. So the three values the tooltip names are exactly the three
`UserFiltersSchema.element` accepts. The `en` copy was already correct;
that is why it is the control.

**Verdict on the second surface (`:4205`): no defect, because no tooltip
reaches it.** `ObjectUserFiltersSchema` is referenced from exactly one
place — `ObjectListViewSchema` (`view.zod.ts:4220-4222`) — which is what
`ViewSchema`'s container keys `views.list` and `views.listViews` use
(`:4306`, `:4308`). No metadata form declares a field for them:

- `grep -n "field: '"` over `packages/spec/src/data/object.form.ts`
returns no `views`, `listViews` or `userFilters` entry;
- `grep -n "listViews" en.metadata-forms.generated.ts` returns nothing;
- `grep -n "dropdown"` over the whole `en` catalog returns exactly
**one** line — `view.fields.userFilters.helpText`, the leaf repaired
here.

⇒ the second enum has **no** metadata-form tooltip, so it carries
neither this defect nor the worse one (a tooltip naming a value its own
enum refuses). Its own English `.describe()` at `view.zod.ts:4206` is
correct and says so explicitly: `"tabs" is page-only — use listViews for
named presets`.

One neighbour worth stating because it looks adjacent and is not:
`page.fields['interfaceConfig.userFilters']` describes the SAME
`UserFiltersSchema` (`page.zod.ts:591`), and its tooltip already keeps
`None / Tabs / Dropdown` verbatim English in all three locales — the
shape PR objectstack-ai#19428 chose. ⛔ It is **not** the shape this PR applies, and
the corrected changeset now says so: those three are the `filter-mode`
widget's UI mode names, capitalised, and `z.enum` is case-sensitive, so
the enum refuses all three; `None` stands for the absence of the config
rather than a value; `toggle` is deliberately not offered there. What
this PR keeps verbatim is the enum's own tokens, which is the stricter
requirement, because they are values an author types. <sub>— clause
corrected in place by the `domain:spec` seat 3 at 2026-09-21T06:20Z,
after the at-tier review found it false; the original read 「and the
shape this PR applies to the `view` panel」. Disclosed here and in the
review record rather than silently rewritten.</sub>

## The pin, and the four controls that keep it honest


`packages/platform-objects/src/apps/translations/user-filters-element-tokens.test.ts`
— 11 cases, one leaf, three locales. Not a census: it reads one key.

1. **The set is derived, not listed.** `SCHEMA_ELEMENT_VALUES` is read
off `UserFiltersSchema.shape.element` (unwrapping `.default()`) and
asserted SET-EQUAL to the pinned literal; every tooltip assertion then
runs over the derived set. So enum **growth** reddens, not only
shrinkage — a hand-listed set catches a dropped member and lets a newly
legal value go unnamed in all four catalogs with the pin green, which is
this card's own defect class reintroduced.
2. **Schema leg.** Every derived token is `safeParse`d through
`UserFiltersSchema` and must be ACCEPTED; the words the translations
used to carry (下拉/开关 · ドロップダウン/トグル · desplegable/interruptor) and the
neighbouring panel's capitalised `None / Tabs / Dropdown` are
`safeParse`d and must be REFUSED.
3. **Exact-set leg.** The tooltip's parenthetical must name the derived
values and nothing else, in order. Checking only "no fewer" lets
`(dropdown / tabs / toggle / chips)` through.
4. **Still-translated leg.** Each translated helpText must differ from
the `en` source and must still carry its own prose at BOTH ends of the
sentence. Anchoring the leading fragment alone lets the trailing half
revert to English — objectstack-ai#19403's defect surviving on the larger half of the
sentence.

`tabs` is deliberately absent from the "former words" row of every
locale: its former word is also the locale's ordinary word for the
tab-presets **prose** at the end of the same sentence, which is
translated on purpose and must stay. Asserting its absence would demand
the prose be de-translated; it is covered by controls 1, 3 and the
presence leg instead. The reason is written in the test.

**Reverse verification** — five legs, each driven from the committed
state with `scripts/ablation-replace.mjs` (anchor hit asserted, blob
hash before/after recorded, restore proved by `blob == HEAD` and an
empty `git diff HEAD`):

| mutation | expected | observed |
|:--|:--|:--|
| `zh-CN` helpText reverted to the pre-fix translated words | red | **1
failed / 10 passed** — `zh-CN helpText does not name 'dropdown'` |
| `zh-CN` helpText replaced by the `en` source sentence | red, on the
still-translated leg | **1 failed / 10 passed** — `zh-CN helpText is a
byte copy of the en source` |
| `zh-CN` trailing clause replaced by its English source | red — the N1
escape, now closed | **1 failed / 10 passed** — `zh-CN helpText lost its
trailing prose` |
| `zh-CN` parenthetical grown to `(dropdown / tabs / toggle / chips)` |
red — the N2 escape, now closed | **1 failed / 10 passed** — `names a
different set from the enum` |
| `UserFiltersSchema.element` grown by a fourth member in
`packages/spec/src/ui/view.zod.ts`, spec rebuilt | red — the B2 class |
**5 failed / 6 passed** — the derived-set control plus all four
catalogs' presence legs |

Every leg restored: `blob == HEAD`, `git diff HEAD` empty. The
fourth-member leg additionally left a mutated `packages/spec/dist`, so
spec was rebuilt and the restore proved by grep with a lit control: the
mutated spelling is absent from `packages/spec/dist` (grep exit 1, 0
hits) while the canonical one is present in 22 files (grep exit 0). ⛔
`view.zod.ts` is untouched by the delivered diff — that mutation was an
ablation, restored, never committed.

<sub>⚠️ This section was rewritten in place by the `domain:spec` seat 3
at 2026-09-21T06:40Z. It previously described the file at the earlier
head `1b2abe57` — 「9 cases」, 「two controls」, and a two-row ablation
table — which the at-tier review found stale for this head. An interim
seat note at 06:36Z said only that the table had not been re-taken here;
the round has since re-taken it, so the measured five-leg table above
replaces that note. ⛔ The numbers above are the round's measurements,
not the seat's; the seat independently re-took only the case count (11)
and the CI roster.</sub>
## Verification

All readings taken in `/home/user/objectstack-issue-19430` at
`805493a4`, merge base `48c39e00`, every exit code captured **before**
any pipe.

- `node scripts/pm/dispatch-gates.mjs --changed --repo
objectstack-ai/objectstack --commands` → **60** families, byte-identical
to the derivation on the previous head. All 60 run, **all 60 at exit 0
on the first run** here (the three that needed a build on the previous
head had their prerequisites already cleared). `--ran` reconciliation:
**exit 0 — 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN**.
- `pnpm --filter @objectstack/platform-objects test` → **48 files / 679
tests passed**, including this pin's 11 cases.
- `pnpm --filter @objectstack/platform-objects typecheck` → exit 0 (`tsc
--noEmit`, `tsconfig.scripts.json`, and `check:test-typecheck`, which
compiles the new test file; the debt ledger did not grow).
- `pnpm build` → 73/73 tasks; the `check:i18n` build closure → 57/57.
- `pnpm lint` (`eslint . --no-inline-config`, the repo-wide scan, not a
narrowing) → exit 0.
- `pnpm check:i18n` → all 9 packages in sync, 11 bundles for
`platform-objects`. `pnpm check:nul-bytes` → 0, plus a direct
control-character scan over all five changed files: each `grep -naP`
exits 1 (clean).
- **CI on this head**, read at 2026-09-21T06:33Z from `GET
/repos/objectstack-ai/objectstack/commits/805493a4.../check-runs`: 41
check runs — **36 success, 5 skipped, 0 failure, 0 in progress, 0
queued**. All six `Test Core` shards are `success`, so CI has now
confirmed `user-filters-element-tokens.test.ts`; `Lint & Repo Gates`,
`TypeScript Type Check`, `Build Core`, `Dogfood Regression Gate`,
`Temporal Conformance (live PG + MySQL)` and `Governed Surface Queue
Guard` are all `success` — the full required set.
## Acceptance notes

- **Fenced and left alone, as the card records it.**
`page.fields['interfaceConfig.appearance'].helpText` writes its renderer
tokens capitalised (`Grid / Kanban / Calendar / …`) while
`page.fields.type.helpText` on the same panel writes them lower-case
(`grid / kanban / calendar / …`, matching `VisualizationTypeSchema`).
Both already keep the tokens English in all three locales, so this is a
capitalisation question, not a writability one. ⛔ Not swept in, ⛔ not
filed — it is this class's recorded edge.
- **The instrument the card proposes was reported, not built.** A
corpus-wide census over "every schema key whose tooltip names enum
values" is a new gate/row, and ruling batch objectstack-ai#208 R6 requires the
maintainer's sentence on its own card for one. None exists. The test
added here is an ordinary unit pin over the single repaired leaf.
- I wrote no label myself: the dispatch named none, and the PR carries a
changeset so `skip-changeset` does not apply. The labels now on the PR —
`documentation`, `size/m`, `tests`, `tooling` — were applied by the Auto
Label check-run, not by me.


---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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