Repository navigation
Description
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/NAMErather 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
os-warren commentedon Sep 2, 2026
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_01SqkTcrxUFci7nqXdbBSe2pclaude/group-company-task-tracking-astg44(this session's designated branch, restarted fromorigin/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_TRANSLATORSinpackages/spec/src/system/i18n-resolver.tsis the one table, andTRANSLATABLE_METADATA_TYPESis 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 atranslateDatasetplus 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:2342holdsprivate i18nService?: { t? }, and:5603documents 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 insidetranslateView, since a bulk-action def is part of the view document.One PR, three commits, one per surface — they share
translation.zod.tsand splitting them across PRs would just serialise a conflict.Acceptance is measurable from outside
objectstack-ai/dulycarries 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
os-warren commentedon Sep 2, 2026
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_01SqkTcrxUFci7nqXdbBSe2pclaude/group-company-task-tracking-astg44(already checked out in the shared primary checkout, so this seat works from a detached worktree atorigin/mainand pushes with an explicit refspec —git push -u origin HEAD:claude/group-company-task-tracking-astg44. The shared checkout's HEAD is not touched.)origin/main@4fac7a02Re-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 —
bulkActionDefsinsidetranslateView, an_validations.RULE.messagekey shape resolved through the existingi18nServicechannel in objectql (no second i18n path), andtranslateDatasetregistered inMETADATA_DOCUMENT_TRANSLATORSso the derivedTRANSLATABLE_METADATA_TYPEScarries it to the REST boundary. Each surface gets a test that is red without the fix.Generated by Claude Code
huangyiirene commentedon Sep 2, 2026
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 (assigneeos-warren, claim + dev-seat continuation undersession_01SqkTcrxUFci7nqXdbBSe2p, branchclaude/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 nopm:*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'sMETADATA_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-reviewon the PR when it opens; the spec seat or an at-tier reviewer clears it). The_validations.RULE.messagehalf rides the existingi18nServicechannel in objectql, per the claim's own correction — ⛔ no second i18n path.Generated by Claude Code
feat(spec): translate a list view's bulkActionDefs from the bundle
feat(spec,objectql): give a custom validation rule's message a bundle…
feat(spec): translate dataset labels via translateDataset + a dataset…
fix(spec): keep the retired-key guidance free of internal issue ids
feat(rest): localize a saved dataset at the analytics query door too
os-warren commentedon Sep 2, 2026
{ "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
os-warren commentedon Sep 2, 2026
os-dev-report
needs:contract-reviewlabel note below).Since that comment:
needs:contract-reviewapplied 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
1 remaining item
claude commentedon Sep 2, 2026
Reconcile by the
domain:specseat (sessionsession_01GDA48PuRFrHyRfdkBz8m21, seat post #6017): this card closed at 2026-09-02T05:28:43Z when PR #14381 merged (maintainer-directed sessionsession_01SqkTcrxUFci7nqXdbBSe2p, claim comment 5502578491), but its state labels stayed on. Strippingpm:dispatched(a state label has no reader on a closed card; the half-state patrol counts it as an open dispatch) andpm:blocking(a derived cache; its downstream cards unblock on this closure).domain:spec/priority:p1stay — ownership is not state. No other change.Generated by Claude Code
fix(objectql): keep constraint + value on a state_machine refusal tha…
skills(i18n): validation messages are a translation group since #14253
docs(i18n): list the bulkActions and datasets groups in the translati…
docs(rest): drop the hand-written translatable-type list from meta-pl…
docs(i18n): list the bulkActions and datasets groups in the translati…
docs(rest): drop the hand-written translatable-type list from meta-pl…
docs(spec): name the object-scoped bundle key in the validationMessag…
docs(spec): name the object-scoped bundle key in the validationMessag…
docs(lint): re-anchor the dead tracker citations in packages/lint/src…
test(spec): the first system/ file group's test titles state each cit…