fix(platform-objects): decide the standalone field panel en-echoes per leaf, and close the composite blind spot - #19485
Conversation
…leaves) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE
… field panel Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE
… its four properties Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE
📓 Docs Drift Check6 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): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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 |
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
fieldmetadata 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
.labelleaves, while nearly every label key carries a siblinghelpTextthat echoes with it and that no.labelcensus sees. Derived per family on this base, the two 9-key rows the round-3 table left are not the same size:field.fields.*— the standalone field panelobject.fields.enable.*(+validations)object's nine keys are mostly bare capability checkboxes with nohelpText(seven of the nine carry only alabel). ⇒ 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 basefbc12be31, 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.tsbundle (nested object literals with bare identifier keys — a regex requiring quoted keys returns 0, so this parses with the TypeScript AST) and compare.labelleaves againsten.Measured on worktree base
fbc12be31:enstring leaves in the metadata-form catalogs.labelleaveszh-CN) · 484 (ja-JP) · 484 (es-ES)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 inen: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 all —
relatedListFilter(#19085 added it tofield.form.tsalone) and three of the fivesummaryOperationsrow 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.helpTextnamesiana_time_zone,iso_4217_currencyandiso_3166_alpha2in prose. Those are the three members ofValueDomainSchema(packages/spec/src/shared/value-domain.zod.ts), az.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.object.fields.fields.valueDomain.helpTextreads "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.helpTextdocuments az.enum(['count','sum','min','max','avg'])but its text is the bare words "Aggregation function" and names no member.summaryOperations.field.helpTextnamescount, 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,ANDorstatus == receivedreds the suite, and the assertion carries a dark control so a green cannot come from anincludesthat 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 wordRowstwice:object.fields.fields.rows.labelis 行数 / 行数 / Filas (an editor height, counted in text rows) andreport.fields.rows.labelis 行 / 行 / 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 inzh-CNandja-JP.es-ESrenders both sensesFilas, so no choice arises there. Per locale, not per string.field.fields.relatedListFilter.label— the one label with NO same-string precedent.Related Listreturns zero hits across both catalogs. Decided by composing this catalog's own authored renderings of its two parts (Relatedis 关联 / 関連 / relacionad-, fromsys_email.fields.related_object.label;Filteris 筛选 / フィルター / Filtro, four precedents).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:
ANDstays English on an authored precedent (dataset.fields.filter.helpTextis 以 AND 方式并入每个查询 / すべてのクエリに AND で結合されます / combinado con AND en cada consulta), whileFKis rendered 外键 / 外部キー / Clave foránea (four authoredsys_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 inen, and the translated ones follow (zh-CN241/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:
translate, so asserting the rule over the ledger alone evaluates nothing — the suite feeds it a syntheticechorow, asserts it is refused, then asserts it is accepted once a departure reason is added.encatalog, not hand-listed. It reaches all 85 string leaves of thefieldentry (the 18 decided plus 67 already-authored neighbours), and its own dark control feeds it theencatalog and requires every leaf to come back flagged.source-hashes.generated.tscompanions. 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.ensource 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.fieldper locale, no more and no fewer — while the other 67 string leaves of the samefieldentry 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
recordblind spot, round 2composite, round 3 the repeater whose ownlabelsits one level above what the pin walks. This panel adds a fourth, measured from both sides:fieldFormdeclares two fields that enumerate children.optionsis atype: 'repeater'with six children —repeater-row-properties.test.tsderives every one of them, and all six were already translated.summaryOperationsdeclares five children in exactly the same shape but is atype: 'composite', so the pin'sspec.type !== 'repeater'filter skips it — while the extractor emits a catalog key for each, because it walks a form field's declaredfieldswhatever 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 — onoptions, 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.tsis generator-owned butgit check-attr mergereadsunspecified, so it is not routed tomerge=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:en/zh-CN/ja-JP/es-ESall 893 keys, 0 added, 0 removed, base vs head;removed 1, so the zeros above are a reading and not a comparator that sees nothing;metadataForms.fieldin any locale, while other families still hold theirs —metadataForms.objectstill has 11 / 43 / 43);enbundle and all four*.objects.generated.tsbundles are untouched.pnpm check:i18ngreen 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/lintand touches none of it.ensource inzh-CN(summaryOperations.relationshipField.label)01faac8ed174to11660fbdbbebzh-CN0f1c855d8a9ato36f88141c07azh-CNvalueDomain.helpText01faac8ed174tod33eede3bb5eEvery restore verified: blob back to HEAD's and
git diff HEADempty. ⇒ the two witnesses are not restatements of each other, and the #19430 guard can actually fail.Verification
pnpm --filter @objectstack/platform-objects test— 47 files, 668 passed (46 / 647 before; +1 file, +21 tests)pnpm --filter @objectstack/platform-objects typecheck— exit 0. Itstsconfig.test.jsonprogram reaches the new file (--listFilesnames 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 signaturestest-typecheck-debt.jsonalready records forsrc/feature-gate-guard.test.ts. The build config excludes*.test.tsby design and names the new file 0 times; that is the shape, not a gappnpm check:i18n— green, 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 failpnpm lint— repo-wide,eslint . --no-inline-config, exit 0, at6b03aa77cscripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackand reconciled with--rancarrying every exit code, each captured before any pipe: 60 derived · 60 run green · 0 unrun · 0 NOT MEASURED. The first pass recordedpnpm check:dual-build-cjs-loadsat exit 3 (PREREQUISITE NOT MET: it reads built output for every package in the repo and named seven with nodist/); after mergingorigin/mainand building the closure it runs clean — 104 require entry points across 67 packages loadorigin/mainmerged before opening (2 commits, zero overlap with this diff, no lockfile movement, noos-regendeferral 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 familyChangeset, measured
@objectstack/platform-objectsdeclares noprivateand shipsfiles: ["dist", …], andsrc/metadata-translations/index.tsimports all three translated bundles. Probed on the built output: the new leaves are present indist/index.js,dist/plugin.jsanddist/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-changesetwould be measurably wrong, and apatchchangeset is included.Both halves of round 2's sharpened lesson confirmed in one run: esbuild emits UPPER-CASE hex in
\uXXXXand\xNNfor U+0080–U+00FF —Marcador de posiciónis findable only asMarcador 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)
action.fields.body.*+action.fields.aiseed·mapping·api·doc·book·capabilityobject.fields.enable.*(+validations) — the object capabilities panelhook.fields.retryPolicy.*+hook.fields.body.memoryMb+hook.fields.timeoutMsreport.fields.drilldown+report.fields.runtimeFilter+report.sections.dataset_bindingobjectleads on keys (9) and is fourth on leaves (11), because seven of its nine keys are bare capability checkboxes carrying only alabel. The rule "take the largest the previous table leaves" needs to say largest by what — on this base the answer isactionor the six bare type-display pairs, both at 12, and not the 9-key row.The
fieldtype is now absent from the echo census and from themetadataForms.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, az.enum, read inpackages/spec/src/shared/value-domain.zod.ts), and its tokens were kept English; the second candidate (summaryOperations.function.helpText, documentingz.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:
repeater-row-properties.test.tsfiltersspec.type === 'repeater', so every field that declaresfieldsunder 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 forfield. Whether any other type has an embedded-vs-standalone pair in the same state is unmeasured here. 承接者: theobjectfamily row above — it is the panel that carries the embedded copy.platform-objectsstill 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