Skip to content

fix(platform-objects): decide the standalone field panel en-echoes per leaf, and close the composite blind spot - #19485

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

huangyiirene merged 5 commits into
mainfrom
claude/issue-19403-round4-en-echoes

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Part of #19403

Clause-②: no

What this is

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

The family: the nine echoing keys of the standalone field metadata form — the field editor reached on its own, as opposed to the one embedded in the object panel. Both string leaves of each are decided.

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

The headline number measures half the work — so this round sized itself from LEAVES

The card records "38 keys remaining". Every count it has carried counts .label leaves, while nearly every label key carries a sibling helpText that echoes with it and that no .label census sees. Derived per family on this base, the two 9-key rows the round-3 table left are not the same size:

row label keys echoing string leaves
field.fields.* — the standalone field panel 9 18
object.fields.enable.* (+ validations) 9 11

object's nine keys are mostly bare capability checkboxes with no helpText (seven of the nine carry only a label). ⇒ the largest family the round-3 table leaves is this one, by leaves rather than by keys, and that is the rule the card means now that the headline has been corrected. The round-3 table is otherwise not stale: re-derived from the census on base fbc12be31, the six rows agree key for key.

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

Census instrument: flatten every *.metadata-forms.generated.ts bundle (nested object literals with bare identifier keys — a regex requiring quoted keys returns 0, so this parses with the TypeScript AST) and compare .label leaves against en.

Measured on worktree base fbc12be31:

reading result
en string leaves in the metadata-form catalogs 893
of which .label leaves 538
POSITIVE CONTROL — labels genuinely translated 500 (zh-CN) · 484 (ja-JP) · 484 (es-ES)
label keys echoing in ALL THREE locales 38 ⇒ 114 leaves

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

After this change: 29 keys / 87 leaves echo in all three locales; control 509 · 493 · 493 — echoes down 9 and the control up 9, same population, same run.

git diff fbc12be31..origin/main -- packages/platform-objects/src/apps/translations/ is empty, so nothing moved this population under me while the round ran.

The control this panel supplies that no earlier round could: the authored twin at the SAME KEY PATH

object.fields.fields.* is the same field editor embedded in the object panel, and it was decided in round 1 of this card. Fourteen of the eighteen leaves here have a twin there, authored in all three locales — and seven of those twins are byte-identical in en:

placeholder.label · valueDomain.label · rows.label · summaryOperations.object.label · summaryOperations.function.label · summaryOperations.function.helpText · summaryOperations.field.label

⇒ the identical key, the identical English, one authored and one echoing, in one bundle. Earlier rounds argued from a same-string precedent elsewhere in the catalog; here the precedent is the same spec key rendered one panel over. An echo cannot be the deliberate rendering when the same key is rendered otherwise beside it.

The four keys with no twin at allrelatedListFilter (#19085 added it to field.form.ts alone) and three of the five summaryOperations row properties — are decided from their own helpText and their schema, and say so in the ledger.

The #19430 trap was met, and checked AT THE SCHEMA before a word was rendered

field.fields.valueDomain.helpText names iana_time_zone, iso_4217_currency and iso_3166_alpha2 in prose. Those are the three members of ValueDomainSchema (packages/spec/src/shared/value-domain.zod.ts), a z.enum. Rendering them as words would tell an author in their own language to write a token the schema refuses — the defect round 2 filed as #19430. Kept English.

⚠️ The authored twin could not have answered this one. object.fields.fields.valueDomain.helpText reads "Standard the written value must belong to; a write carrying a non-member is refused" and names no token at all. The twin settled the prose; the schema settled the tokens. Same key, two sources of evidence, and taking the twin alone would have produced the defect.

The other leaf that could have carried it does not: summaryOperations.function.helpText documents a z.enum(['count','sum','min','max','avg']) but its text is the bare words "Aggregation function" and names no member. summaryOperations.field.helpText names count, one member of that same enum, and keeps it English — as the authored twin already did.

The tokens this round decided to KEEP are now asserted, not merely described: a later reword that renders iana_time_zone, inlineHelpText, count, AND or status == received reds the suite, and the assertion carries a dark control so a green cannot come from an includes that matches anything.

Per-leaf judgement — not a family translated wholesale

Every verdict is translate, and not one is "translate because it is English". Two worth reading:

  • field.fields.rows.label — decided BETWEEN two authored precedents by SENSE. This catalog answers the bare word Rows twice: object.fields.fields.rows.label is 行数 / 行数 / Filas (an editor height, counted in text rows) and report.fields.rows.label is 行 / 行 / Filas (a report axis). This key is the first sense — its own helpText says "Inline editor height in text rows" — so it takes 行数 and departs from the report reading in zh-CN and ja-JP. es-ES renders both senses Filas, so no choice arises there. Per locale, not per string.
  • field.fields.relatedListFilter.label — the one label with NO same-string precedent. Related List returns zero hits across both catalogs. Decided by composing this catalog's own authored renderings of its two parts (Related is 关联 / 関連 / relacionad-, from sys_email.fields.related_object.label; Filter is 筛选 / フィルター / Filtro, four precedents). ⚠️ Recorded as a LITERAL rendering, not an expansion — unlike round 3's include, the English compound already names its content, so nothing had to be added. The honest difference from the round-3 shape is stated rather than smoothed.

Also measured rather than assumed: AND stays English on an authored precedent (dataset.fields.filter.helpText is 以 AND 方式并入每个查询 / すべてのクエリに AND で結合されます / combinado con AND en cada consulta), while FK is rendered 外键 / 外部キー / Clave foránea (four authored sys_oauth_* precedents) — an abbreviation of an English term is rendered, an operator or enum member an author must type is not. And the helpText house style was measured on its own population: of 276 helpText leaves only 12 end in sentence punctuation in en, and the translated ones follow (zh-CN 241/252 without a full stop), which is a different answer from round 3's section-description finding.

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

The instrument, extended not reinvented

All four properties carried:

  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. It reaches all 85 string leaves of the field entry (the 18 decided plus 67 already-authored neighbours), and its own dark control feeds it the en catalog and requires every leaf to come back flagged.
  3. A SECOND, independent witness: the provenance table. 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 is per-locale. Its own dark control is matched by pattern rather than pinned to a family, so a later round cannot red it by repairing one.
  4. The en source is pinned EQUAL to the live bundle, so rewording the English reds the row instead of leaving a decision standing over text nobody judged.

Panel-internal control: all 18 leaves carried a provenance entry in all three locales — exactly 18 rows under metadataForms.field per locale, no more and no fewer — while the other 67 string leaves of the same field entry carried none in any of them. That is what an unauthored extractor fill looks like here.

The blind spot, FOURTH shape — and this one is a coverage CONTROL that passes over the gap

Round 1 measured the record blind spot, round 2 composite, round 3 the repeater whose own label sits one level above what the pin walks. This panel adds a fourth, measured from both sides:

fieldForm declares two fields that enumerate children. options is a type: 'repeater' with six children — repeater-row-properties.test.ts derives every one of them, and all six were already translated. summaryOperations declares five children in exactly the same shape but is a type: 'composite', so the pin's spec.type !== 'repeater' filter skips it — while the extractor emits a catalog key for each, because it walks a form field's declared fields whatever the declared type is. All five of those children echoed in all three locales, and they are five of the nine keys this round decides.

The sharp half: that pin's own vacuity control asserts carrying.has('field') === true, and it passes — on options, the parent that was never broken. A per-type coverage control cannot see a per-field gap. The pin is green over exactly the keys that were broken, one enumerated parent to the side of what it walks rather than one level up. 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

*.metadata-forms.generated.ts is generator-owned but git check-attr merge reads unspecified, so it is not routed to merge=os-regen: two branches each ADDING keys merge exit 0 with one side silently lost.

Regenerated with pnpm i18n:extract, never by hand, and no key was added or removed, so that gap has nothing to lose even in principle. Measured rather than asserted:

  • the bundle diff is 18 insertions / 18 deletions in each of the three translated bundles — a pure value replacement;
  • the full flattened key sets are identical on all four bundles: en / zh-CN / ja-JP / es-ES all 893 keys, 0 added, 0 removed, base vs head;
  • NEGATIVE CONTROL: dropping one key by hand makes the same comparator report removed 1, so the zeros above are a reading and not a comparator that sees nothing;
  • the extract's own diff is 0 insertions / 18 deletions per provenance table (413 / 467 / 476 rows, exactly −18 each; 0 rows remain under metadataForms.field in any locale, while other families still hold theirs — metadataForms.object still has 11 / 43 / 43);
  • 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 — three ablations, four distinct assertions

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: the ledger imports the bundles by relative path inside its own package, and this package's one vitest alias is for @objectstack/lint and touches none of it.

ablation mutation suite assertions reddened
A — a decided leaf reverted to its en source in zh-CN (summaryOperations.relationshipField.label) anchor x1 to x0 · blob 01faac8ed174 to 11660fbdbbeb 2 failed / 19 passed the enumerated verdict and the derived panel pin, independently
B — a provenance row re-pointed at a decided leaf in zh-CN anchor x1 to x0 · blob 0f1c855d8a9a to 36f88141c07a 1 failed / 20 passed a third, different one: the second witness
C — a kept enum token rendered as words in zh-CN valueDomain.helpText anchor x1 to x0 · blob 01faac8ed174 to d33eede3bb5e 1 failed / 20 passed a fourth, different one: the machine-token guard

Every restore verified: blob back to HEAD's and git diff HEAD empty. ⇒ the two witnesses are not restatements of each other, and the #19430 guard can actually fail.

Verification

  • pnpm --filter @objectstack/platform-objects test47 files, 668 passed (46 / 647 before; +1 file, +21 tests)
  • pnpm --filter @objectstack/platform-objects typecheckexit 0. Its tsconfig.test.json program reaches the new file (--listFiles names it; 47 of 47 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 excludes *.test.ts by design and names the new file 0 times; that is the shape, not a gap
  • pnpm check:i18ngreen, 9 packages in sync. Its first run exited 3 (PREREQUISITE NOT MET — the built CLI plus the build closure of every package whose extract config it runs, which the gate names in one command); cleared by building that closure, then re-run green. Exit 3 is neither a pass nor a fail
  • pnpm lint — repo-wide, eslint . --no-inline-config, exit 0, at 6b03aa77c
  • 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 · 60 run green · 0 unrun · 0 NOT MEASURED. The first pass recorded pnpm check:dual-build-cjs-loads at exit 3 (PREREQUISITE NOT MET: it reads built output for every package in the repo and named seven with no dist/); after merging origin/main and building the closure it runs clean — 104 require entry points across 67 packages load
  • origin/main merged before opening (2 commits, zero overlap with this diff, no lockfile movement, no os-regen deferral recorded); the dependency closure was rebuilt and the whole verification re-run on the merged tree, and the gate derivation re-taken there is identical, family for family

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: the new leaves are present in dist/index.js, dist/plugin.js and dist/metadata-translations/index.js, with two pre-existing translated leaves as positive controls (对象 70 hits, フィールド 54) 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 measurably wrong, and a patch changeset is included.

Both halves of round 2's sharpened lesson confirmed in one run: esbuild emits UPPER-CASE hex in \uXXXX and \xNN for U+0080–U+00FF — Marcador de posición is findable only as Marcador de posici\xF3n (3 hits), the lower-case spelling reads 0, and the raw spelling reads 0. The raw negative control is in the probe so the next reader can see that a zero means "escaped", not "absent".

The five families that remain (29 label keys / 51 leaves, all-three-locale echoes, this base)

family keys leaves
action.fields.body.* + action.fields.ai 6 12
metadata-type display names with no panel: seed · mapping · api · doc · book · capability 6 12
object.fields.enable.* (+ validations) — the object capabilities panel 9 11
hook.fields.retryPolicy.* + hook.fields.body.memoryMb + hook.fields.timeoutMs 5 10
report.fields.drilldown + report.fields.runtimeFilter + report.sections.dataset_binding 3 6

⚠️ For whoever sizes round 5: the table is now sorted by LEAVES, and that order differs from the key order. object leads on keys (9) and is fourth on leaves (11), because seven of its nine keys are bare capability checkboxes carrying only a label. The rule "take the largest the previous table leaves" needs to say largest by what — on this base the answer is action or the six bare type-display pairs, both at 12, and not the 9-key row.

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

Acceptance notes

No class (a), (b) or (c) finding came out of this family, and that is a reading rather than a silence. The one leaf that could have reproduced #19430's enum trap was checked directly at the schema before any word was rendered (ValueDomainSchema, a z.enum, read in packages/spec/src/shared/value-domain.zod.ts), and its tokens were kept English; the second candidate (summaryOperations.function.helpText, documenting z.enum(['count','sum','min','max','avg'])) names no member at all, so it could not carry it. Nothing else on this panel names an enum's values in prose. #19430 itself is not this card's to fix, and the bounded-fix exemption does not reach it — it is a different defect class on a different panel.

Noted, not filed:

  • The blind spot's fourth shape, and it is not closed in general. repeater-row-properties.test.ts filters spec.type === 'repeater', so every field that declares fields under any other type keeps its children outside the walk while the extractor still emits catalog keys for them. Closed for THIS panel by the derived pin this PR adds. The general shape — and the fact that the pin's per-type vacuity control passes over it — is a real observation about an existing pin, not a reproducible defect reachable today, so it is prose here rather than a card. 承接者: the remaining-family rounds of this card, each of which will meet the same filter.
  • object.fields.fields.* (the embedded twin) is fully authored while the standalone panel was not. That asymmetry is what this round used as evidence, and it is now gone for field. Whether any other type has an embedded-vs-standalone pair in the same state is unmeasured here. 承接者: the object family row above — it is the panel that carries the embedded copy.
  • The eight bundle sets other than platform-objects 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 github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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 32b5831c4e48ac9b1626322cf185b286a09073aapackageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 32b5831c4e48ac9b1626322cf185b286a09073aa

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

@huangyiirene
huangyiirene marked this pull request as ready for review September 21, 2026 03:16
@huangyiirene
huangyiirene added this pull request to the merge queue Sep 21, 2026
Merged via the queue into main with commit 4045781 Sep 21, 2026
36 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-19403-round4-en-echoes branch September 21, 2026 03:46
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