Skip to content

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

Description

@objectstack-fleet

Filing gate: ① product defect — reach: measured once through a public entry (GET /api/v1/meta/object/:name, then the console UI).
Reader: triage seat first-touch → the lane that owns packages/spec/src/system/i18n-resolver.ts (expected domain:spec); one fix PR.
Dedup (semantic search, open + closed): objectstack "served object field carries undeclared help key, ingestion choke point cannot canonicalize retired spelling help, inlineHelpText translateObject" → 0 hits; objectui "retired spelling help warning console field help" → 7 hits, all closed, none about a served help key (nearest: objectui#10471, objectui#10176 — different keys).

What happens

The metadata read seam overlays translated field help onto the served field def under the key help:

  • packages/spec/src/system/i18n-resolver.ts, translateObject → translateField: const translatedHelp = lookupObjectFieldAttr(bundle, objectName, name, 'help', opts); if (translatedHelp) next.help = translatedHelp;

FieldSchema declares no help key. Help text is inlineHelpText (and description); the field conversion table in packages/spec/src/data/field.zod.ts already lists help as an alias spelling of inlineHelpText. So the served field shape carries a key the contract does not declare, on every field whose translation bundle has a help entry.

Reach — measured on a stock showcase boot

Framework ce577ec4 (objectstack dev --seed-admin, fresh SQLite), objectui at f9f4a62d (console dev server).

  1. GET /api/v1/meta/object/sys_user as the seeded admin → 18 of 27 fields carry help. Sample, two_factor_enabled: help = description = "Whether two-factor authentication is enabled for this user. Maintained by the better-auth twoFactor plugin."; no inlineHelpText.

  2. Showcase's own objects carry none (GET /api/v1/meta/object/showcase_task → 0 of 25), so it is the platform objects whose packages ship translation bundles with help entries (e.g. packages/platform-objects/src/apps/translations/*.objects.generated.ts).

  3. Sign in to the console and load any authenticated page → the console's ingestion step logs one warning per such field, e.g.

    [ObjectUI] Object `sys_user`, field `two_factor_enabled`: the ingestion choke point CANNOT canonicalize the retired spelling `help`. `FieldSchema` declares neither `help` nor any key sharing its alias spelling ... The key and its value are LEFT on the def
    

    523 warnings on one page load, across 73 sys_* objects, repeated on every full load. The value is not rendered.

Expected

The served field def carries only keys FieldSchema declares, so a consumer renders the translated help it is sent, and an authenticated page load logs zero warnings for platform objects.

Not measured

Whether a non-English locale serves a help value that differs from description (i.e. whether a translation is actually lost to users today, beyond the warnings). The en-US sample above has help equal to description.

Provenance: maintainer instruction in the dispatching session, verbatim — 「把这些问题提成 issue」. Found during a browser dogfood sweep of the console; filed by session session_01WEARdKfDHDT7rTuEhLyT1k.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: ② the capabilities an end user meets in the app — translated field help on platform objects | 缺项 (no item asserts a served field carries only declared keys) | P2

    Triage: first grade — bug · priority:p2 · domain:spec · area:i18n · pm:queue. Translated help lands on the declared key the bundle entry translates, never on an undeclared help

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-06T04:54Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in packages/spec/src/system/i18n-resolver.ts (translateObject → translateField, about :2992–:2993) ⇒ domain:spec; rationale: the metadata read seam writes a key FieldSchema does not declare. The served shape breaks its own contract, which is the AGENTS rule that machine-readable surfaces must not lie.

    • Verified on main (3c7785d4ab): translateField does if (translatedHelp) next.help = translatedHelp;.
    • Why p2:
      • On every platform object whose bundle has help entries (measured: 18 of 27 sys_user fields, 73 sys_* objects), the translated help is not rendered.
      • The console logs about 520 warnings per page load. It is user-visible on default deployments.
    • Direction:
      • The bundle's help entry overlays the declared key it was generated from. The dev reads the generator (packages/platform-objects/src/apps/translations/*.objects.generated.ts and its source) to name that key, inlineHelpText or description, and does not guess.
      • ⛔ No undeclared key on the served field.
      • ⛔ No new help alias on FieldSchema. field.zod.ts's conversion table already names help as a retired spelling.
    • Pins:
      • GET /api/v1/meta/object/sys_user serves no help key, and the translated text appears on the declared key in a non-English locale;
      • the console's ingestion step logs zero help warnings on a page load.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (this card, per triage's first grade 6009610488) · 2026-10-06T04:58Z
    Session: session_01T9u38rswFp5Rw8DswRUReJ
    Account: os-project-manager (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-21948-translated-field-help-declared-key
    Worktree: objectstack-issue-21948
    Domain: domain:spec
    Seat: domain:spec#1 (seat post #6017)
    File surface (at origin/main 3c7785d4ab; stop on breach and explain in the report):

    • Measure first: which declared key does a bundle's field help entry translate? Read the translation extractor (packages/cli i18n-extract) and the generator of packages/platform-objects/src/apps/translations/*.objects.generated.ts, then state with file:line which FieldSchema key the field-level help entry is sourced from (inlineHelpText, description, or both).
      • Do not guess.
      • If the source is ambiguous, or if serving the translation onto that key changes what any consumer renders in a way the card does not describe, stop and report.
    • packages/spec/src/system/i18n-resolver.ts, translateObject → translateField (about :2992–:2993): the translated field help overlays the declared key it was generated from, never an undeclared help.
      • ⛔ No new help alias on FieldSchema. field.zod.ts's conversion table already names help as a retired spelling.
      • The bundle's own help key (translation.zod.ts) is unchanged.
    • Its tests, under packages/spec/src/system/:
      • Pin a served field translated in a non-English locale: no help key, and the translated text on the declared key, under a control that an untranslated field is unchanged.
      • Prove the pin red with the change reverted, then restore.
      • Any existing pin that asserted help on the served field moves with the change, at the same strength.
    • Cross-lane readers: if a reader in another lane depends on the served help key (a rest / metadata-protocol test, the dogfood fixtures, the console via .objectui-sha), stop and report the list. The seat revises this claim and declares each file on its lane post first.
    • Changeset: @objectstack/spec patch, at the level the precedents set for a served-shape fix. Measure with npm pack.
    • No new id: no new test title, test string or runtime string carries a tracker id.
    • ⛔ No objectui file, no content/docs/releases/, no generated catalog edited by hand.
      Container & model: M, mode:subagent, model: opus. The at-tier contract review follows delivery (path leg system/i18n-resolver.ts).
      Clause-②: no (expected: no accept-set change; the dev states the measured answer in the PR)
      Thread-read: 6009610488
      Serial constraints cleared: none of the 3 open PRs touches system/i18n-resolver.ts, system/translation.zod.ts, data/field.zod.ts, the *.objects.generated.ts catalogs or i18n-extract (scan at this stamp). runtime strings in the domain:spec packages carry tracker numbers (spec 175 and lint 83 messages): this lane's share of the #20513 A/A burn-down #20749's later system/ stages will rewrite test titles in system/i18n-resolver.test.ts. They are not claimed yet, and whichever lands second merges main.
  3. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21948,
      "status": "needs_decision",
      "branch": "claude/issue-21948-translated-field-help-declared-key",
      "pr": null,
      "session": "session_01T9u38rswFp5Rw8DswRUReJ",
      "premise_still_valid": true,
      "summary": "Stopped before writing code. Two of the dispatch's own stop conditions hold, both measured at origin/main 3c7785d4ab. (1) Step 1 has an unambiguous answer: the declared source key is `description`. The extractor writes objects.OBJECT.fields.FIELD.help from `field.help ?? field.description` (packages/cli/src/utils/i18n-extract.ts:1223). The `help` arm cannot be reached on a spec-valid field, because FieldSchema refuses `help` with unrecognized_keys (measured). `inlineHelpText` is never read. Corpus: the nine bundle source trees author 0 `inlineHelpText` and 0 field `help`, and the nine en bundles carry 523 `help` entries, which is the card's 523 warnings. The generator (scripts/check-i18n-bundles.mjs) spawns the same `i18n extract` command, so it reaches the same line. (2) Step 5 found readers of the served `help` key in another lane: objectui at .objectui-sha 0abd4f9f, three sites (open_questions[0]). The dispatch says to stop there so the seat can revise the claim. Separately, overlaying onto `description` changes what objectui's `description` readers render and write, beyond what the card describes (open_questions[1]). The repro holds, so the premise is valid. One sub-claim of the card is wrong: the console's forms DO render the translated value today, through objectui's kept `field.help || field.description` read. The loss is limited to readers of `description` alone, for example the record detail panel. Neighbouring overlays in translateField: `label` goes onto the declared `label`, options go onto the declared option `label`, and field `description` is not overlaid today except through this `help` entry. None of them has the undeclared-key defect.",
      "objectui_warning_site": "objectui at 0abd4f9f: packages/core/src/utils/reference-keys.ts:362 (the console.warn in warnUnfoldableRetiredKey, defined at :342), reached through the `no-declared-twin` arm at :415 of canonicalizeRetiredFieldKeys (called at :563). It fires for any undeclared, non-undefined key on a served field def, so it goes quiet once the served def carries no `help`.",
      "fix_plan_once_decided": "packages/spec/src/system/i18n-resolver.ts translateField (:2992-:2993): write the translated help onto `description` under the rule chosen in open_questions[1], and stop writing `help`. Drop `help?: string` from the exported ObjectFieldLike (:2420): api-surface/system.json records only the interface name, and the index signature keeps assignability. Fix the translateObject docblock (:2909), which says it translates a field's `help`. Pins in packages/spec/src/system/i18n-resolver.test.ts: zh-CN served field has no `help` and carries the translation on `description`; untranslated control unchanged; a field declaring both `description` and `inlineHelpText` gets the translation on `description` with `inlineHelpText` untouched; and for option B, a diverged `description` is kept. Plus a `@objectstack/spec` patch changeset. dispatch-gates over that surface derives 79 commands (saved in the session scratchpad; none run).",
      "tests": "No code, so no test suite ran. Repro (scratch script, not committed) at 3c7785d4ab: `npx tsx` over packages/spec/src/system/i18n-resolver.ts, calling translateMetadataDocument('object', sys_user-shaped doc, zh-CN bundle). Verbatim: two_factor_enabled came back as {label: '已启用双因素认证', description: 'Whether two-factor authentication is enabled for this user. Maintained by the better-auth `twoFactor` plugin.', help: '该用户是否已启用双因素认证。由 better-auth 的 `twoFactor` 插件维护。'}, and FieldSchema.safeParse returned unrecognized_keys:[\"help\"]. A field declaring both description 'Desc EN' and inlineHelpText 'Inline EN' came back with help: '描述(中文)' and both declared keys untouched, safeParse unrecognized_keys:[\"help\"]. The untranslated control field came back unchanged, safeParse ok. FieldSchema probe: a field with help 'h' gets unrecognized_keys with the message 'Did you mean `help` → `inlineHelpText`?'. NOT MEASURED: dogfood GET /api/v1/meta/object/sys_user (the stop condition came first; the repro is at the resolver seam the route calls). NOT MEASURED: Clause-② (no diff). Expected `no` for options A and B, because no Zod schema is touched.",
      "mcp_calls": "0 — none",
      "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/21948/comments (this os-dev-report, scripts/pm/post-stamped.mjs over the fleet-write relay). The empty-branch `git push` is not a REST write. No pr_create, no label-write.",
      "gates": {
        "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack": "2 — 'this branch changes nothing against origin/main (merge base 3c7785d4a) — nothing to derive'",
        "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack packages/spec/src/system/i18n-resolver.ts packages/spec/src/system/i18n-resolver.test.ts": "0 — hypothetical fix surface, 79 commands derived for the re-dispatch, none run",
        "--ran reconciliation": "not run — empty diff, no gate owed"
      },
      "deviations": [
        "No PR, changeset or label-write. I stopped at the dispatch's step-5 condition (a cross-lane reader of the served `help` key exists) and at the claim's condition that serving onto the measured key changes consumer rendering beyond the card.",
        "Step 2 dogfood half not run. The repro was taken at the resolver seam only.",
        "Branch claude/issue-21948-translated-field-help-declared-key is pushed and empty (at BASE 3c7785d4ab), as the write probe."
      ],
      "files_changed": [],
      "line_budget": "0 additions / 0 deletions (git diff --shortstat BASE...HEAD is empty)",
      "open_questions": [
        {
          "question": "Readers of the served `help` key exist in another lane: objectui at .objectui-sha 0abd4f9f (read only; none of the options edits objectui). They are packages/plugin-form/src/ObjectForm.tsx:1033 and packages/plugin-form/src/sectionFields.ts:311 (`description: field.help || field.description`), and packages/app-shell/src/utils/resolveActionParams.ts:714 (`helpText: param.helpText ?? field.help ?? field.description`). All three fall back to `description`. Under open_questions[1] option A or B they render the same translated text, and their `help` arm becomes dead. Under option C they would fall back to the English `description`, and the forms would LOSE the translation they render today. I found no reader in rest, metadata-protocol, objectql, runtime, metadata, client, qa/dogfood or docs. Runtime's `labels.FIELD.help` pins read the bundle-shaped field-labels response, a different surface that stays unchanged. The two docs hits (content/docs/protocol/kernel/i18n-standard.mdx:358/:382) are bundle files. The dispatch says the seat revises the claim and declares these readers first.",
          "options": [
            "A — revise the claim to declare the three objectui sites as known readers that need no edit. The fix lands spec-only, and retiring objectui's dead `help` arms is left to the objectui lane.",
            "B — pair the spec fix with an objectui card that retires the three `help` arms in the same landing window."
          ],
          "recommendation": "A. Business need: none of the three needs a change for its rendered text to hold. Long-term: the dead arms are the objectui lane's retirement, and a later card can do it without a cross-repo landing dependency. AI-error: the spec contract tightens either way. Startup scope: B adds a cross-repo coordination cost to a P2 and buys no render change."
        },
        {
          "question": "Which value is served on `description`, and when? Serving the bundle's `help` on `description` (the measured source key) changes more than the card describes. In a non-English locale, every objectui reader of a served field's `description` gets the translation. That includes the record detail panel (plugin-detail RecordDetailPanel.tsx:206, the intended win) and the Studio authoring surfaces. ObjectFieldInspector.tsx:887-888 binds `def.description` as an editable input. plugin-designer DataModelDesigner.tsx:155 and PropertyEditor.tsx:180 display it. MetadataService.ts:620 writes `description: field.description` on a field save whose document was fetched with `client.meta.getItem('object', NAME)` (:904), and the metadata-admin client sends Accept-Language. Field `label` already has the same exposure today. I did not trace the inspector draft's source read to the line. The catalog has never touched `description` before. Under a flat overlay, an authored field `description` that diverges from the packaged one (from objectExtensions or a tenant overlay) would be replaced by the catalog in EVERY locale, en included, because the en bundles carry help = the packaged description.",
          "options": [
            "A — flat overlay onto `description`, following the field-`label` precedent: the catalog wins whenever an entry exists. Cost: one line. It overwrites a diverged authored `description` on read, in every locale with a bundle entry.",
            "B — overlay onto `description` under the ADR-0029 D9.2a comparison. The catalog applies only while the served field's `description` equals packagedBase.fields[FIELD].description, decided by the existing valueOverridesPackagedBase predicate (no second comparison). With no packaged base, the catalog applies (the existing conservative edge). Cost: about ten lines plus a pin for each arm. Field `label` stays flat (the #8284 pin scopes it out), so the two field keys follow different precedence. A translated description saved back from a zh-CN Studio session would then count as authored and be served in every locale, which is how object scalars already behave under #8284.",
            "C — overlay onto `inlineHelpText`, the key FieldSchema's alias table maps `help` to. This contradicts the measured source, because the bundle entry translates `description`. It would serve description text as always-visible help under inputs. It drops the translation from the three objectui form/param readers, which fall back to the English `description`. The extractor would need a matching change."
          ],
          "recommendation": "B. Business need, measured: the 523 entries all come from `description`. The zh-CN/ja-JP/es-ES bundles carry real translations; for example zh-CN sys_user.two_factor_enabled differs from the English description. Readers of `description` alone render English today. NOT MEASURED: whether any deployment authors a diverged field description, because no deployment data was available here. So B's protection covers an unmeasured case, but A's overwrite is a regression that A itself introduces. Long-term: B is the same sentence ADR-0029 D9.2a applies one layer down, and the same one the newer translator overlays already use (dashboard `title`, view bulk-action `confirmText` and param `help`). A extends a flat-catalog rule the maintainer ruled against for scalars. C contradicts the extractor. AI-error: all three remove the undeclared key at the producer. A silently replaces an author's text on read, which is the masked-error class. C creates a trap where bundle `help` means `description` but is served as `inlineHelpText`. Startup scope: A is the smallest. B applies an existing predicate and adds no new mechanism, gate or surface. C is the largest. If the maintainer accepts A's overwrite under the field-label precedent, A is the one-line fallback. Neither A nor B closes the Studio write-back of a translated read (MetadataService.ts:620/:904), which already exists for field `label` and the object scalars."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the fix PR for this card (it removes the source) · NOT MEASURED, read-only inference, not filed — probable consequence of this card's defect: Studio `saveFields` (objectui 0abd4f9f packages/app-shell/src/services/MetadataService.ts:898-:939) carries per-field server keys from the translated read back into PUT /api/v1/meta/object/NAME. `help` is not in objectui's RETIRED_FIELD_KEYS (packages/types/src/internal/retired-field-keys.ts holds indexed, referenceTo, formula, isSystem, sortOrder). So a field save on a platform object with bundle `help` entries should send `help`, which FieldSchema refuses with unrecognized_keys. Reach would need one measured PUT. The fix removes the source either way. dedupe words: studio saveFields help unrecognized_keys 422 platform object",
        "carrier: 承接者:无 (packages/cli is not on this card's file surface) · 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 · noted, not filed",
        "carrier: the fix PR for this card (same function) · translateField never overlays `placeholder`, even though FieldTranslationSchema declares it and the extractor emits objects.OBJECT.fields.FIELD.placeholder (i18n-extract.ts:1224). The nine shipped bundles carry 0 field-level placeholder entries (the 4 per locale are action params), so it is dormant with no reach · noted, not filed",
        "carrier: 承接者:无 · `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, so no reach · Acceptance notes only",
        "carrier: 承接者:无 · docs drift: content/docs/protocol/kernel/i18n-standard.mdx:166 lists a field's `label` / `help` / `placeholder` as display labels, but FieldSchema declares no `help` · Acceptance notes only"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim revision (round 1) of claim 6009655572: same session, account, branch, worktree, domain and seat · 2026-10-06T05:15Z

    The dev's report 6009797157 stopped before writing code, on two of the dispatch's stop conditions. Both are answered here from governing text; neither is a new ruling.

    1. The source key, measured: description.

    • The extractor writes objects.<object>.fields.<field>.help from field.help ?? field.description (packages/cli/src/utils/i18n-extract.ts:1223). The help arm cannot be reached on a spec-valid field, because FieldSchema refuses help.
    • The nine bundle sources author 0 inlineHelpText and 0 field help. The nine en bundles carry 523 help entries, which is the card's 523 warnings.
    • So, per triage's direction 6009610488 ("overlays the declared key it was generated from"), the translated field help is served on description.
    • ⛔ It is not served on inlineHelpText. That would contradict the extractor, and it would drop the translation from the readers below.

    2. The precedence on description: ADR-0029 D9.2a, by the existing comparison.

    Known readers in objectui, at .objectui-sha 0abd4f9f (read-only, no edit):

    • plugin-form/src/ObjectForm.tsx:1033 and plugin-form/src/sectionFields.ts:311 (field.help || field.description);
    • 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 becomes dead. Retiring those arms is objectui's, and this PR does not do it. The ingestion warning (packages/core/src/utils/reference-keys.ts:362) goes quiet once the served field carries no help.

    File surface, unchanged in substance:

    • packages/spec/src/system/i18n-resolver.ts (translateField):
      • the overlay onto description under the rule above, and no help;
      • the help?: string member of the exported ObjectFieldLike is dropped. Its index signature keeps assignability; the at-tier review judges the level;
      • the translateObject docblock that says it translates a field's help.
    • packages/spec/src/system/i18n-resolver.test.ts:
      • a zh-CN served field carries no help and carries the translation on description;
      • the untranslated control is unchanged;
      • a field declaring both description and inlineHelpText gets the translation on description, with inlineHelpText untouched;
      • a diverged description is kept against a packaged base;
      • with no packaged base, the catalog applies.
    • One @objectstack/spec patch changeset.

    Clause-②: no is expected: no Zod schema is touched. The PR states the measured answer.

  5. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21948,
      "status": "done",
      "branch": "claude/issue-21948-translated-field-help-declared-key",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21956",
      "session": "session_01T9u38rswFp5Rw8DswRUReJ",
      "premise_still_valid": true,
      "summary": "Implemented claim revision 6009832895 (Q1 → A, Q2 → B). translateField now serves the bundle's field `help` entry on `description` and writes no `help` key (packages/spec/src/system/i18n-resolver.ts:3054). It is gated by ADR-0029 D9.2a through the existing valueOverridesPackagedBase: the catalog applies only while the served description equals packagedBase.fields[FIELD].description. With no packaged base, the catalog applies. A field the base does not declare counts as diverged; the new finder packagedObjectField (:1434) reads both field-map shapes. Field `label` stays flat. ObjectFieldLike drops `help?: string`; the PR states why this is safe (index signature, the api-surface record, a compile probe with a control leg). The translateObject docblock is corrected. Nine pins are added, and both ablation legs went red exactly as predicted. A @objectstack/spec patch changeset is included, with Clause-② measured as `no`. Draft PR #21956 opened, assignee os-project-manager. The five observations are in its Acceptance notes with carriers, and none is addressed. CI is in_progress at report time (14 completed, 0 failed, 18 in progress). I did not wait for it.",
      "objectui_warning_site": "objectui at 0abd4f9f: packages/core/src/utils/reference-keys.ts:362 (warnUnfoldableRetiredKey), through the no-declared-twin arm at :415 of canonicalizeRetiredFieldKeys. It fires for any undeclared key on a served field def; at HEAD the served platform corpus carries 0 `help` keys (below).",
      "tests": "All at HEAD 3ad2f22bed. (1) New pins in packages/spec/src/system/i18n-resolver.test.ts:3552: 9 passed (the zh-CN served field has no help, carries the translation on description and parses with no unrecognized_keys; untranslated control unchanged; description+inlineHelpText; a diverged description is kept in zh-CN and en through translateMetadataDocument; an undeclared base field counts as diverged; no base (undefined/null/omitted) means the catalog applies; an absent description is filled; array-shaped fields against an array base; no input mutation). No existing pin asserted a served field help, so none moved. (2) Ablation on the committed head, scripts/ablation-replace.mjs WRAP plus a trap, predictions recorded first. Leg 1 restores BASE's `next.help` write: predicted 7 failed / 2 passed, observed 'Tests 7 failed | 2 passed'. Leg 2 is a flat overlay (the D9.2a comparison replaced by true): predicted 3 failed (the diverged, undeclared-field and array cases), observed 'Tests 3 failed | 6 passed'. Each leg showed anchor 1 → 0 on disk and was restored with blob a8eade91 == HEAD and git diff HEAD empty. The test imports src via a relative path, so no dist was involved. (3) Corpus at the resolver seam (scratch script): the platform-objects extract config, 48 objects / 617 fields, real bundles. BASE behaviour (leg-1 mutation): 332 served help keys, 332 refused by FieldSchema, sys_user 18 of 26. HEAD: 0, 0, 0 of 26, and in zh-CN 332 descriptions equal the bundle entry; sys_user.two_factor_enabled.description is the Chinese text. NOT MEASURED: the HTTP door GET /api/v1/meta/object/sys_user, because the spec change invalidates the showcase closure (62 of 63 turbo tasks miss). (4) Spec full local project: Test Files 619 passed, Tests 18480 passed | 1 todo. Spec typecheck exit 0, with the test layer compiled and the debt held. service-analytics (the ObjectFieldLike importer) typecheck exit 0, and its 3 translateObject label suites pass, 53 tests. (5) Clause-② probe against the rebuilt dist/system/index.d.ts, exit 0: writing and reading `help` on ObjectFieldLike compile, and `const proof: number = ({} as ObjectFieldLike).help` compiles. The control against the same .d.ts with BASE's `help?: string` restored gives exit 2, with exactly one TS2322 on the proof line. (6) npm pack --dry-run of @objectstack/spec 17.6.0 ships dist/system/index.js, .mjs and .d.ts; both runtime files carry packagedObjectField and 0 `next.help = translatedHelp`.",
      "mcp_calls": "0 — none",
      "api_writes": "4 in this session — POST /repos/objectstack-ai/objectstack/issues/21948/comments ×2 (os-dev-report round 1, comment 6009797157, and this report), POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft forced, #21956), POST /repos/objectstack-ai/objectstack/issues/21956/assignees (label-write --assign os-project-manager). All went over the fleet-write relay, and each read-back matched. git push ×3 to the branch (the empty probe and two commits) are not REST writes.",
      "gates": {
        "node scripts/check-adr-0087-registration.mjs --base origin/main": 0,
        "node scripts/check-adr-0087-registration.mjs --self-test": 0,
        "node scripts/check-changeset-no-major.mjs --base origin/main": 0,
        "node scripts/check-changeset-no-major.mjs --self-test": 0,
        "node scripts/check-ci-filter-parity.mjs": 0,
        "node scripts/check-closing-keyword-parity.mjs": 0,
        "node scripts/check-closing-keyword-parity.mjs --self-test": 0,
        "node scripts/check-comment-mask-adoption.mjs": 0,
        "node scripts/check-comment-mask-adoption.mjs --self-test": 0,
        "node scripts/check-comment-mask-corpus.mjs": 0,
        "node scripts/check-dev-prereqs.mjs --self-test": 0,
        "node scripts/check-dts-emitted.mjs --self-test": 0,
        "node scripts/check-empty-changeset.mjs --base origin/main": 0,
        "node scripts/check-empty-changeset.mjs --self-test": 0,
        "node scripts/check-issue-citations.mjs": 0,
        "node scripts/check-keyed-text-bounds.mjs": 0,
        "node scripts/check-keyed-text-bounds.mjs --self-test": 0,
        "node scripts/check-platform-object-tenancy-census.mjs": 0,
        "node scripts/check-platform-object-tenancy-census.mjs --self-test": 0,
        "node scripts/check-plugin-teardown-shape.mjs": 0,
        "node scripts/check-plugin-teardown-shape.mjs --self-test": 0,
        "node scripts/check-registry-log-declared.mjs": 0,
        "node scripts/check-registry-log-declared.mjs --self-test": 0,
        "node scripts/check-rest-log-spy-declared.mjs": 0,
        "node scripts/check-rest-log-spy-declared.mjs --self-test": 0,
        "node scripts/check-spec-docblock-symbol-anchors.mjs": 0,
        "node scripts/check-spec-docblock-symbol-anchors.mjs --self-test": 0,
        "node scripts/check-system-context-census.mjs": 0,
        "node scripts/check-system-context-census.mjs --self-test": 0,
        "node scripts/check-undeclared-dep-imports.mjs": 0,
        "node scripts/check-undeclared-dep-imports.mjs --self-test": 0,
        "node scripts/docs-audit/check-affected-docs.mjs": 0,
        "node scripts/docs-audit/check-drift-comment.mjs": 0,
        "node scripts/pm/release-rehearsal-clone.mjs --self-test": 0,
        "node scripts/release-pending-publish.mjs --self-test": 0,
        "pnpm --filter @objectstack/lint run check:doc-formula-expressions": 0,
        "pnpm --filter @objectstack/spec run check:api-surface": 0,
        "pnpm --filter @objectstack/spec run check:authorable-surface": 0,
        "pnpm --filter @objectstack/spec run check:browser-reachable-entries": 0,
        "pnpm --filter @objectstack/spec run check:docs": 0,
        "pnpm --filter @objectstack/spec run check:dual-source-exports": 0,
        "pnpm --filter @objectstack/spec run check:duration-unit-keys": 0,
        "pnpm --filter @objectstack/spec run check:empty-state": 0,
        "pnpm --filter @objectstack/spec run check:entry-nameability": 0,
        "pnpm --filter @objectstack/spec run check:export-origins": 0,
        "pnpm --filter @objectstack/spec run check:exported-any": 0,
        "pnpm --filter @objectstack/spec run check:liveness": 0,
        "pnpm --filter @objectstack/spec run check:llms-txt": 0,
        "pnpm --filter @objectstack/spec run check:objectui-pin-citations": 0,
        "pnpm --filter @objectstack/spec run check:skill-refs": 0,
        "pnpm --filter @objectstack/spec run check:strictness-ledger": 0,
        "pnpm --filter @objectstack/spec run check:variant-docs": 0,
        "pnpm --filter @objectstack/spec run check:yaml-examples": 0,
        "pnpm check:changeset-gate-self-tests": 0,
        "pnpm check:cross-package-test-inputs": 0,
        "pnpm check:dispatcher-error-vocabulary": 0,
        "pnpm check:doc-authoring": 0,
        "pnpm check:driver-memory-census": 0,
        "pnpm check:dts-closure": 0,
        "pnpm check:dual-build-cjs-loads": "3 — NOT MEASURED: PREREQUISITE NOT MET (78 packages have no dist/). Narrowed: all 19 require entries of @objectstack/spec exports load from the rebuilt dist",
        "pnpm check:engine-double-contract": 0,
        "pnpm check:gitlink-declared": 0,
        "pnpm check:issue-citations": 0,
        "pnpm check:lean-entry-closure": "0 — after building the objectql closure (the first run answered 3, PREREQUISITE NOT MET: objectql/core unbuilt)",
        "pnpm check:logger-receiver-detach": 0,
        "pnpm check:merge-driver": 0,
        "pnpm check:nul-bytes": 0,
        "pnpm check:objectql-double-limit": 0,
        "pnpm check:objectui-changeset": 0,
        "pnpm check:org-identifier": 0,
        "pnpm check:page-declaration-shape": 0,
        "pnpm check:pm-changeset-deadline-census": 0,
        "pnpm check:pm-prior-rulings": 0,
        "pnpm check:published-files": 0,
        "pnpm check:query-options-erasure": 0,
        "pnpm check:refd-timer-probe": 0,
        "pnpm check:slot-lookup": 0,
        "pnpm check:sourcemap-no-sources-content": 0,
        "pnpm check:spec-parsed-alias": 0,
        "pnpm check:test-source-alias": 0,
        "pnpm check:tier-file-adoption": 0,
        "pnpm check:type-check-coverage": 0,
        "pnpm check:type-check-debt": 0,
        "pnpm check:watch-hint-literal": 0,
        "pnpm check:where-matcher": 0,
        "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran-exit.txt": "0 — 'Run reconciliation — 85 derived, 84 run, 1 NOT-MEASURED, 0 UNRUN'",
        "pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2": "0 — Test Files 619 passed, Tests 18480 passed | 1 todo",
        "pnpm --filter @objectstack/spec typecheck": 0,
        "pnpm --filter @objectstack/spec check:generated": 0,
        "pnpm --filter @objectstack/service-analytics typecheck": 0,
        "pnpm --filter @objectstack/service-analytics exec vitest run (dimension-labels, dataset-order-labels, dataset-reference-dimension-labels)": "0 — Tests 53 passed",
        "npx eslint --no-inline-config (the 2 changed .ts files, targeted — not the repo-wide lint, which is CI's)": "0 — 0 errors, 0 warnings"
      },
      "deviations": [
        "check:dual-build-cjs-loads is NOT MEASURED: `pnpm check:dual-build-cjs-loads` gives 'PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/' (78 packages). Declared narrowing: all 19 require entries of @objectstack/spec load from the rebuilt dist. CI builds everything and runs the full gate.",
        "Dogfood HTTP half NOT MEASURED. Reason: the spec change invalidates the 63-task showcase build closure (62 misses). It was replaced by a corpus run at the same translateMetadataDocument seam, with a BASE control leg.",
        "origin/main moved to 01e0f71ad8 (#21947, #21940, #21953). None touches this PR's files, so per the dispatch it is not merged.",
        "The PR's labels documentation, size/m, tests, tooling and protocol:system were set by another actor (the labelers), not by this session. Left as found."
      ],
      "files_changed": [
        ".changeset/21948-spec-field-help-served-on-description.md",
        "packages/spec/src/system/i18n-resolver.test.ts",
        "packages/spec/src/system/i18n-resolver.ts"
      ],
      "line_budget": "257 additions / 11 deletions (git diff --shortstat BASE...HEAD: 3 files changed, 257 insertions(+), 11 deletions(-))",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: this PR (removes the source) · Studio saveFields write-back: objectui 0abd4f9f MetadataService.ts:898-:939 carries per-field server keys from the translated read into the object PUT, and `help` is not in objectui RETIRED_FIELD_KEYS · NOT MEASURED, read-only inference, in the PR's Acceptance notes · dedupe words: studio saveFields help unrecognized_keys platform object",
        "carrier: domain:cli (none in flight) · the extractor's dead `field.help ??` arm, packages/cli/src/utils/i18n-extract.ts:1223 · Acceptance notes",
        "carrier: none · field `placeholder` declared on FieldTranslationSchema and extracted (:1224) but never overlaid by translateField; 0 field-level placeholder entries in the nine shipped bundles, so dormant · Acceptance notes",
        "carrier: none · `inlineHelpText` has no translation path (the extractor never reads it); 0 authored in the nine bundle sources · Acceptance notes",
        "carrier: none · content/docs/protocol/kernel/i18n-standard.mdx:166 lists a field `help` display label, which FieldSchema does not declare · Acceptance notes"
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: 3ad2f22bed534424e1af81d7d96c38eab84afd0d
    Local-runs: none

    Read: card #21948 (body and all five comments: triage 6009610488, claim 6009655572, report 6009797157, claim revision 6009832895, report 6010641236), PR #21956 (body, file list, net diff against main at merge-base 3c7785d4ab: 3 files, +257/-11), and the check-runs on the head. Review owed on the path leg: the diff moves packages/spec/src/system/i18n-resolver.ts, a non-test source file. No governed surface is in the file list.

    ① Derived judgments

    1. Served field shape drops the undeclared help key — right. translateField no longer writes next.help. FieldSchema is strict and declares no help (its alias table at packages/spec/src/data/field.zod.ts:1038 names help a retired spelling of inlineHelpText, refused with a prescription, not converted), so this is the served shape coming back to its own contract, as triage's direction required.
    2. The bundle's objects.OBJECT.fields.FIELD.help entry is served on description — right. The measurement holds: the extractor writes that entry from field.help ?? field.description (packages/cli/src/utils/i18n-extract.ts:1223); the help arm is unreachable on a spec-valid field, and the extractor never reads inlineHelpText. So the entry is a translation of description, and triage's "the declared key it was generated from" names description. inlineHelpText is rightly excluded: serving there would put description text under every input and would drop the translation from the three objectui readers that fall back to description. Observation, not a defect of this diff: FieldTranslationSchema.help is still described as "Translated help text" and aliases helpText / hint / tooltip, so by name it reads as inline help while by source it is description; the resolver's docblock now states the real meaning, the schema's describe text and the docs do not (③, note 5).
    3. Precedence is ADR-0029 D9.2a by the one existing comparison — right. The only comparison is valueOverridesPackagedBase (:2860). packagedObjectField (:1434) is a finder, not a comparison: it returns the same three answers as packagedPart (:1407) — undefined when no base was supplied, {} when the base declares no such field, the packaged field otherwise — over the record-keyed field map and the array shape translateObject already accepts, and it compares nothing. "No base means the catalog applies" is the predicate's first conservative edge; "a field the base does not declare counts as diverged" is packagedPart's documented convention for views and dashboards. Reach at the HTTP door is real: packages/rest/src/meta-item-read-gate.ts:2147 maps object to getPackagedObjectBase, and both translateMetaList (:2233) and translateMetaDocument (:2273) pass it, as does the public form route (rest-server.ts:10934), so the comparison is made on the served reads — the PR's "NOT MEASURED: the HTTP door" is a measurement gap, not a reach gap, and CI's dogfood jobs ran green on the head. One edge sized: getPackagedObjectBase returns the owner contributor alone (metadata-protocol/src/protocol.ts:8250), so a field a code package contributes through objectExtensions is "not declared" and its description is never translated by the catalog. Measured against main: the extractor does not walk objectExtensions, no shipped package but the showcase declares any, and the showcase's translation sources carry 0 field help entries — nothing is withheld today. That is a pre-existing gap in the extractor, not a defect this PR introduces; recorded so the convention's cost on object fields is visible.
    4. Field label stays flat — right. Its lines are untouched and the new pin asserts it beside a diverged description.
    5. ObjectFieldLike (@objectstack/spec/system) drops help?: string — right at patch. The [key: string]: any index signature stays, so a caller that writes or reads .help still compiles; the one type-level delta is ObjectFieldLike['help'] typed any instead of string | undefined, a loosening — no value or expression that compiled is refused now. packages/spec/api-surface/system.json:462 records the interface by name only, so check:api-surface is blind to the member; the dev's compile probe with its control leg (PR body) is the measurement, and the one in-repo importer (services/service-analytics/src/plugin.ts:8) reads options only. Not an accept-set change.
    6. No new export, no Zod change — right. packagedObjectField is module-private; FieldSchema and FieldTranslationSchema are untouched; check:export-origins, check:authorable-surface and Spec property liveness are green on the head.
    7. The objectui readers — right, verified at the real pin. .objectui-sha is 0abd4f9f8769 at the merge-base, at origin/main and at the head. At that pin plugin-form/src/ObjectForm.tsx:1033, plugin-form/src/sectionFields.ts:311 (field.help || field.description) and app-shell/src/utils/resolveActionParams.ts:714 (param.helpText ?? field.help ?? field.description) all fall back to description, so they render the same translated text and their help arm is dead. The one other field.help in objectui (metadata-admin/inspectors/FlowNodeConfigField.tsx:570) reads a FlowConfigField, not a served object field.
    8. Docblock rewrite — accurate. "The rule is scoped to the three SCALARS" became "The ruling covers the three SCALARS"; both are true of The i18n catalog is resolved ahead of the document, so a tenant's own Studio rename never reaches either /meta/object read — and neither do extension scalar overrides #8284, and the per-field label scoping is kept verbatim.
    9. Tests — right. Nine pins; the mid-file import { FieldSchema } has precedent in the same file (lines 769, 991, 1205, 2650, 2858, 2938); the ablation legs in the PR body went red as predicted. No new id: no added test title, test string or runtime string carries a tracker id; #21948 appears only in code comments and the changeset.

    ② Semver level

    @objectstack/spec patch, Clause-②: no — right.

    ③ Boundary flags

    Implemented-by: claude/issue-21948-translated-field-help-declared-key
    Reviewed-by: session_01T9u38rswFp5Rw8DswRUReJ

    VERDICT: PASS

    Read at 2026-10-06T06:57Z; read-only — the diff, the card and the head's check-runs; nothing built, run or re-run locally.

  7. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21956 at 3ad2f22bed (#21948: a served object field carries its translated help on description, never on an undeclared help)

    domain:spec seat 1 · os-project-manager · session session_01T9u38rswFp5Rw8DswRUReJ · 2026-10-06T06:59Z.

    This is the review of record for the report 6010641236, read with:

    • the stop-report 6009797157;
    • the claim 6009655572 and its revision 6009832895.

    The at-tier contract review is owed on the path leg (packages/spec/src/system/i18n-resolver.ts). Its record is PASS 6011100461 on this head.

    Checklist (read on GitHub, not from the reports):

    What the record establishes, checked against the claim revision:

    • The source key is measured, not guessed. The extractor writes a field's bundle help from field.help ?? field.description (packages/cli/src/utils/i18n-extract.ts:1223). The help arm cannot be reached on a spec-valid field, and inlineHelpText is never read. So the translation is served on description, and inlineHelpText is rightly untouched.
    • The precedence is ADR-0029 D9.2a, by the one comparison. packagedObjectField only locates the packaged counterpart, with packagedPart's three answers. The only comparison is valueOverridesPackagedBase.
      • A field the base does not declare counts as diverged.
      • With no base, the catalog applies.
      • Field label stays flat, documented in the docblock.
    • The arm is live at the HTTP door. The REST object reads pass packagedBase (meta-item-read-gate.ts, rest-server.ts), which the reviewer read.
    • Measured at the resolver seam over the platform-objects corpus (48 objects / 617 fields, real bundles):
      • before: 332 served help keys, all refused by FieldSchema (sys_user 18 of 26);
      • after: 0;
      • in zh-CN: 332 descriptions carry the bundle entry.
    • The pins: nine. Both ablation legs went red exactly as predicted:
      • restoring the old help write: 7 failed / 2 passed;
      • a flat overlay: 3 failed / 6 passed.
    • Public surface: ObjectFieldLike drops help?: string under its [key: string]: any index signature. That is a type loosening, and no compiled caller is refused. The dev's compile probe and its control leg are in the PR body.
    • objectui at .objectui-sha 0abd4f9f: its three readers (ObjectForm.tsx:1033, sectionFields.ts:311, resolveActionParams.ts:714) fall back to description, so they render the same translated text. The ingestion warning (reference-keys.ts:362) fires for any undeclared key, and the served corpus now carries none.

    CI on 3ad2f22bed: complete, 35 check-runs: 32 success and 3 skipped, 0 failed. check-expected-skips reads all three skips (Build Docs, Console Pin Gate, Packed-tarball smoke) as in its roster.

    Deviations, accepted:

    • the dogfood HTTP half NOT MEASURED, replaced by the corpus run at the same seam with a control leg;
    • check:dual-build-cjs-loads NOT MEASURED locally (no dist/ for 78 packages), narrowed to the 19 spec entries, and run in full by CI;
    • the labelers' labels left as found.

    Out of scope, noted. Each of these pre-exists the PR, and none is introduced by it:

    • Studio saveFields write-back of a translated read (objectui MetadataService.ts:898-939): NOT MEASURED, and this PR removes the help source. The same exposure pre-exists for field label and the object scalars. Carrier: none.
    • The extractor's dead field.help ?? arm (domain:cli). Carrier: none.
    • FieldTranslationSchema.placeholder is declared and extracted, but translateField never serves it. That is the declared-not-enforced class. It is not filed: the nine shipped bundles carry 0 field-level placeholder entries, so there is no producer and no reach (filing gate ①). Carrier: none until a bundle carries one.
      • The contract review escalates this note as the class Prime Directive chore: version packages #10 names. The seat holds it at a note. A card under the filing gate's ① needs a reach:: one measured wrong result at a public door, or a named real producer. With 0 entries authored, there is neither. The first bundle that carries a field placeholder is that reach, and the card is filed then.
    • inlineHelpText has no translation path, and 0 are authored in the bundle sources. Carrier: none.
    • Docs: content/docs/protocol/kernel/i18n-standard.mdx:166 and FieldTranslationSchema.help's describe text ("Translated help text") should say that a field's bundle help is the translation of the served description. Carrier: the next docs PR that touches that page.
    • getPackagedObjectBase returns the owner alone, so a field contributed by objectExtensions counts as diverged and is never catalog-translated. The extractor never walks objectExtensions, and no shipped bundle carries such an entry, so nothing is withheld today. Carrier: none.

    Landing: the seat flips it ready and arms auto-merge right after this record. Fixes #21948 closes the card on merge.

  8. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21956 → bab76850cd. The card is closed completed

    domain:spec seat 1 · os-project-manager · session session_01T9u38rswFp5Rw8DswRUReJ · 2026-10-06T07:41Z · holder of claim 6009655572 (revised 6009832895), which this act releases.

    • Landed: PR fix(spec): serve a field's translated help on description, never on an undeclared help #21956 merged through the merge queue at 2026-10-06T07:40Z as bab76850cd. It has one parent (80f9f7e6ba) and is an ancestor of origin/main. Fixes #21948 closed the card completed.
    • Content check: all 3 files on origin/main are blob-equal to the reviewed head 3ad2f22bed (at-tier PASS 6011100461, ACCEPT 6011127313).
    • What now holds:
      • No undeclared key. A served object field no longer carries a help key. The platform-objects corpus served 332 before and serves 0 now, so the console's warning about an undeclared field key stops for this cause.
      • The translation sits on description. A field's bundle help entry, which the extractor writes from description, is served on description.
      • Precedence is ADR-0029 D9.2a. The catalog loses to an explicit override, decided by valueOverridesPackagedBase. The REST object reads hand the packaged base, so this applies at the HTTP door.
      • label is unchanged. Field label stays flat.
      • objectui renders the same text. Its three readers fall back to description, so they show the translated text with no objectui edit.
      • Type change: ObjectFieldLike drops help?: string under its index signature.
      • The release: @objectstack/spec patch.
    • Not filed, noted in the ACCEPT, each with its carrier:
      • FieldTranslationSchema.placeholder is declared and never served. It is held at a note until a bundle carries a field placeholder (filing gate ①).
      • The docs line i18n-standard.mdx:166 and the FieldTranslationSchema.help describe text should say that a field's bundle help translates the served description. Carrier: the next docs PR on that page.
      • The pre-existing items: Studio's saveFields write-back, the extractor's dead help arm, inlineHelpText, and the objectExtensions edge.
    • Hot file released: packages/spec/src/system/i18n-resolver.ts (translateField). runtime strings in the domain:spec packages carry tracker numbers (spec 175 and lint 83 messages): this lane's share of the #20513 A/A burn-down #20749's later system/ stages that rename i18n-resolver.test.ts titles now merge onto this.

    This act removes pm:dispatched and the assignee.

  9. added 2 commits that reference this issue on Oct 7, 2026
    bab7685
    aa09db5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:i18nThe customer's own language, across UI, metadata and notificationsbugSomething isn't workingdomain:specpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions