Skip to content

docs(spec): stop the translation @example blocks teaching an unresolvable messages id - #18553

Merged
os-bill merged 1 commit into
mainfrom
claude/issue-18190-messages-example-dotted-id
Sep 17, 2026
Merged

os-bill merged 1 commit into
mainfrom
claude/issue-18190-messages-example-dotted-id

Conversation

@os-bill

@os-bill os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #18190

Clause-②: no

The two docblock @example blocks on TranslationDataSchema and TranslationItemSchema demonstrated a messages id that is writable, schema-valid and resolves to nothing. They now author the single-segment spelling, and both docblocks state the rule.

Scope — the doorway half only

The card names two independent halves and says in terms that one does not imply the other. This PR takes half 1 (fix the doorway) and deliberately leaves half 2 (narrow the schema) alone: a single-segment constraint on messages keys narrows a published accept set, which the card itself marks as a Clause-②: yes shape owing the contract-review path. Nothing here narrows, retires or refuses anything — the accept set is byte-identical to main.

The mechanism, measured first-hand at BOTH implementations

The card's load-bearing leg was relayed, not measured by this seat, and it demanded both t() implementations. Triage had located one. Both are now read and run:

implementation walk t()
packages/core/src/fallbacks/memory-i18n.ts resolveKey() at :106key.split('.'), segment-by-segment :152, and again on the declared-fallback leg
packages/services/service-i18n/src/file-i18n-adapter.ts a second resolveKey() at :27 — same body :142, through resolveFromLocale() at :277

They agree. Same split, same walk, same undefined at the first missing hop, same "return the key itself" on a miss. So the bigger finding the card warned about — one resolving a dotted id while the other does not — does not exist. Nothing is absorbed into this PR on that account.

messages is declared a flat record of string to string (translation.zod.ts:670), so a dotted id is one key that merely contains a dot. t('messages.common.save', …) therefore looks for a nested common object under messages, finds a string or nothing at the first hop, and returns the key string.

Acceptance item 2 — verified by running it, not by eye

A one-time measurement drove the exact authored payload of each repaired @example through both real t() implementations, loaded through their published entry points (built dist/), via the real authored-translation path that strips the item envelope (readAuthoredTranslationLayer's destructure) and hands the groups to replaceAuthoredTranslations:

== leg 0: both payloads are SCHEMA-VALID (nothing is narrowed by this PR) ==
  PASS  TranslationItemSchema.safeParse(ITEM_BEFORE).success   got: true
  PASS  TranslationItemSchema.safeParse(ITEM_AFTER).success    got: true
  PASS  TranslationDataSchema.safeParse(DATA_BEFORE).success   got: true
  PASS  TranslationDataSchema.safeParse(DATA_AFTER).success    got: true

== BEFORE (the id the @example used to teach) — authored layer messages keys: ["common.save"] ==
  PASS  memory-i18n       t('messages.common.save', 'zh-CN')          got: "messages.common.save"
  PASS  memory-i18n       FIRING CONTROL t('objects.account.label')   got: "客户"
  PASS  file-i18n-adapter t('messages.common.save', 'zh-CN')          got: "messages.common.save"
  PASS  file-i18n-adapter FIRING CONTROL t('objects.account.label')   got: "客户"

== AFTER (the id the @example teaches now) — authored layer messages keys: ["commonSave"] ==
  PASS  memory-i18n       t('messages.commonSave', 'zh-CN')           got: "保存"
  PASS  memory-i18n       FIRING CONTROL t('objects.account.label')   got: "客户"
  PASS  file-i18n-adapter t('messages.commonSave', 'zh-CN')           got: "保存"
  PASS  file-i18n-adapter FIRING CONTROL t('objects.account.label')   got: "客户"

== the JSON @example, through loadTranslations (the file-authored bundle door) ==
  PASS  BEFORE memory-i18n       t('messages.common.save', 'en')  got: "messages.common.save"
  PASS  BEFORE file-i18n-adapter t('messages.common.save', 'en')  got: "messages.common.save"
  PASS  AFTER  memory-i18n       t('messages.commonSave', 'en')   got: "Save"
  PASS  AFTER  file-i18n-adapter t('messages.commonSave', 'en')   got: "Save"

VERDICT: ALL LEGS AS PREDICTED          (exit 0)

Leg 0 is the part that keeps this PR honest about its scope: the old payload still parses. Nothing is refused that was accepted before.

The old id is the control that fires: it returns the key string at both implementations, which is what "resolves to nothing" looks like from a caller. The new id returns the translation at both.

What is NOT owed

  • No content/docs/references/** regeneration. Those pages are generated from .describe() text (property tables) and carry no @example block — verified by check:generated, which reports the docs artifact up to date on this diff. No .describe() string moved.
  • No new permanent test. The deliverable is docblock text; the acceptance item is a measurement, cited above, and a permanent pin would be a cross-package test reading spec source — named in the notes below rather than added here.

Acceptance notes (out of scope, not filed)

  • packages/spec/src/conversions/registry.ts (the translation-validation-messages-removed fixture) and packages/qa/downstream-contract/src/additional-domains.fixtures.ts both carry 'common.save' as inert fixture data. Neither is a doorway: conversion fixtures do not render into the upgrade guide or any docs page (checked), and the key's role in both is "the neighbour that survives the strip". Noted, not filed. Successor: whoever next edits those fixtures.
  • packages/spec/src/system/translation.test.ts pins 'common.save' as a key the record accepts. That is a true statement about the schema and this PR does not change it, so the fixtures stay. Successor: the card that decides half 2, which is the PR that would make those assertions false.
  • packages/lint/src/lint-liveness-properties.test.ts carries the same fixture key. Out of bounds for this card (held by lint: translation-target-unknown reads apps[].navigation only, so every locale key for a CONTRIBUTED navigation item (navigationContributions) is a false positive whose advice deletes a translation the runtime honours #18203) and inert there for the same reason. Noted, not filed.
  • A permanent pin asserting the shipped @example ids resolve would need a test outside packages/spec reading spec source — a cross-package test input with its own gate discipline. Noted, not filed; successor: none today.

Filed separately (3-class finding, in the report for the seat to file)

examples/app-todo/src/translations/{en,zh-CN,ja-JP}.ts author twelve dotted messages ids each ('common.save', 'common.cancel', …). That is the same class as this card — authorable, schema-valid, resolves to nothing — but in a shipped example app rather than a docblock, so it is a separate surface and a separate card. Not folded in.

Tests

All figures below were taken at 9620486d10, this branch's final commit; origin/main has not moved since the branch point (79a046f8cd), so no merge is owed.

  • Derived gate rosternode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 78 families from the actual change set (2 paths). All 78 were run and reconciled with --ran, each line carrying its exit code: "78 derived famil(ies) accounted for — 78 run, 0 NOT-MEASURED (a DERIVED zero — all 78 recorded an exit code and none of them is 3)".
  • pnpm check:i18n-walk-parity — the family this card's surface makes load-bearing: exit 0, "11 declared group(s), 8 walked, 3 exempted — every declared group has an extractor face".
  • pnpm --filter @objectstack/spec check:generated — exit 0, "All 15 generated artifacts are up to date", check:docs (content/docs/references) among them. This is the evidence that no generated page is owed.
  • pnpm --filter @objectstack/spec typecheck && pnpm --filter @objectstack/spec test — exit 0; 483 test files, 13773 tests passed.
  • Repo-wide eslint . --no-inline-config — run in full rather than narrowed: 6803 files linted, 0 errors, 0 warnings, exit 0.
  • One gate exits 1, and it is not this diff: pnpm check:cross-package-test-inputs. It flags packages/cli/test/init-created-files-summary.e2e.test.ts descending packages/spec/dist/, a radius no declared glob reaches. Controlled: the gate enumerates the real filesystem (readdirSync), so with packages/spec/dist/ moved aside it exits 0 ("OK: 29 package(s) read outside themselves, all declared") and with it present it exits 1 — on the identical source tree. The trigger is that spec was built locally, which every local gate run requires; CI runs this gate on an unbuilt checkout and therefore never sees it. Neither file is in this diff. Filed as a finding in the report on class (c): a dotted messages id is authorable, schema-valid and resolves to nothing — and TranslationItemSchema's own @example teaches it, at two sites #18190, ⛔ not repaired here.
  • Two further gates first answered PREREQUISITE NOT MET (exit 3, explicitly "nothing was measured") and one self-test refused on a shallow clone. All three were re-run after building the missing packages and unshallowing, and all three then exited 0; the reconciliation above reflects the re-runs.

Generated by Claude Code


Generated by Claude Code

…able messages id

`messages` is a flat `Record<string, string>` while `t()` resolves a key by
walking its dot path, so an id that merely contains a dot is unreachable.
Both docblock `@example` blocks on `TranslationDataSchema` and
`TranslationItemSchema` demonstrated exactly such an id (`'common.save'`);
they now author `commonSave` and state the rule.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:system tooling labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/protocol/kernel/i18n-standard.mdx (via common.save (literal, a string literal in a comment on a changed line), commonSave (literal, a string literal in a comment on a changed line))
What this run could not see
  • 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 — 136 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 79a046f8cdf085d95200826ee9bb2fa6584bc3d5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 257f69203a722e1d3658b979de884a52f555098c — the merge of head 9620486d10051794636f8f10e66cc1c4f7e2ef21 into base 79a046f8cdf085d95200826ee9bb2fa6584bc3d5, 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 257f69203a722e1d3658b979de884a52f555098c && git checkout 257f69203a722e1d3658b979de884a52f555098c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 79a046f8cdf085d95200826ee9bb2fa6584bc3d5 9620486d10051794636f8f10e66cc1c4f7e2ef21 && git checkout -B drift-repro 79a046f8cdf085d95200826ee9bb2fa6584bc3d5 && git merge --no-ff 9620486d10051794636f8f10e66cc1c4f7e2ef21

node scripts/docs-audit/affected-docs.mjs --json 79a046f8cdf085d95200826ee9bb2fa6584bc3d5

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 79a046f8cdf085d95200826ee9bb2fa6584bc3d5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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/s tooling

Projects

None yet

2 participants