Skip to content

i18n: three authored display surfaces have no bundle key at all (bulk-action defs, custom validation messages, dataset labels) #14253

Description

@os-warren

Measured on @objectstack/spec / @objectstack/objectql / @objectstack/rest 17.2.0, while giving an application (objectstack-ai/duly) a complete zh-CN bundle.

Note: key placeholders below are written OBJECT / VIEW / ACTION / RULE / NAME rather than in angle brackets — GitHub's body sanitizer strips <a>-shaped fragments, which silently ate them on the first revision of this issue.

TranslationDataSchema addresses objects, fields, field options, views, sections, actions, action params, apps, navigation, dashboards, widgets, pages, flows (label + screens), settings and messages. TRANSLATABLE_METADATA_TYPES is { view, action, object, app, dashboard, page }.

Three surfaces carry authored, user-facing display text that no key in that schema can reach. Each renders in the source locale inside a fully translated screen — which is the bad failure mode, because it reads as a styling quirk rather than a missing translation.

1. bulkActionDefs on a list view

ListViewSchema.bulkActionDefs[] carries label, confirmText, confirmLabel and params[].label / .placeholder / .help. A bulk-action def is not an action document, so it never reaches translateAction, and no bundle group addresses it.

Measured in a browser, Accept-Language: zh-CN, against a fully translated app — the selection bar reads:

已选择 1 项 · Complete · Skip · 清除

Two English words between two Chinese ones. Screenshot: https://raw.githubusercontent.com/objectstack-ai/duly/claude/issue-18-screenshots/shots/bulk-toolbar-english.png

The workaround (bulkActions: ['some_row_action'], promoting row actions, which are translated) is not equivalent: it is N elevated action dispatches instead of one data-plane updateMany.

Suggested shape: objects.OBJECT._views.VIEW.bulkActions.NAME.{label,confirmText,confirmLabel,params.PARAM.{label,placeholder,help}}, resolved in translateView.

2. A custom validation rule's message

objectql emits an authored rule message verbatim — { field, code: 'rule_violation', message: rule.message }, and the same in checkStateMachine's fallback. There is no _validations group on ObjectTranslationDataSchema.

messages['validation.field.SOME_KEY'] is a different thing: it overrides the platform's built-in field catalog (BUILTIN_VALIDATION_MESSAGES, which already ships zh-CN). It cannot address duly_duty's standing_no_frequency.

So a deployment gets platform-generated refusals in Chinese and author-written refusals in English, side by side.

Suggested shape: objects.OBJECT._validations.RULE.message, resolved where the rule error is built — the object and the rule name are both in hand there.

3. Dataset labels

defineDataset carries label, description, dimensions[].label and measures[].label. dataset is not in TRANSLATABLE_METADATA_TYPES and TranslationDataSchema has no datasets group — yet a measure label is drawn on the dashboard, under every metric tile and on every chart axis.

Measured on a translated dashboard: the tile titles and descriptions are Chinese (they are dashboards.NAME.widgets.ID.*), and directly beneath each one the measure label renders Untouched > 14 days / Untouched > 30 days / Oldest touch. Screenshot: https://raw.githubusercontent.com/objectstack-ai/duly/claude/issue-18-screenshots/shots/dashboard-zh.png

Suggested shape: datasets.NAME.{label,description,dimensions.DIM.label,measures.MEASURE.label} plus a translateDataset in the dispatch table.


Filed from objectstack-ai/duly#18. The application records all three as declared, reasoned exemptions in its coverage walk (src/translations/authored-text.ts) rather than working around them, and its gate fails if a new one appears or an existing one stops matching.


Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 2, 2026
  2. os-warren commented on Sep 2, 2026

    @os-warren
    CollaboratorAuthor

    Claiming this and taking it to a fix — I filed it from the application side; the maintainer has asked for it solved rather than worked around, because it blocks a Chinese-language first release.

    • Session: session_01SqkTcrxUFci7nqXdbBSe2p
    • Branch: claude/group-company-task-tracking-astg44 (this session's designated branch, restarted from origin/main; it carried no commits and had never been pushed)

    Scoping done before dispatch, so the shape is on the record

    Reading the code changes two things about how big this is.

    The dispatch table is the single seam, and it already knows that. METADATA_DOCUMENT_TRANSLATORS in packages/spec/src/system/i18n-resolver.ts is the one table, and TRANSLATABLE_METADATA_TYPES is derived from it rather than restated — with a comment saying that is deliberate so "adding a translator here reaches the REST boundary with nothing else to remember (#3786)". So surface 3 (datasets) is a translateDataset plus one table entry plus a schema group, and the REST surface follows for free.

    Surface 2 is much smaller than the issue implies. I wrote that the rule message is emitted verbatim with nowhere to look up a translation. The lookup mechanism is in fact already there: packages/objectql/src/engine.ts:2342 holds private i18nService?: { t? }, and :5603 documents it as "the i18n service used to localize write-path validation messages and field-label localization (#3957)". Built-in field-catalog messages already flow through it. What is missing is only a key shape for an author-written rule — not a channel to resolve one. That is a correction to my own issue body worth having before anyone starts.

    Surface 1 (bulkActionDefs) stays as described: an extension inside translateView, since a bulk-action def is part of the view document.

    One PR, three commits, one per surface — they share translation.zod.ts and splitting them across PRs would just serialise a conflict.

    Acceptance is measurable from outside

    objectstack-ai/duly carries all three as declared, reasoned exemptions in its i18n coverage walk (src/translations/authored-text.ts), and its gate fails when an exemption stops matching. So when this lands, that app's gate goes red on the three stale exemptions — which is the signal the fix is real, not a claim about it. The screenshots in this issue are from that app and can be re-taken against the fix.


    Generated by Claude Code

  3. os-warren commented on Sep 2, 2026

    @os-warren
    CollaboratorAuthor

    Dev seat claim — picking this up from the PM dispatch above; same session, so this is a continuation of that claim, not a competing one.

    • Session: session_01SqkTcrxUFci7nqXdbBSe2p
    • Branch: claude/group-company-task-tracking-astg44 (already checked out in the shared primary checkout, so this seat works from a detached worktree at origin/main and pushes with an explicit refspec — git push -u origin HEAD:claude/group-company-task-tracking-astg44. The shared checkout's HEAD is not touched.)
    • Base: origin/main @ 4fac7a02

    Re-read the comment thread before starting: the only prior claim is the PM's, under this same session ID. No competing claim.

    Plan follows the scoping comment: one PR, three commits — bulkActionDefs inside translateView, an _validations.RULE.message key shape resolved through the existing i18nService channel in objectql (no second i18n path), and translateDataset registered in METADATA_DOCUMENT_TRANSLATORS so the derived TRANSLATABLE_METADATA_TYPES carries it to the REST boundary. Each surface gets a test that is red without the fix.


    Generated by Claude Code

  4. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage (R+89, triage seat, session session_019kDRpB7D2XzVzkaLp57T5D): state machine brought in line with the live thread — pm:dispatched · priority:p1 · domain:spec · type Feature. ⛔ This is not a dispatch by the triage seat; the card is already claimed (assignee os-warren, claim + dev-seat continuation under session_01SqkTcrxUFci7nqXdbBSe2p, branch claude/group-company-task-tracking-astg44) and the claim records the maintainer's own direction ("the maintainer has asked for it solved rather than worked around, because it blocks a Chinese-language first release") — the maintainer direct-route channel. A claimed card carrying no pm:* label is invisible to every lane and patrol view, which is the only thing corrected here.

    Routing note for the landing: all three surfaces land in packages/spec (translation.zod.ts, i18n-resolver.ts's METADATA_DOCUMENT_TRANSLATORS), and each adds a translation key shape to a published schema ⇒ Clause-②: yes — the delivered PR goes through the contract-review tier before it is enqueued (needs:contract-review on the PR when it opens; the spec seat or an at-tier reviewer clears it). The _validations.RULE.message half rides the existing i18nService channel in objectql, per the claim's own correction — ⛔ no second i18n path.


    Generated by Claude Code

  5. added
    priority:p1High: required for production / M2
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Sep 2, 2026
  6. os-warren commented on Sep 2, 2026

    @os-warren
    CollaboratorAuthor
    {
      "issue": 14253,
      "status": "done",
      "branch": "claude/group-company-task-tracking-astg44",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14381",
      "premise_still_valid": true,
      "summary": "All three surfaces landed as one draft PR, six commits (three surfaces + a doc-authoring fix the gate caught + a fifth commit the work turned up + the origin/main merge). (1) objects.OBJECT._views.VIEW.bulkActions.NAME.{label,confirmText,confirmLabel,params.PARAM.{label,help,placeholder}} resolved in translateView against config.bulkActionDefs — the one address a served def has, since both ViewItemSchema and expandViewContainer nest the ListView under `config`; the def's label stays a plain z.string() so the renderer contract is untouched. (2) objects.OBJECT._validations.RULE.message, spelled by a new objectValidationMessageKey and read by a new authoredRuleMessage seat routing all five authored-message emitters — on the EXISTING ValidationMessageContext.translate hook (#3957), no second i18n path into objectql; a nested conditional branch is keyed by the branch's own name and a platform-generated unevaluable-predicate rejection is deliberately left alone. (3) datasets.NAME.{label,description,dimensions.DIM.label,measures.MEASURE.label} + translateDataset registered in METADATA_DOCUMENT_TRANSLATORS; TRANSLATABLE_METADATA_TYPES is derived and no second hand-maintained list exists, so the REST boundary followed for free. ONE CORRECTION TO THE CARD'S PRESCRIPTION, not to its premise: translateDataset alone would NOT have fixed the screenshot. AnalyticsResult.fields[].label ('for legends/KPIs') is filled by copying dataset.measures[].label off the definition, and that definition arrives via POST /analytics/dataset/query, which resolves a saved dataset through getMetaItems and never passes through translateMetaItem. So the metadata read was the door a dashboard does not draw through. Commit 5 sends the resolved SAVED definition through the existing translateMetaItem before it is parsed and compiled (the inline Studio-draft branch is deliberately excluded, pinned both ways) — no new i18n path, the analytics service stays free of bundles. Key faces were measured against the authoring schemas, not mirrored: bulk-param options refused (options[].value is string|number|boolean, so a value-keyed map cannot address true vs \"true\" — the FLOW_SCREEN_FIELD_NO_OPTIONS argument), def successMessage refused (none declared), validation label/description refused (neither reaches a rejected caller), dataset dimension/measure description refused (dataset.zod.ts says so at the authoring site); every exclusion carries guidance. The retired validationMessages and errors tombstones now point at the live replacement instead of asserting no route exists — that retirement is NOT reversed (it was top-level-keyed and unread; its ADR-0087 conversion is untouched). Liveness: datasets seeded live and DRILLED with its reader in the same change; two stale ledger sentences corrected alongside (the translation header still called validationMessages 'the one dead group' a major version after #4667 removed it, and validation.message's note still named it as the override route).",
      "tests": "RED-FIRST, per surface, each done the safe way — commit the fix, `git restore --source=<ref> --` the SOURCE only (tree, not index), confirm the reversion on disk with an anchored `grep -c`, run, then `git checkout HEAD --` and confirm restoration the same way with `git status --porcelain` empty. (1) bulkActionDefs, reverting i18n-resolver.ts + translation.zod.ts (`bulkActions` count 6->0, `translateBulkActionDefs` 2->0): 10 failed / 190 passed. (2) validation messages, reverting rule-validator.ts only (`authoredRuleMessage` 6->0, `message: rule.message` 0->5): 3 failed / 180 passed. (3) datasets metadata door, reverting the same two spec files (`translateDataset` 7->0, `DATASET_MEMBER_NO_DESCRIPTION` 3->0): 10 failed / 210 passed. (3b) datasets analytics door, reverting rest-server.ts only (`translateMetaItem(req, 'dataset'` 1->0): 1 failed / 12 passed. REBUILD DISCIPLINE: `@objectstack/spec` was rebuilt before every objectql/rest run and the new exports were proven present in dist (`grep -c objectValidationMessageKey packages/spec/dist/system/index.mjs` = 3, `translateDataset` = 8) — objectql and rest resolve spec through its `exports` map, i.e. dist. Their OWN sources are relative imports, so the two reverse-verifications above needed no rebuild of those packages, and spec's dist was unchanged across them. SUITES at the merged head e865543fe: @objectstack/spec 12103 passed / 450 files; @objectstack/objectql 4398 passed / 254 files; @objectstack/rest 2805 passed / 167 files; typecheck green for all three (each printed its own OK line, e.g. 'check:test-typecheck: OK — @objectstack/objectql's test layer compiles'). GATES: `node scripts/pm/dispatch-gates.mjs --commands` derives 78 families for this diff (74 by path + 7 by kind, 3 shared); all 78 run AFTER the final commit at e865543fe — 76 exit 0, 2 exit 3 = NOT MEASURED (`check-test-completeness` needs a saved `turbo run test` log; `check:pm-half-states` needs the GitHub PM board — both print 'Nothing was measured' and both are explicitly not findings). Exit codes captured before any pipe (redirect to a file, then `EXIT=$?`, then tail). `pnpm lint` = `eslint . --no-inline-config` over the WHOLE repo, exit 0 in 101s — no narrowing to declare. `check:nul-bytes` OK (7830 files) plus a direct `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` over every file in the diff: clean. `check:generated` reports all 15 spec artifacts current post-merge. TWO GATES WERE RED FIRST and are recorded rather than hidden: `check:doc-authoring` caught two `#NNNN` citations I had written into customer-facing tombstone text (fixed in commit 4, which also adds the negative pin the gate's own remedy asks for), and five spec dist-reading gates refused on a stale dist after the reverse-verification restores moved src mtimes (rebuild, then green). EXTERNAL ACCEPTANCE: objectstack-ai/duly at dd3c619, its i18n gate run twice — baseline on published 17.2.0 = 29 passed; with node_modules/@objectstack/spec repointed at this branch's build (untracked symlink, trap-restored and verified back to ../.pnpm/@objectstack+spec@17.2.0/..., duly's tree left clean) = 1 failed / 28 passed, the failure being 'pins the metadata types the PLATFORM says are translatable' with 4 elements where 3 were expected. TRANSLATABLE_METADATA_TYPES now reads [action, app, dashboard, dataset, object, page, view]. STATED PLAINLY rather than over-claimed: duly's gate has two mechanisms and only ONE fires here. The platform-derived pin goes red (correct). The exemption-STALENESS mechanism cannot fire yet, because duly's authored strings are unchanged so its three exemption path-sets still match — retiring them is duly's own follow-up. So this is proof the platform's answer moved, not proof duly's exemptions are retired. POST-MERGE: origin/main @ 72adb7fcb merged in; the single overlapping file is packages/objectql/src/validation/rule-validator.ts (main's #14088 hookWrittenKeys work, a different region) — both sides verified present after the merge (`authoredRuleMessage` 6, `hookWrittenKeys` 5, `message: rule.message` 0), no merge=os-regen path was changed by both sides so the driver's silent-drop hazard did not arise, no os-regen-pending debt, and the full 78-gate union plus all three suites were re-run on the merge commit.",
      "open_questions": [
        {
          "question": "Surface 2 re-opens a translation route for author-written validation-rule messages that 17.0.0 closed. I landed it without a new ADR — is that the right call, or does it want one? The retirement of `validationMessages` (#4667, ADR-0049) came with the sentence 'Validation messages are not translated through a translation group', and this change makes that sentence false. My reading: ADR-0049 is enforce-or-remove, removal was chosen *because nothing enforced it*, and shipping a resolver is the other half of the same policy rather than a reversal — the new key is object-scoped (the old one could not tell two objects' rules apart), and its ADR-0087 conversion is untouched. But Prime Directive #13 says reversing a recorded decision is itself a decision, and the decision here is recorded in a tombstone rather than in an ADR, which is exactly the shape #13 warns is easy to miss.",
          "options": [
            "A — land as-is (what the PR does): treat this as ADR-0049 enforcement, not reversal. The tombstone text is updated to point at the live route; no ADR is written. Cost: the governance record for 'rule messages are translatable again' lives only in a changeset and a tombstone.",
            "B — same code, plus an amended status line or a short ADR recording that rule-message translation is back with a reader. Cost: one more governed-surface PR (human-merge only), and it delays nothing since the code is independent.",
            "C — revert surface 2 and escalate the whole question, keeping surfaces 1 and 3. Cost: the deployment keeps shipping author-written refusals in English beside platform refusals in Chinese, which is the half of the card the maintainer called blocking."
          ],
          "recommendation": "A, with B as a cheap follow-up if the maintainer wants the record. On real business need: this is measured, not speculative — a real zh-CN-first release is blocked by it and duly's own gate names the gap. On long-term soundness: object-scoped addressing with a reader in the same change is strictly the sound version of what was retired, and it adds no consumer-side tolerance. On making AI-written metadata hard to get wrong: the key face is one key, every neighbouring spelling that would translate nothing is refused with guidance naming the right home, and the two tombstones now route authors to a live key instead of a dead end — which is a net reduction in ways to get this wrong. On startup scope discipline: this is not a capability expansion, it is closing a hole in a shipped capability with zero new authoring surface beyond one optional string. The only axis pulling toward B is the paper trail, and that is cheap to add later without touching this code. ⛔ Do NOT read this as a request to hold the PR: it is a ratification question, and C is listed only for completeness."
        }
      ],
      "out_of_scope_findings": [
        "filed as #14376: i18n-extract scaffolds none of the three new key families, so `os i18n extract` will not offer them and `check:i18n-coverage` cannot see them — third time this exact follow-up has been needed (#11485, #13109), so the card also asks whether the fix should be structural rather than a fourth manual walk. Blocked-by #14253; kept out because it moves nine generated bundles and the coverage baseline.",
        "filed as #14377: the published skill `skills/objectstack-i18n` teaches that validation messages are not a translation group — false once this lands. `skills/**` is a governed surface, so it may not ride a mixed diff; the card carries the net-line-budget warning and flags a collision with the open DIET card #14315 on the same file.",
        "filed as #14378 (label `finding`): `rest-server.ts:2963` restates the translatable-type set by hand inside `translateMetaItem`, and this change makes that parenthetical short by one. Left alone deliberately — a one-word comment edit is not worth the conflict risk in that file. The card also records that the similar sentence in `conversions/registry.ts:3039` should be LEFT as it is, being the historical rationale for a 17.0.0 retirement."
      ]
    }

    Generated by Claude Code

  7. os-warren commented on Sep 2, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    ⚠️ Marker repost. The previous comment opened with an HTML-comment marker and the sanitizer ate it on the way into the store — read back, that comment now starts with a bare newline and the JSON fence, so a scan keying on the marker cannot see it. There is no comment-edit path from this seat, so the report is reposted here with the marker as literal first-line text. This comment supersedes #14253 (comment) (identical payload, plus the needs:contract-review label note below).

    Since that comment: needs:contract-review applied to PR #14381 per the triage seat's clause-② routing note, written as the union with the auto-labeler's set (documentation, size/xl, tests, tooling, protocol:system) and read back to confirm — a bare set would have wiped them.

    {
      "issue": 14253,
      "status": "done",
      "branch": "claude/group-company-task-tracking-astg44",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14381",
      "premise_still_valid": true,
      "summary": "All three surfaces landed as one draft PR, six commits (three surfaces + a doc-authoring fix the gate caught + a fifth commit the work turned up + the origin/main merge). (1) objects.OBJECT._views.VIEW.bulkActions.NAME.{label,confirmText,confirmLabel,params.PARAM.{label,help,placeholder}} resolved in translateView against config.bulkActionDefs — the one address a served def has, since both ViewItemSchema and expandViewContainer nest the ListView under `config`; the def's label stays a plain z.string() so the renderer contract is untouched. (2) objects.OBJECT._validations.RULE.message, spelled by a new objectValidationMessageKey and read by a new authoredRuleMessage seat routing all five authored-message emitters — on the EXISTING ValidationMessageContext.translate hook (#3957), no second i18n path into objectql; a nested conditional branch is keyed by the branch's own name and a platform-generated unevaluable-predicate rejection is deliberately left alone. (3) datasets.NAME.{label,description,dimensions.DIM.label,measures.MEASURE.label} + translateDataset registered in METADATA_DOCUMENT_TRANSLATORS; TRANSLATABLE_METADATA_TYPES is derived and no second hand-maintained list exists, so the REST boundary followed for free. ONE CORRECTION TO THE CARD'S PRESCRIPTION, not to its premise: translateDataset alone would NOT have fixed the screenshot. AnalyticsResult.fields[].label ('for legends/KPIs') is filled by copying dataset.measures[].label off the definition, and that definition arrives via POST /analytics/dataset/query, which resolves a saved dataset through getMetaItems and never passes through translateMetaItem. So the metadata read was the door a dashboard does not draw through. Commit 5 sends the resolved SAVED definition through the existing translateMetaItem before it is parsed and compiled (the inline Studio-draft branch is deliberately excluded, pinned both ways) — no new i18n path, the analytics service stays free of bundles. Key faces were measured against the authoring schemas, not mirrored: bulk-param options refused (options[].value is string|number|boolean, so a value-keyed map cannot address true vs the string 'true' — the FLOW_SCREEN_FIELD_NO_OPTIONS argument), def successMessage refused (none declared), validation label/description refused (neither reaches a rejected caller), dataset dimension/measure description refused (dataset.zod.ts says so at the authoring site); every exclusion carries guidance. The retired validationMessages and errors tombstones now point at the live replacement instead of asserting no route exists — that retirement is NOT reversed (it was top-level-keyed and unread; its ADR-0087 conversion is untouched). Liveness: datasets seeded live and DRILLED with its reader in the same change; two stale ledger sentences corrected alongside (the translation header still called validationMessages 'the one dead group' a major version after #4667 removed it, and validation.message's note still named it as the override route).",
      "tests": "RED-FIRST, per surface, each done the safe way — commit the fix, `git restore --source=REF --` the SOURCE only (tree, not index), confirm the reversion on disk with an anchored `grep -c`, run, then `git checkout HEAD --` and confirm restoration the same way with `git status --porcelain` empty. (1) bulkActionDefs, reverting i18n-resolver.ts + translation.zod.ts (`bulkActions` count 6 to 0, `translateBulkActionDefs` 2 to 0): 10 failed / 190 passed. (2) validation messages, reverting rule-validator.ts only (`authoredRuleMessage` 6 to 0, `message: rule.message` 0 to 5): 3 failed / 180 passed. (3) datasets metadata door, reverting the same two spec files (`translateDataset` 7 to 0, `DATASET_MEMBER_NO_DESCRIPTION` 3 to 0): 10 failed / 210 passed. (3b) datasets analytics door, reverting rest-server.ts only (the translateMetaItem dataset call 1 to 0): 1 failed / 12 passed. REBUILD DISCIPLINE: @objectstack/spec was rebuilt before every objectql/rest run and the new exports were proven present in dist (`grep -c objectValidationMessageKey packages/spec/dist/system/index.mjs` = 3, `translateDataset` = 8) — objectql and rest resolve spec through its `exports` map, i.e. dist. Their OWN sources are relative imports, so the two reverse-verifications above needed no rebuild of those packages, and spec's dist was unchanged across them. SUITES at the merged head e865543fe: @objectstack/spec 12103 passed / 450 files; @objectstack/objectql 4398 passed / 254 files; @objectstack/rest 2805 passed / 167 files; typecheck green for all three (each printed its own OK line, e.g. 'check:test-typecheck: OK — @objectstack/objectql's test layer compiles'). GATES: `node scripts/pm/dispatch-gates.mjs --commands` derives 78 families for this diff (74 by path + 7 by kind, 3 shared); all 78 run AFTER the final commit at e865543fe — 76 exit 0, 2 exit 3 = NOT MEASURED (`check-test-completeness` needs a saved `turbo run test` log; `check:pm-half-states` needs the GitHub PM board — both print 'Nothing was measured' and both are explicitly not findings). Exit codes captured before any pipe (redirect to a file, then EXIT=$?, then tail). `pnpm lint` = `eslint . --no-inline-config` over the WHOLE repo, exit 0 in 101s — no narrowing to declare. `check:nul-bytes` OK (7830 files) plus a direct control-character grep over every file in the diff: clean. `check:generated` reports all 15 spec artifacts current post-merge. TWO GATES WERE RED FIRST and are recorded rather than hidden: `check:doc-authoring` caught two issue-id citations I had written into customer-facing tombstone text (fixed in commit 4, which also adds the negative pin the gate's own remedy asks for), and five spec dist-reading gates refused on a stale dist after the reverse-verification restores moved src mtimes (rebuild, then green). EXTERNAL ACCEPTANCE: objectstack-ai/duly at dd3c619, its i18n gate run twice — baseline on published 17.2.0 = 29 passed; with node_modules/@objectstack/spec repointed at this branch's build (untracked symlink, trap-restored and verified back to the 17.2.0 store path, duly's tree left clean) = 1 failed / 28 passed, the failure being 'pins the metadata types the PLATFORM says are translatable' with 4 elements where 3 were expected. TRANSLATABLE_METADATA_TYPES now reads [action, app, dashboard, dataset, object, page, view]. STATED PLAINLY rather than over-claimed: duly's gate has two mechanisms and only ONE fires here. The platform-derived pin goes red (correct). The exemption-STALENESS mechanism cannot fire yet, because duly's authored strings are unchanged so its three exemption path-sets still match — retiring them is duly's own follow-up. So this is proof the platform's answer moved, not proof duly's exemptions are retired. POST-MERGE: origin/main @ 72adb7fcb merged in; the single overlapping file is packages/objectql/src/validation/rule-validator.ts (main's #14088 hookWrittenKeys work, a different region) — both sides verified present after the merge (`authoredRuleMessage` 6, `hookWrittenKeys` 5, `message: rule.message` 0), no merge=os-regen path was changed by both sides so the driver's silent-drop hazard did not arise, no os-regen-pending debt, and the full 78-gate union plus all three suites were re-run on the merge commit.",
      "open_questions": [
        {
          "question": "Surface 2 re-opens a translation route for author-written validation-rule messages that 17.0.0 closed. I landed it without a new ADR — is that the right call, or does it want one? The retirement of `validationMessages` (#4667, ADR-0049) came with the sentence 'Validation messages are not translated through a translation group', and this change makes that sentence false. My reading: ADR-0049 is enforce-or-remove, removal was chosen *because nothing enforced it*, and shipping a resolver is the other half of the same policy rather than a reversal — the new key is object-scoped (the old one could not tell two objects' rules apart), and its ADR-0087 conversion is untouched. But Prime Directive #13 says reversing a recorded decision is itself a decision, and the decision here is recorded in a tombstone rather than in an ADR, which is exactly the shape #13 warns is easy to miss.",
          "options": [
            "A — land as-is (what the PR does): treat this as ADR-0049 enforcement, not reversal. The tombstone text is updated to point at the live route; no ADR is written. Cost: the governance record for 'rule messages are translatable again' lives only in a changeset and a tombstone.",
            "B — same code, plus an amended status line or a short ADR recording that rule-message translation is back with a reader. Cost: one more governed-surface PR (human-merge only), and it delays nothing since the code is independent.",
            "C — revert surface 2 and escalate the whole question, keeping surfaces 1 and 3. Cost: the deployment keeps shipping author-written refusals in English beside platform refusals in Chinese, which is the half of the card the maintainer called blocking."
          ],
          "recommendation": "A, with B as a cheap follow-up if the maintainer wants the record. On real business need: this is measured, not speculative — a real zh-CN-first release is blocked by it and duly's own gate names the gap. On long-term soundness: object-scoped addressing with a reader in the same change is strictly the sound version of what was retired, and it adds no consumer-side tolerance. On making AI-written metadata hard to get wrong: the key face is one key, every neighbouring spelling that would translate nothing is refused with guidance naming the right home, and the two tombstones now route authors to a live key instead of a dead end — a net reduction in ways to get this wrong. On startup scope discipline: this is not a capability expansion, it is closing a hole in a shipped capability with zero new authoring surface beyond one optional string. The only axis pulling toward B is the paper trail, and that is cheap to add later without touching this code. ⛔ Do NOT read this as a request to hold the PR: it is a ratification question, and C is listed only for completeness."
        }
      ],
      "out_of_scope_findings": [
        "filed as #14376: i18n-extract scaffolds none of the three new key families, so `os i18n extract` will not offer them and `check:i18n-coverage` cannot see them — third time this exact follow-up has been needed (#11485, #13109), so the card also asks whether the fix should be structural rather than a fourth manual walk. Blocked-by #14253; kept out because it moves nine generated bundles and the coverage baseline.",
        "filed as #14377: the published skill `skills/objectstack-i18n` teaches that validation messages are not a translation group — false once this lands. `skills/**` is a governed surface, so it may not ride a mixed diff; the card carries the net-line-budget warning and flags a collision with the open DIET card #14315 on the same file.",
        "filed as #14378 (label `finding`): `rest-server.ts:2963` restates the translatable-type set by hand inside `translateMetaItem`, and this change makes that parenthetical short by one. Left alone deliberately — a one-word comment edit is not worth the conflict risk in that file. The card also records that the similar sentence in `conversions/registry.ts:3039` should be LEFT as it is, being the historical rationale for a 17.0.0 retirement."
      ]
    }

    Generated by Claude Code

  8. 1 remaining item

  9. added
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    on Sep 2, 2026
  10. claude commented on Sep 2, 2026

    @claude
    Contributor

    Reconcile by the domain:spec seat (session session_01GDA48PuRFrHyRfdkBz8m21, seat post #6017): this card closed at 2026-09-02T05:28:43Z when PR #14381 merged (maintainer-directed session session_01SqkTcrxUFci7nqXdbBSe2p, claim comment 5502578491), but its state labels stayed on. Stripping pm:dispatched (a state label has no reader on a closed card; the half-state patrol counts it as an open dispatch) and pm:blocking (a derived cache; its downstream cards unblock on this closure). domain:spec / priority:p1 stay — ownership is not state. No other change.


    Generated by Claude Code

  11. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    on Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions