Skip to content

fix(platform-objects): carry the tree-reference rule into the es-ES, ja-JP and zh-CN reference help text - #16858

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-16820-reference-helptext-three-locales
Sep 8, 2026
Merged

fix(platform-objects): carry the tree-reference rule into the es-ES, ja-JP and zh-CN reference help text#16858
os-musk merged 2 commits into
mainfrom
claude/issue-16820-reference-helptext-three-locales

Conversation

@os-musk

@os-musk os-musk commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16820

One leaf, three locales: metadataForms.object.fields.fields.reference.helpText in es-ES, ja-JP and zh-CN. Nothing else moves.

All readings below are pinned to this PR's head, dcdb32971f.

Clause-②: no

What was wrong, and why it is a bug

The English source for this row gained a normative sentence on 2026-09-05. The three translated locales still served the pre-2026-09-05 text, and were wrong in both directions at once — they dropped the tree rule, and they asserted a purpose the source no longer states.

locale before after
en (source copy, untouched) Target object name. For a tree field it is optional and, if given, must be this object (a tree is a hierarchy within its own object — link a different object with a lookup) unchanged
es-ES Objeto de destino (para lookup/master_detail) Nombre del objeto de destino. En un campo tree es opcional y, si se indica, debe ser este mismo objeto (un árbol es una jerarquía dentro de su propio objeto — para enlazar con otro objeto, usa un lookup)
ja-JP 対象オブジェクト(lookup/master_detail 用) 対象オブジェクト名。tree フィールドでは省略可能で、指定する場合はこのオブジェクト自身でなければなりません(ツリーは同一オブジェクト内の階層です — 別のオブジェクトへリンクする場合は lookup を使います)
zh-CN 目标对象(用于 lookup / master_detail) 目标对象名称。tree 字段可省略;若填写,必须是本对象自身(树是同一对象内部的层级 — 链接到其他对象请用 lookup)

The dropped half is the user-facing side of a constraint enforced at parse time, so a Spanish, Japanese or Chinese author was told a purpose that excludes tree, got no hint, and met the rule as a parse failure. packages/spec/src/data/object.zod.ts is where the refusal lives:

tree field `FIELDNAME` on object `OWNNAME` references `TARGET`,
but a `tree` field's `reference` must name the declaring object itself — a hierarchy is
parent/child within one object. Drop `reference` (it is optional on a `tree`), set it to
'OWNNAME', or declare a `lookup` if a link to a different object was meant.

The fossils, read before rewriting anything

1. The current English source, established by reading the tree

The leaf is generated from packages/spec/src/data/object.form.ts:230, whose own comment states the intent:

// ONE shared row for the three reference-carrying types (#14892):
// the text carries the `tree` rule the schema enforces — optional,
// and if given this object — so the designer never invites the
// foreign target `ObjectSchema` refuses at publish.
{ field: 'reference', type: 'text', helpText: 'Target object name. For a tree field it is optional and, if given, must be this object (a tree is a hierarchy within its own object — link a different object with a lookup)', visibleWhen: "data.type in ['lookup','master_detail','tree']" },

The commit that put it there:

fb77aa541f7a662ac83dc6cc9e5f1cb51ec597af   Sat Sep 5 22:43:37 2026 +0000
feat(spec)!: a `tree` field's `reference`, when present, must name the declaring
object — refused at parse otherwise (#14892) (#15979)

2. The instrument is working as designed — this PR does not touch it

source-hash.ts's own file header documents the coverage hole, in the section "Why the generated tables can be BACKFILLED with no history":

The generated predicate only ever fires on a leaf whose value is still a byte
copy of the recorded revision. So the only records worth writing are for
leaves that ARE currently source copies, and those are identifiable from the
committed tree alone: `value === currentSource ⇒ record hash(value)`. A leaf
that differs from the current source is left with NO record — legacy-trusted,
exactly as the ruling's property 1 requires — because nothing in the tree says
which revision it was made from.

Measured on this tree, with firing controls, the three tables hold no record for this leaf — which is correct and stays correct after this PR, because the new values are real translations rather than source copies:

es-ES  579 total records · 69 under metadataForms.object.fields · 0 for fields.reference
ja-JP  570 total records · 69 under metadataForms.object.fields · 0 for fields.reference
zh-CN  516 total records · 37 under metadataForms.object.fields · 0 for fields.reference

The two positive controls are the non-zero counts; the negative control (metadataForms.object.fields.ZZZNOPE) returned 0 in all three. No hash table is edited by this PR.

3. Hand-editing the leaf value is the sanctioned path, and the tree says so

Every one of these bundles carries this docblock, es-ES.metadata-forms.generated.ts included:

 * Auto-generated by 'os i18n extract' for locale 'es-ES'.
 * Edit translations in place; re-run extract (with --merge) to fill new gaps.
 * Do not hand-edit the structure — only the leaf string values.
 * Merge only fills gaps: correcting a source label/description does not
 * push the correction into a leaf here that already holds a translation —
 * a present-but-stale string is not a gap, so it is left as-is. Re-translate
 * it by hand when its source changes; nothing here or in `os i18n check`
 * tells a leaf a translator updated on purpose from one nobody has looked
 * at since the source moved.

That is how the hand edit is proven sanctioned rather than assumed: the generator's own header prescribes "Edit translations in place", restricts the prohibition to "the structure — only the leaf string values", and states that re-running the generator would not perform this repair, because "a present-but-stale string is not a gap, so it is left as-is". Re-running os i18n extract --merge is therefore the one thing that could not have produced this diff. Only leaf string values change here; pnpm check:i18n independently confirms the structure is intact.

Verification

The leaf moved, both halves, in all three locales

Values after the change, extracted by anchoring on the leaf's dotted key rather than by line number:

LEAF en     "Target object name. For a tree field it is optional and, if given, must be this object (a tree is a hierarchy within its own object — link a different object with a lookup)"
LEAF es-ES  "Nombre del objeto de destino. En un campo tree es opcional y, si se indica, debe ser este mismo objeto (un árbol es una jerarquía dentro de su propio objeto — para enlazar con otro objeto, usa un lookup)"
LEAF ja-JP  "対象オブジェクト名。tree フィールドでは省略可能で、指定する場合はこのオブジェクト自身でなければなりません(ツリーは同一オブジェクト内の階層です — 別のオブジェクトへリンクする場合は lookup を使います)"
LEAF zh-CN  "目标对象名称。tree 字段可省略;若填写,必须是本对象自身(树是同一对象内部的层级 — 链接到其他对象请用 lookup)"

The absence check on the removed claim is leaf-scoped, because master_detail legitimately survives elsewhere in each file:

es-ES  falseAssertionGone=true  [posControl(leaf visible)=true  negControl(must be false)=false]
ja-JP  falseAssertionGone=true  [posControl(leaf visible)=true  negControl(must be false)=false]
zh-CN  falseAssertionGone=true  [posControl(leaf visible)=true  negControl(must be false)=false]

File-level, master_detail goes 3 occurrences to 2 in each of the three files. The two survivors (lines 1245 and 1275) are the dataset form's own rows, which are genuinely about lookup / master_detail relationships and are out of scope here.

Before the change, the same greps fired the other way — 3 hits per file, the leaf among them — which is the positive control proving these checks can detect the claim at all.

It reaches consumers

@objectstack/platform-objects@17.3.0 is published (private absent, publishConfig.access: public, files: ["dist", ...]). Reading the built bundle, after decoding the \uXXXX escapes the build emits:

DIST FILE: dist/metadata-translations/index.mjs
FIRING CONTROL — en source leaf present in dist: true
NEGATIVE CONTROL — impossible string absent: true
es-ES  newTranslationShips=true   falseAssertionGoneFromDist=true
ja-JP  newTranslationShips=true   falseAssertionGoneFromDist=true
zh-CN  newTranslationShips=true   falseAssertionGoneFromDist=true

A raw byte grep of dist/ returns zero for every non-ASCII string, in both directions, because the bundler escapes them — that reading is void, and only the decoded comparison above counts.

Package suite

pnpm --filter @objectstack/platform-objects test
  Test Files  37 passed (37)
       Tests  545 passed (545)

pnpm --filter @objectstack/platform-objects typecheck
  check:test-typecheck: OK — @objectstack/platform-objects's test layer compiles under
  packages/platform-objects/tsconfig.test.json; 1 file(s) / 3 error(s) / 2 pinned
  signature(s) held in test-typecheck-debt.json (shrink-only and identity-pinned)

Both under scripts/pm/os-verify-lock.sh, which reported VERDICT command-exit 0.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at head dcdb32971f, all 50 run, then reconciled. The reconciliation line, verbatim:

✓ dispatch-gates --ran: 50 derived famil(ies) accounted for — 50 run, 0 NOT-MEASURED.

That line accounts for coverage, not verdicts. The verdicts, stated separately: 50 of 50 exited 0 at head dcdb32971f.

On the first pass two gates exited 3 (PREREQUISITE NOT MET). Exit 3 is NOT MEASURED and never a pass, so each named closure was built and the gate re-run to a real verdict:

pnpm check:i18n                 EXIT=0
  check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).
    platform-objects               in sync (11 bundle(s))

pnpm check:dual-build-cjs-loads  EXIT=0
  ✓ 104 published require entry point(s) across 67 package(s) load; 620 emitted
    CommonJS file(s) parse

check:i18n is the load-bearing one here: it is what proves the hand edit left the generated structure and key set in sync with the source.

Every exit code was captured before any pipe (cmd > log 2>&1; EXIT=$?), never through | head or | tail.

Lint

The repo-wide eslint . --no-inline-config scan belongs to CI. The narrowed local run is a measurement, with all three pieces of evidence:

  1. Receiving population, from ESLint's own config resolution (isPathIgnored over git ls-files): 6358 lintable files, 0 ignored — so the population is 6358, not a guess.
  2. Files linted, from --format json: 3 files, 0 errors, 0 warnings.
  3. Invariance, quoted from eslint.config.mjs itself: "this repo runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, test or not" — so this diff cannot move the verdict on any file it does not touch.

Bytes

pnpm check:nul-bytes green, plus a direct scan of the edited files for control characters: no matches.

Changeset

patch, on @objectstack/platform-objects — route 1 of the WHICH LEVEL block in .github/workflows/pr-automation.yml, decided on the dist measurement above rather than on size: the strings do reach a published package's dist, so this releases something and skip-changeset would be wrong. The level is patch and not minor because no public surface widens — no exported symbol is added and no key is added to a published payload; three leaf string values change. AGENTS.md lines 1017-1020 set the floor: "A bug fix in a released package takes a patch changeset — never none, and ⛔ never skip-changeset".

This matches triage's grading, 「按 i18n 修正处理级别即可,⛔ 不必按破坏性变更发」.

Scope, and the four exclusions

Neighbouring cards, named for context only and none of them addressed by this PR: #16227 was bounded to the nine leaves it enumerates and this leaf is in neither that count nor its two borderline cases; #16165 concerns other leaves of the same class and does not reach this one; #15979 is the source edit that created the gap, and it remains as it is.

Notes for review

  • One paragraph of the dispatch order cited the changeset floor at AGENTS.md:1033-1036. On this tree the floor sentence is at lines 1017-1020 and the landing rule at 1012-1016; the text is unchanged, only the line numbers differ. Quoted above from the file rather than from the order.
  • The card writes the leaf path as metadataForms.object.fields.fields.reference.helpText. In the bundle source that is the dotted string key "fields.reference" nested under object.fields, which is also exactly how the *.source-hashes.generated.ts tables spell their keys. A first attempt to resolve the path with an identifier-only walker returned "PATH NOT FOUND" for all four locales; since all four failing together is an instrument failure rather than a reading, it was discarded and replaced with the key-anchored extractor used throughout above.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg


Generated by Claude Code

…ja-JP and zh-CN reference help text

The English source for `object.fields.reference` gained a normative sentence on
2026-09-05: a `tree` field's `reference` is optional and, when present, must
name the declaring object — a link to a different object is a `lookup`. The
rule is enforced at parse time.

The three translated locales still served the pre-2026-09-05 text, and were
wrong in both directions at once: they dropped the tree rule, and they asserted
a purpose the source no longer states ("for lookup / master_detail") that the
tree case contradicts. Both halves move in all three locales.

Only the three `helpText` leaf values change. The `label` siblings, the key set
and the generated structure are untouched, and the `*.source-hashes.generated`
tables correctly keep no record for a leaf that is a real translation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
@github-actions github-actions Bot added the size/s label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 2 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 7f745c3ffc5eae6a72999180d0b87d39ff9ebbbapackageMentionDocs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Sep 8, 2026
@os-musk
os-musk marked this pull request as ready for review September 8, 2026 11:51
@os-musk
os-musk enabled auto-merge September 8, 2026 11:51
@os-musk
os-musk added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit c930f85 Sep 8, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-16820-reference-helptext-three-locales branch September 8, 2026 12:17
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/s tooling

Projects

None yet

2 participants