Skip to content

Record that author-written validation-rule messages are translatable again (objects.OBJECT._validations.RULE.message) — ADR status line or short ADR #14402

Closed

Description

@os-warren

Governance follow-up promised on PR #14381 before it merged, filed so the trail is queued rather than remembered.

Why a record is owed

#14381 surface 2 gives an author-written validation rule's message a bundle key (objects.OBJECT._validations.RULE.message) and a reader on the write path (authoredRuleMessage in packages/objectql/src/validation/rule-validator.ts, over the existing ValidationMessageContext.translate hook from #3957).

17.0.0 retired the old validationMessages group (#4667, ADR-0049) with the recorded sentence "Validation messages are not translated through a translation group." #14381 makes that sentence false. Prime Directive #13 says reversing a recorded decision is itself a decision — and today the only place this one is recorded is a retired-key tombstone and a changeset, which is precisely the shape #13 warns is easy to miss.

The ruling to record (made on #14381, not reopened here)

This is ADR-0049 being enforced, not reversed:

  • the policy is enforce-or-remove; validationMessages was removed because nothing read it;
  • the new key ships with its reader in the same change — the other half of the same policy;
  • it is not the same key: the old group was top-level-keyed and could not tell two objects' rules apart; the new one is object-scoped beside _views / _actions / _tabs;
  • the ADR-0087 conversion that strips the old key from stored bundles is untouched.

What this card delivers

Whichever is the house shape for this — an amended status line on ADR-0049 (or the 17.0.0 retirement record), or a short standalone ADR — stating: author-written rule messages are translatable via the object-scoped key as of the version #14381 ships in; the 17.0.0 retirement stands; the two are not the same route. One governed-surface PR, human-merge; the code is independent and does not wait on it.

Not a request to hold #14381.


Triage (R+99) — graded, and the Blocked-by line removed

The body opened with **Blocked-by: #14381.** That one line was wrong three ways, so it is gone rather than corrected:

  1. It pointed at a PR. The Blocked-by: canon points at an issue, never a pull request — an issue closes and clears the block; a PR number has no such lifecycle in the index.
  2. It was already exhausted. PR i18n: give bulk-action defs, custom validation messages and dataset labels a bundle key #14381 merged at 2026-09-02T05:28:42Z (merged_by: os-warren, head 7b1c387bb). The thing this card was waiting for has happened — the key and its reader are on main.
  3. It could not have parsed anyway. The marker is read line-anchored and undecorated; **Blocked-by: #14381.** is bolded, ends in a full stop, and sits inside a sentence. Even while i18n: give bulk-action defs, custom validation messages and dataset labels a bundle key #14381 was open, nothing mechanical would have seen this card as blocked.

⇒ pm:queue. This card is ready now. Its premise is not "wait for the code" — the code has landed, which is exactly what makes the missing governance record live rather than anticipatory. Prime Directive #13's concern is at its sharpest today: main contains a change that falsifies a recorded 17.0.0 sentence, and the only trace is a tombstone and a changeset.

Scope ruled

Exactly what the card asks and nothing more: one governed-surface PR recording that author-written rule messages are translatable via the object-scoped key as of the version #14381 shipped in, that the 17.0.0 retirement stands, and that the two are not the same route. The card's four-bullet "enforce, not reverse" reasoning is the content — it was settled on #14381 and ⛔ is not reopened here.

⚠️ docs/adr/** is a governed surface (Prime Directive #14): no agent seat marks this PR ready, queues it, or arms auto-merge. A human merges it. Say so in the PR body.

Choosing between an amended ADR-0049 status line and a short standalone ADR is the domain:spec seat's call by house convention — it is a form question, not a direction question, so it does not need the maintainer.

Routing

domain:spec — ADR-0049 is the enforce-or-remove policy governing packages/spec properties, and the record lands on that ADR or beside it. documentation. priority:p3: no code waits on it, nothing is broken, and the change it records has already shipped — it is owed as a trail, and the cost of it going missing is paid by whoever next reads the 17.0.0 retirement and believes it.

Generated by Claude Code

Activity

  1. added
    documentationImprovements or additions to documentation
    pm:queueReady for the PM dispatch loop
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Sep 2, 2026
  2. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Correction to my triage comment above — reason 3 was wrong.

    I wrote that **Blocked-by: #14381.** "could not have parsed anyway" because the marker is read line-anchored and undecorated. That is not how the gate reads it.

    scripts/pm/check-half-states.mjs:113-117 says the opposite, deliberately:

    The line may be DECORATED in either channel — 「Blocked-by: #9612」 is the natural markdown for a line meant to be grepped, and reading it as absent left #10063 permanently blocked in silence (#10102).

    So decoration is tolerated on purpose, and my reason 3 was the exact mistake #10102 exists to prevent. Withdrawn.

    The other two reasons stand and are what removed the line:

    1. it pointed at a PR, not an issue — and PR i18n: give bulk-action defs, custom validation messages and dataset labels a bundle key #14381 has no issue lifecycle for the unlock sweep to read;
    2. it was exhausted — i18n: give bulk-action defs, custom validation messages and dataset labels a bundle key #14381 merged at 2026-09-02T05:28:42Z.

    The grading is unchanged: pm:queue, ready now, scope as ruled above.

    While checking this I also confirmed a second thing I have had wrong in my own notes: the Blocked-by: line is read in two channels, body and comments — check-half-states.mjs:106-112 records that 26 of 40 blocked cards were body-clean at the 2026-08-19 census, and that parking the line in a comment is a deliberate response to the MCP body-escaping hazard (#8813). A body-only read reports every one of those as having left the machine nothing. Noting it here because it changes where a future reader should look before concluding a card has no blocker line.


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 4, 2026
  4. self-assigned this
    on Sep 4, 2026
  5. os-sales commented on Sep 4, 2026

    @os-sales
    Collaborator

    Claim: PM loop round R1
    Session: session_01G4138K1EG7kQ81FNba5Kp4
    Branch: claude/issue-14402-adr-0049-validation-message-record
    Worktree: objectstack-issue-14402
    Domain: domain:spec
    File surface: docs/adr/0049-no-unenforced-security-properties.md (the real filename, located on origin/main at 15:12Z — the card's illustrative docs/adr/** is a directory, not a path) · content/docs/ui/translations.mdx (the one site on origin/main carrying the falsified 17.0.0 sentence — git grep -l "Validation messages are not translated" origin/main returns exactly this file) · at most one new docs/adr/*.md if the form ruling below is overturned by measurement (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus — node scripts/pm/dispatch-gates.mjs --tier docs/adr/0049-... run at 2026-09-04T15:12Z against the tree at 097a01c: "no path-derived mandate", tier is the PM's per-card call. opus because the card is a governance record whose wording is the deliverable. ⚠️ docs/adr/** is a governed surface: draft PR only, ⛔ no ready-flip, ⛔ no queue, ⛔ no auto-merge — a human merges it.
    Clause-②: no — a governance record. No contract file, no accept/reject change, no public-surface widening.
    Serial constraints cleared: docs/adr/** carries no reservation on this seat's hot-file queue at 2026-09-04T15:12Z, and no in-flight card in this lane names it. content/docs/ui/translations.mdx is likewise unreserved and is NOT in the content/docs/references/** generated corpus that this round's #14455 and #14079 dispatches share — no overlap with either. In-flight/queued PRs at 15:12Z: PR #15386 (#14788) and PR #15281 (#15229) — both disjoint.


    Generated by Claude Code

  6. claude commented on Sep 4, 2026

    @claude
    Contributor

    Claim: os-dev subagent, round R1 (separate from the dispatching PM claim 5542494462 above).

    Session: session_01G4138K1EG7kQ81FNba5Kp4 (dev subagent of the dispatching domain:spec PM session; the seat shares one session id, so this comment plus the branch is the identity bit)
    Branch: claude/issue-14402-adr-0049-validation-message-record (pushed empty at claim time as the write-route probe — push succeeded)
    Worktree: objectstack-issue-14402
    Base: 52d5a52d5a15d46c18d47ef09ab2cdb64dbe3051

    Clause-2: no — a governance record. No contract file, no accept/reject change, no public-surface widening.

    Assignee untouched by this seat (it is the PM's field).

    Declared file surface, to be re-derived from origin/main before the first edit:

    • docs/adr/0049-no-unenforced-security-properties.md
    • content/docs/ui/translations.mdx

    Governed surface: docs/adr/**. This PR will be opened as a draft and left as a draft — no ready-flip, no queue, no auto-merge from this seat. A human merges it.


    Generated by Claude Code

  7. claude commented on Sep 4, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 14402,
      "status": "done",
      "branch": "claude/issue-14402-adr-0049-validation-message-record",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15438",
      "premise_still_valid": true,
      "summary": "PR #15438 is open as a DRAFT and is LEFT as a draft for a human to merge — docs/adr/** is a governed surface, so this seat did not flip it ready, queue it, arm auto-merge, or approve it. Two files, 70 insertions and 6 deletions, final commit 815478bc6: (1) docs/adr/0049-no-unenforced-security-properties.md gets a status-line note plus a dated 'Amendment (2026-09-04)' section in this repo's existing amendment shape (ADR-0045 is the worked example), carrying the card's four settled bullets — enforce-or-remove, reader-in-the-same-change, not the same key, ADR-0087 conversion untouched; (2) content/docs/ui/translations.mdx loses the falsified sentence 'Validation messages are not translated through bundles' and gains the objects.OBJECT._validations.RULE.message row in the 'What you can translate' table, with the boundary bullet rewritten to the limit that is actually current (the authored sentence is substituted whole, no interpolation). R2 (amend in place, no new ADR) was followed. H1 PARTIALLY FALSIFIED and reported in the ADR itself rather than papered over: ADR-0049's filename is right and it IS the enforce-or-remove gate, but its own text is scoped to SECURITY properties and its Non-goals place the non-security clusters elsewhere, while validationMessages is a translation key. The record still lands there because the repo cites ADR-0049 as the general spec-property retirement policy, including in TRANSLATION_KEY_GUIDANCE in packages/spec/src/system/translation.zod.ts; the amendment states explicitly that no new scope is claimed for the ADR. H2 CONFIRMED on origin/main: authoredRuleMessage exists at packages/objectql/src/validation/rule-validator.ts and objectValidationMessageKey at packages/spec/src/system/i18n-resolver.ts; PR #14381 merged 2026-09-02T05:28:42Z, merge commit b2eab9594bcb9816e65d834523d286b28f1d6196, head 7b1c387bb. H3 DERIVED, not guessed: 17.3.0. H4: the wider sweep found exactly ONE falsified prose site (the one amended). The spec strict-door guidance, the liveness ledger and the published objectstack-i18n skill were already corrected; two adjacent gaps were filed rather than ridden along. Assignee field untouched by this seat. content/docs/releases/ untouched. skip-changeset label applied additively (POST) and read back — it survived the size labeler's write (size/s present, skip-changeset present). One cosmetic platform artefact, not repaired on purpose: the PR body carries two attribution footers because the platform appended its own recognised block under mine; AGENTS.md forbids the re-append loop and a PATCH would normalise the session-URL footer down to bare, so it is left as is. Body otherwise read back byte-identical.",
      "tests": "Union re-run on the FINAL commit 815478bc6 (git rev-parse --short HEAD from that run; git status --porcelain empty). Gate family derived with 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' (no paths passed; it took the change set from git, 2 paths vs merge base) giving 43 runnable families, harvested with --commands. ALL 43 EXIT 0. Exit codes captured before any pipe (command redirected to a log file, then EXIT captured, then tail). Four first answered PREREQUISITE NOT MET, read as NOT MEASURED and not as red: check:doc-formula-expressions EXIT=3 (formula not built), check:doc-security-posture EXIT=3 (lint not built), check:docs EXIT=1 (packages/spec/json-schema missing), check:skill-examples EXIT=1 (no .d.ts in packages/spec/dist, then in packages/client-react/dist). Cleared by two builds, both through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-14402-dev: turbo build for spec, lint and formula (VERDICT command-exit 0, held 181s, waited 146s) and for client-react and client (VERDICT command-exit 0, held 180s, waited 0s). Re-run after: 17, 18, 19 EXIT=0; 22 EXIT=0 printing its own verdict line '257 prose examples type-check across 3 surface(s)'. Load-bearing greens for this diff, taken from the gates' own verdict lines: check:adr-symbol-anchors (both new path-hash-symbol anchors resolve), check:adr-anchors, check:adr-links (plus self-tests), check:doc-authoring, check:docs-single-h1, check:corpus-claim-drift, check:doc-anchors, check:nul-bytes. Control-byte self-scan beyond the gate: grep -naP over both changed files, no match. No ablation and no reverse-verification were performed — this is a prose governance record with no executable behaviour to mutate, so the template's ablation field would be fabricated here. DECLARED NARROWING (the only deviation): pnpm lint, the repo-wide ESLint scan, was narrowed, and the narrowing is a measurement with all three pieces of evidence. (1) Population read from ESLint's own config: 'pnpm exec eslint --no-inline-config --format json' on both changed files returns, per file, 'File ignored because no matching configuration was supplied.' (2) File count read from --format json: 2 results, 0 files linted; every files: block in eslint.config.mjs targets the ts/tsx/mts/cts/js/jsx/mjs/cjs extension set, and neither package.json nor eslint.config.mjs configures a markdown or mdx processor. (3) Invariance for untouched files: git diff --name-only with three-dot origin/main against HEAD is exactly the two Markdown files, eslint.config.mjs is not in the diff and declares no project or projectService key, so type-aware linting is off and no untouched file's verdict can move because of this diff. Version derivation (H3), four independent legs: changeset b2eab95 sits under the 17.3.0 heading in the spec, objectql, lint and cli CHANGELOGs; packages/spec/package.json and packages/objectql/package.json both read 17.3.0; tags for spec 17.3.0 and objectql 17.3.0 exist on origin; 'git merge-base --is-ancestor b2eab959... refs/tags/spec-17.3.0' EXIT=0 — a positive ancestry read, self-proving on a shallow checkout, with a same-target control leg also EXIT=0. Churn re-derived as instructed with 'node scripts/pm/git-history.mjs log --days=30' over both paths (EXIT=0, method line printed: git log --first-parent origin/main since 2026-08-05, floor 2026-07-29, tip 2026-09-04): five commits, most recent 09cc6be43 (2026-09-03) and 68c5dbaab (2026-08-26) — no in-flight collision. Dedupe before filing the two findings ran on the REST channel (a repo-scoped probe returned 200, so REST not MCP), enumerating open issues to closure (pages 1-6, page 7 empty) plus the documentation-labelled state=all window 13108 to 15434 — 687 unique issues, PRs excluded — then grepped locally. POSITIVE CONTROL: the first attempt's control (#14402 itself) was ABSENT, so that read was discarded and the window deepened until the control answered present; only then were the findings filed. Nearest neighbour #14377 (closed) is the same falsehood one layer up in the published skill and is already fixed; neither finding duplicates it.",
      "mcp_calls": "0 — the whole run used the container REST channel (repo-scoped probe returned 200) and git; no MCP GitHub call was made",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #15435: content/docs/ui/translations.mdx's 'What you can translate' table omits the bulkActions and datasets groups — the other two surfaces #14381 shipped live in 17.3.0; a missing-inventory defect, not a false sentence, on the same page. Unassigned, unlabelled, left for PM triage.",
        "filed as #15437: the ADR-0087 conversion text rendering the translation.validationMessages row of docs/protocol-upgrade-guide.md still stops at the authored literal and never names the bundle key, while its sibling prescription TRANSLATION_KEY_GUIDANCE in packages/spec/src/system/translation.zod.ts does. It lands in packages/spec source plus a regenerated artifact, so a different gate family — the bounded in-place-fix exemption fails on its fourth condition (new verification surface) and it was NOT touched here. Unassigned, unlabelled.",
        "not filed, recorded here as a measurement rather than a defect: ADR-0049's own text is security-scoped and its Non-goals exclude the non-security clusters, yet the repo routes all spec-property retirement through it. Recorded inside the amendment's scope note rather than opened as a card, because nothing is wrong on main — it is a naming question for the maintainer if it is ever worth one."
      ]
    }

    Generated by Claude Code

  8. os-sales commented on Sep 4, 2026

    @os-sales
    Collaborator

    ACCEPT — PR #15438 (head 815478bc6, Fixes #14402) · ⚠️ governed surface ⇒ awaiting a human merge, ⛔ not queued

    Reviewer of record: domain:spec PM seat, session session_01G4138K1EG7kQ81FNba5Kp4 (os-sales), R1, 2026-09-04T15:55Z. Every reading below was taken against GitHub or origin/main, ⛔ not against the report's own prose.

    Clause ② gate

    Clause-②: no, declared on the claim and confirmed against the diff: get_files returns exactly two Markdown files, no packages/spec/src/**, no accept/reject change, no public-surface widening. No tier review owed; needs:contract-review correctly never hung.

    PR form (15:52Z)

    Draft ✅ · base main ✅ · first body line Fixes #14402 ✅ · 2 files, +70 −6 ✅ · skip-changeset present and read back after the size labeler's write ✅. Changeset disposition correct: docs/adr/** is a governance record and content/docs/** feeds the private docs site — the PR publishes nothing from a released package.

    Independent verification of the load-bearing claims

    ⛔ Not taken from the report. All at origin/main, 15:53Z, each with a negative control where a zero would otherwise be unprovable:

    claim reading verdict
    authoredRuleMessage exists and falls back to the authored literal on a bundle miss rule-validator.ts:2282; :2291-2293 — II18nService echoes the key back on a miss, and the guard translated !== key falls through ✅ the docs sentence about fallback is accurate, not aspirational
    objectValidationMessageKey spells the object-scoped key exported at i18n-resolver.ts:2039 ✅
    shipped version is 17.3.0 packages/spec/package.json reads 17.3.0 ✅ (dev derived it on four independent legs, including a positive ancestry read)
    the amendment shape has a precedent docs/adr/0045-…md carries a ## Amendment section ✅
    ADR-0049 is cited as the general spec-property retirement policy translation.zod.ts:533 ✅ — and stronger than claimed, see below

    ⭐ On H1, which the dev partially falsified rather than papering over

    The dev measured that ADR-0049's own text is scoped to security properties and its Non-goals place the non-security clusters elsewhere, while validationMessages is a translation key — then landed the record there anyway, with an explicit scope note stating that no new scope is claimed. That is the right call and it is disclosed in the artifact rather than in a side channel, which is what my R2 asked for.

    ⭐ My own re-measurement makes the routing better supported than the dev argued: translation.zod.ts cites ADR-0049 for three further non-security retirements — :867 and :923 (submitLabel), :931 (a ComponentPropsMap key) — not just the validationMessages line the report cited. So the repo's practice of routing all spec-property retirement through ADR-0049 is established well beyond this one case, and the amendment lands where readers actually look.

    ⚠️ The dev's third out-of-scope item — that ADR-0049's title and text are security-scoped while the repo uses it as the general gate — was deliberately not filed as a card, on the reasoning that nothing is wrong on main and it is a naming question. I agree it is not a defect; ⭐ I also think it is the most durable observation in this run, and it is now recorded in three places (that scope note, the report, and here) so a future maintainer decision has the evidence ready without anyone re-deriving it.

    Evidence

    43 of 43 gate families exit 0 on the final commit, exit codes captured before any pipe, working tree clean. Four first answered PREREQUISITE NOT MET (unbuilt packages, exit 3 / exit 1) and were recorded as NOT MEASURED, not red — the honest reading — then went green after two locked builds. check:adr-symbol-anchors, check:adr-anchors, check:adr-links, check:doc-authoring, check:docs-single-h1, check:corpus-claim-drift, check:nul-bytes are the load-bearing ones for this diff and all are green. No ablation was performed and the dev said so explicitly rather than inventing one — correct for a prose record with no executable behaviour to mutate. The pnpm lint narrowing is declared with all three legs (population read from ESLint's own resolution, file count from --format json = 0 files linted, and invariance because the config declares no project/projectService).

    Out-of-scope findings — verified present

    #15435 (the same page's table omits the bulkActions and datasets groups) and #15437 (the ADR-0087 conversion text never names the bundle key) both exist, open, unassigned and unlabelled for triage. ⭐ The dev deepened its dedupe window until a positive control answered present, discarding a first read whose control was absent — the failure mode that makes a zero-hit dedupe worthless, caught by the dev itself.

    ⚠️ Landing — this PR is NOT queued, and that is the finished state

    docs/adr/** is a governed surface. ⛔ Not flipped ready, ⛔ not enqueued, ⛔ no auto-merge, ⛔ no approval from any agent seat. A human merges it, and that merge is the review record.

    Review requested from both authorized approvers, os-zhuang and hotlong, on the draft at 15:54Z — pushed actively rather than left to be discovered. Read back at 15:55Z: requested_reviewers: [hotlong, os-zhuang], draft: true preserved, labels intact. This card is carried in the round report under awaiting a human merge until it lands, because "waiting for a human" and "forgotten" look identical on GitHub.

    Card state: pm:dispatched held (delivery is not landing); assignee held. On MERGED: strip pm:dispatched, probe origin/main for the amendment heading and the replaced sentence, landing note here.


    Generated by Claude Code

  9. os-sales commented on Sep 4, 2026

    @os-sales
    Collaborator

    Claim: PM loop round R1 — corrected re-issue, ⛔ not a re-claim of new work
    Session: session_01G4138K1EG7kQ81FNba5Kp4
    Branch: claude/issue-14402-adr-0049-validation-message-record
    Worktree: objectstack-issue-14402
    Domain: domain:spec
    Clause-②: no
    Container & model: S, mode:subagent, model: opus — unchanged from the original claim 5542494462.
    File surface: unchanged from 5542494462; delivered as PR #15438 at 815478bc6.
    ⚠️ Governed surface (docs/adr/**): draft-only, human merge. ⛔ Never ready-flipped, ⛔ never enqueued, ⛔ never approved by an agent seat. Unchanged.


    Why this comment exists — a record defect, ⛔ not a re-grading. node scripts/pm/check-clause2-carriers.mjs --pair 15438 reads the pair as NOT clause-② legible (exit 4), for the same two seat-side causes found across this round:

    1. the original claim appended rationale to the predicate's line (Clause-②: no — a governance record. …), and the fixed spelling admits exactly Clause-②: yes / Clause-②: no;
    2. the most recent claim governs — the implementing dev's claim came after this seat's, so a declaration living only in the PM's claim is invisible to the gate.

    The substance was and is no, verified against the PR's actual diff: two Markdown files, docs/adr/0049-no-unenforced-security-properties.md and content/docs/ui/translations.mdx. No contract file, no accept/reject change, no public-surface widening.

    ⚠️ Worth noting for this card specifically: the clause-② legibility defect changes nothing about its landing path. This PR is governed, so it was never going to be enqueued by a seat under any reading — a human merges it, and that merge is the review record. The correction is for the audit trail, not for a gate this card ever passes through.

    ⛔ Nothing was rewritten in the dev's claim comment — the correction is additive. Review remains requested from both os-zhuang and hotlong; all 35 checks were green at 16:00Z.


    Generated by Claude Code

  10. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 5, 2026
  11. claude commented on Sep 5, 2026

    @claude
    Contributor

    Landed — PR #15438 merged, card closed and fully stroked

    domain:spec follow-up (seat vacant since 01:38Z; os-project-manager seated 01:44Z — this card was inside the declared linger fence), 2026-09-05T03:58Z.

    ⭐ A human did the governed merge, exactly as the rule requires. os-zhuang approved at 03:16:45Z and added the PR to the merge queue at 03:17:00Z; it merged 8e8860ed8 at 2026-09-05T03:56:55Z. ⛔ No agent seat ever flipped it ready, queued it, armed auto-merge, or approved it — the PR body's own published commitment held end to end. (The 01:30Z ready_for_review was also os-zhuang.)

    Close verified from the API, ⛔ not asserted: state=closed, state_reason=completed, closed_at=2026-09-05T03:56:56Z. pm:dispatched stripped and assignee cleared in one PATCH, read back and diffed: ['documentation','domain:spec','pm:dispatched','priority:p3'] → ['documentation','domain:spec','priority:p3'], assignees [], nothing collaterally dropped.

    Landing probes — both directions, each with a live control

    probe main 8e8860ed8 control 581d8f84c (merge commit's first parent) reading
    ## Amendment (2026-09-04) in docs/adr/0049-…md 1 0 ✅ added
    Validation messages are not translated through bundles in content/docs/ui/translations.mdx 0 1 ✅ removed
    positive: _validations row in the translations guide present at :72 and :252 — ✅

    ⭐ Two probes moving in opposite directions against live controls is the strongest form of this reading — a vacuous probe cannot produce both. That matters here because the 0-vs-0 trap cost this lane a wrong landing note on #14406 earlier in the shift.

    What this actually fixes, in business terms

    content/docs/ui/translations.mdx:248 carried, under the heading "Honest limits worth knowing before you plan around them" — the section customers read specifically to plan around — the sentence "Validation messages are not translated through bundles." That has been false since 17.3.0: objectValidationMessageKey (packages/spec/src/system/i18n-resolver.ts) is read on the write path by the rule evaluator (packages/objectql/src/validation/rule-validator.ts, six call sites through authoredRuleMessage).

    So a shipped, paid-for localization capability was denied by our own documentation:

    • a customer running a multi-language tenant either ships English validation errors to non-English users, or builds a workaround (locale logic inside rule expressions, or duplicated rules per locale) that the platform then has to support;
    • ⭐ and an AI agent authoring a metadata app reads that same page as its context — so it would never emit the _validations key, and had no route to discover the capability, because the doc explicitly denied it existed. On the anti-AI-error axis that is worse than a missing guardrail: the documentation was actively teaching the error.

    The ADR amendment is the other half, and it is insurance rather than paperwork: without it, a later correctness pass reading the 17.0.0 retirement of the top-level validationMessages group (#4667) would conclude that validation-message translation was killed as policy, and would either refuse to re-add it or delete the new object-scoped key as a violation. ⛔ The two are not the same route — the retired one was keyed by rule name alone and could not tell two objects' rules apart; the live one is object-scoped and shipped with its reader.

    ⚠️ Still open on the same page — this was one of three

    ⇒ The systemic reading, offered for triage and ⛔ not as a ruling: this page misstates or omits three shipped translation surfaces, and #15438 fixed one. The defect class is discoverability of shipped capability, not three unrelated p3 doc nits.


    Generated by Claude Code

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

    documentationImprovements or additions to documentationdomain:specDomain lane — seat card indexed by label:pm:seatpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions