Skip to content

fix(spec): serve a field's translated help on description, never on an undeclared help - #21956

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21948-translated-field-help-declared-key
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21948-translated-field-help-declared-key

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21948

Clause-②: no

What changed

translateObject used to put a bundle's objects.OBJECT.fields.FIELD.help entry on a help key that FieldSchema does not declare. The served field then failed FieldSchema with unrecognized_keys. A consumer that reads only declared keys rendered the English description, and the console logged one ingestion warning per such field.

  • Target key, measured. The extractor writes that bundle entry from field.help ?? field.description (packages/cli/src/utils/i18n-extract.ts:1223). FieldSchema refuses help, so on a spec-valid field the only source is description. The nine bundle source trees author 0 inlineHelpText and 0 field help. The nine en bundles carry 523 help entries.
  • translateField (packages/spec/src/system/i18n-resolver.ts:3054) now serves the translated help on description and writes no help key. inlineHelpText is not touched.
  • Precedence: ADR-0029 D9.2a, the resolver's own rule, applied through the existing valueOverridesPackagedBase predicate. There is no second comparison. The catalog applies only while the served field's description equals the packaged field's.
  • ObjectFieldLike (:2453) drops its help?: string member. This is safe:
    • its [key: string]: any index signature still accepts and types a help key, so a caller that writes or reads one still compiles (probe below);
    • packages/spec/api-surface/system.json records only ObjectFieldLike (interface), not its members, and check:api-surface stays green;
    • the one in-repo importer (service-analytics, which reads options only) still typechecks.
  • The translateObject docblock no longer says it translates a field's help. A new section states the target key and the D9.2a precedence.
  • .changeset/21948-spec-field-help-served-on-description.md: @objectstack/spec patch.

Clause-② — measured no

  • No Zod schema, parse or export changed. These all stay green: check:authorable-surface, check:api-surface, check:export-origins, and check:generated (15 artifacts up to date).
  • Type probe (scratch, not committed), compiled against the rebuilt dist/system/index.d.ts, exit 0:
    • const legacy: ObjectFieldLike = { name: 'x', help: 'legacy' } compiles, and so does reading legacy.help;
    • a declared member still refuses a wrong value (an @ts-expect-error on label: 42 is consumed);
    • const proof: number = ({} as ObjectFieldLike).help compiles, which shows the probe read the rebuilt declaration.
  • Control leg: the same probe against a copy of that .d.ts with BASE's help?: string put back gives exit 2, with exactly one error: TS2322 on the proof line. Writing and reading help compile under both declarations.
  • So no value the type or the schemas used to accept is refused now. The one type-level delta is that ObjectFieldLike['help'] is now read through the index signature (any) instead of string | undefined. That is stated here for the at-tier review.

Measured

All at HEAD 3ad2f22bed unless stated.

Corpus at the resolver seam (scratch script, not committed). Every object in packages/platform-objects/scripts/i18n-extract.config.ts (48 objects, 617 fields), with that package's real bundles, using each object as its own packaged base:

served help keys help refused by FieldSchema sys_user fields with help zh-CN description equal to the bundle entry
BASE behaviour (ablation leg 1 below) 332 332 18 of 26 0
HEAD 0 0 0 of 26 332
  • At HEAD in zh-CN, sys_user.two_factor_enabled.description = 该用户是否已启用双因素认证。由 better-auth 的 \twoFactor` 插件维护。`.
  • In en, 0 descriptions change from source, because the en entries repeat the source.
  • NOT MEASURED: the HTTP door (GET /api/v1/meta/object/sys_user). The spec change invalidates the showcase build closure: 62 of 63 turbo tasks miss the cache. The pins and the corpus run read the same translateMetadataDocument dispatch the REST read calls.

Pins (packages/spec/src/system/i18n-resolver.test.ts:3552, 9 cases):

  • a zh-CN served field carries no help, has the translation on description, and parses with no unrecognized_keys;
  • the untranslated control is unchanged;
  • a field with both description and inlineHelpText gets the translation on description, and inlineHelpText is untouched;
  • a diverged description is kept in zh-CN and en, through the type dispatch, while the undiverged sibling is translated;
  • a field the base does not declare counts as diverged;
  • with no packaged base (undefined, null, or omitted) the catalog applies;
  • an absent description is filled by the catalog;
  • array-shaped fields are judged against an array-shaped base;
  • the input is not mutated.

No existing pin asserted a served field help. I searched every test outside the bundle suites, so none had to move.

Ablation, run on the committed head. Each leg used scripts/ablation-replace.mjs in WRAP mode plus a trap. Predictions were written down before the run. Both legs were restored, with the restored blob equal to HEAD (a8eade91) and git diff HEAD empty:

leg mutation (anchor 1 → 0 on disk) predicted observed
1 restore BASE's two lines (write next.help, never description) 7 failed / 2 passed (control and no-mutation stay green) Tests 7 failed | 2 passed
2 flat overlay (the D9.2a comparison replaced by true) 3 failed: the diverged, undeclared-field and array cases Tests 3 failed | 6 passed
  • Leg 1 sample: expected 'Whether two-factor authentication is …' to be '该用户是否已启用双因素认证。…'.
  • Leg 2 sample: expected '该用户是否已启用双因素认证。…' to be 'Edited by the tenant.'.

Tests

  • @objectstack/spec, full local project: Test Files 619 passed, Tests 18480 passed | 1 todo.
  • pnpm --filter @objectstack/spec typecheck: exit 0. Its test layer compiles under tsconfig.test.json with the identity-pinned debt held.
  • @objectstack/service-analytics: typecheck exit 0. The three dimension/label suites that call translateObject: Tests 53 passed.
  • Targeted eslint over the two changed .ts files: 0 errors and 0 warnings. That run is not the repo-wide lint, which is CI's.

Gates. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran derived 85 families: 84 ran with exit 0 and 1 is NOT MEASURED.

  • The NOT MEASURED one is pnpm check:dual-build-cjs-loads, which exits 3 with PREREQUISITE NOT MET because 78 packages have no dist/.
    • Narrowed in its place: all 19 require entries of @objectstack/spec's exports load from the rebuilt dist, and system.translateObject is a function.
  • pnpm check:lean-entry-closure first answered exit 3 (objectql/core unbuilt). After turbo run build --filter=@objectstack/objectql... it answered exit 0, and that is the recorded code.

Ships: npm pack --dry-run of @objectstack/spec lists dist/system/index.js, .mjs and .d.ts. Both runtime files carry packagedObjectField and no next.help = translatedHelp.

Base: origin/main moved to a3bd157730 (#21947, test titles under packages/spec/src/ui/ only). None of this PR's files is touched, so it is not merged.

Known readers in objectui

objectui at .objectui-sha 0abd4f9f, read-only. No objectui file is edited here.

  • packages/plugin-form/src/ObjectForm.tsx:1033 and packages/plugin-form/src/sectionFields.ts:311 (field.help || field.description), and packages/app-shell/src/utils/resolveActionParams.ts:714 (param.helpText ?? field.help ?? field.description).
    • All three fall back to description, so they render the same translated text, and their help arm is now dead. Retiring those arms is objectui's job.
  • The ingestion warning is packages/core/src/utils/reference-keys.ts:362, reached through the no-declared-twin arm (:415) of canonicalizeRetiredFieldKeys. It fires for any undeclared key on a served field def, so it goes quiet once no help is served.

Acceptance notes

None of these is addressed in this PR.

  1. Studio saveFields write-back. NOT MEASURED; this is a read-only inference.
    • objectui packages/app-shell/src/services/MetadataService.ts:898 to :939 carries per-field server keys from the translated read back into the object PUT.
    • help is not in objectui's RETIRED_FIELD_KEYS (packages/types/src/internal/retired-field-keys.ts). So a field save on a platform object with bundle help entries should have sent a key FieldSchema refuses.
    • This PR removes the source.
    • Carrier: this PR. Reach would need one measured PUT.
  2. The extractor's field.help ?? arm (packages/cli/src/utils/i18n-extract.ts:1223) reads a key FieldSchema refuses, so it is dead on every spec-valid field. Carrier: domain:cli; none in flight.
  3. Field placeholder is never overlaid. FieldTranslationSchema declares placeholder, and the extractor emits objects.OBJECT.fields.FIELD.placeholder (:1224), but translateField never overlays it. The nine shipped bundles carry 0 field-level placeholder entries (the 4 per locale are action params), so it is dormant. Carrier: none.
  4. inlineHelpText has no translation path. The extractor never reads it, so an authored one is never offered for translation. The nine bundle sources author 0 of them. Carrier: none.
  5. Docs line. content/docs/protocol/kernel/i18n-standard.mdx:166 lists a field's label / help / placeholder as display labels, but FieldSchema declares no help. Carrier: none.

Generated by Claude Code

claude added 2 commits October 6, 2026 05:19
… an undeclared `help`

translateObject overlaid the bundle's `objects.OBJECT.fields.FIELD.help`
entry onto a `help` key FieldSchema does not declare. The entry is the
translation of the field's `description` (the i18n extractor writes it from
that key), so it is now served there, under ADR-0029 D9.2a: the catalog
applies only while the served description equals the packaged field's
(valueOverridesPackagedBase), and with no packaged base it applies.
ObjectFieldLike drops its `help` member.

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:system tests tooling labels Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 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
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 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 01e0f71ad8518ed208dc95eb7bb6bf2b2fc2fa05 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1562b97cc7f3ebadae0712bb31964613a5fa3a83 — the merge of head 3ad2f22bed534424e1af81d7d96c38eab84afd0d into base 01e0f71ad8518ed208dc95eb7bb6bf2b2fc2fa05, 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 1562b97cc7f3ebadae0712bb31964613a5fa3a83 && git checkout 1562b97cc7f3ebadae0712bb31964613a5fa3a83
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 01e0f71ad8518ed208dc95eb7bb6bf2b2fc2fa05 3ad2f22bed534424e1af81d7d96c38eab84afd0d && git checkout -B drift-repro 01e0f71ad8518ed208dc95eb7bb6bf2b2fc2fa05 && git merge --no-ff 3ad2f22bed534424e1af81d7d96c38eab84afd0d

node scripts/docs-audit/affected-docs.mjs --json 01e0f71ad8518ed208dc95eb7bb6bf2b2fc2fa05

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

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 protocol:system size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(i18n): translateObject writes translated field help onto an undeclared help key — the console warns ~520× per page load and drops the value

2 participants