Skip to content

feat(spec,cli): positions and permission sets are translatable — positions and permissions in the translation bundle - #25701

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-25652-translation-positions-permissions
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-25652-translation-positions-permissions

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #25652

Clause-②: yes (widening)
Metadata-protocol: added
Ruling-ref: none; triage first grade 6385078248 (a protocol addition under the standing order, p1)

What

A translation bundle can now carry positions and permission sets:

positions: { srm_legal_counsel: { label: 'Legal Counsel' } },
permissions: { srm_contract_reviewer: { label: 'Contract Review', description: '…' } },

Before, the same en entry in a Chinese-authored app (hotsrm#22) failed objectstack validate with Unrecognized key(s) on this locale of the translation bundle: positions, so position and permission-set labels stayed in the authoring language while every object, app and flow label beside them was translated, and os i18n check --strict had nowhere to report the gap.

  • @objectstack/spec (system/translation.zod.ts): two new groups on the shared per-app shape, so the bundle entry (TranslationData), the registered translation item and the platform bundle all accept them. Each is keyed by the item's name and closed at label / description (strict: any other key is refused; name / title are refused with a message pointing at label, and position / permission / permissionSets with a message pointing at the groups). The group for permission sets is spelled permissions, as the stack spells the collection.
  • @objectstack/spec (system/i18n-resolver.ts): translatePosition and translatePermissionSet, registered in METADATA_DOCUMENT_TRANSLATORS. TRANSLATABLE_METADATA_TYPES is derived from that table and @objectstack/rest reads it, so GET /meta/position and GET /meta/permission (list and item, both transports) are now served in the request's locale. These two lists are what the console's position-ref and permission-ref pickers draw. A key the bundle does not carry leaves the authored string, and an untouched document keeps its reference.
  • @objectstack/spec (liveness ledger): translation/positions and translation/permissions were added as live, with drilled label / description children. They were seeded together with their reader (the datasets precedent), never declared first and wired later.
  • @objectstack/cli: os i18n extract walks positions[] and permissions[] (walkSecurityCatalog), and os i18n check / os lint coverage report a gap in each group's own bucket (i18n/missing-position, i18n/missing-permission). Every key is recorded with pushOptional, so a string nobody wrote is never required to have a translation.
  • @objectstack/lint: translation-target-unknown resolves each positions.NAME key against the stack's positions[] names and each permissions.NAME key against its permissions[] names, sibling packages' declarations included. An unknown name is an error with the nearest declared name; the audience anchors everyone / guest always resolve. os explain translation-target-unknown names both legs.
  • scripts/fixtures/i18n-walk-parity/every-group.stack.json: authors one member of each new group, so check:i18n-walk-parity sees both walked.
  • packages/runtime meta-list projection census: permission / permissions joined the list axis, and position / permission joined the item axis. The census requires every translatable type to be a cell. The reference-moves block pins the zh-CN label on both reads.
  • examples/app-crm, examples/app-showcase: zh-CN translations for their 5 + 19 positions and permission sets. The new walk counts them, and without these check:i18n-coverage grew 101 → 109 (crm) and 320 → 349 (showcase). The fix was to translate them, not to raise the ratchet.
  • Docs: the group lists in protocol/kernel/i18n-standard.mdx and ui/translations.mdx now include the new groups. They also include the missing picklists / picklist, which those same sentences claimed exhaustively. The leg table in i18n-standard.mdx names the two new translation-target-unknown legs.

Acceptance notes

  • Deviation from the claim's file surface (and the reason): the claim scoped the spec half to the schema, typegen and CLI coverage, with the overlay left to objectui. Shipping the schema alone would have declared a key that parses clean and translates nothing until a downstream card lands. That is the declared-not-enforced shape this file pays to remove (Prime Directive chore: version packages #10, the datasets precedent in the liveness ledger). The console's pickers already read /meta/position and /meta/permission, so the one-line registration in the translator table is the overlay for those two surfaces, and the runtime census had to learn the two cells. Anywhere else the console renders a position or permission-set label from another source still belongs to objectui's card.
  • @objectstack/lint: translation-target-unknown resolves positions.NAME / permissions.NAME against the declared names (anchors everyone / guest always resolve); an unknown name is an error. Added in patch round 1, after the dev reported the silent orphan as a finding.
  • No ! in the title and no (narrowing) arm, so no full dogfood run is owed.

Verification

Patch round 1, at head 5ec0b745f (touches only packages/lint, the changeset and the docs): @objectstack/lint test 161 files / 7274 tests, typecheck exit 0; dispatch-gates --commands 129 families, all exit 0, --ran 0 NOT-MEASURED, 0 UNRUN. Ablation: replacing if (known.has(name)) continue; with continue; failed 2 of the 5 new tests, then was restored blob-identical.

The package suites ran on 8ec6209f. The only commit after it, the final head f444163b7, changes the two example bundles and nothing else. Every result below is exit 0:

  • @objectstack/spec: test 21786 passed, test:repo 1269 passed, typecheck passed.
  • @objectstack/cli: unit project 4821 passed, typecheck passed.
  • @objectstack/rest: test 6406 passed, test:repo 280 passed.
  • @objectstack/runtime: test 7080 passed, test:repo 908 passed, typecheck passed.
  • @objectstack/lint (7269 passed), @objectstack/platform-objects (1352 passed) and @objectstack/metadata-protocol (21475 passed): test.
  • example-crm / example-showcase typecheck passed on f444163b7.
  • dispatch-gates --commands at f444163b7 derived 129 families. Each was run with its exit code captured before any pipe, and all 129 exited 0. --ran reports 129 run, 0 NOT-MEASURED, 0 UNRUN.
  • check:generated --fix regenerated api-surface, export-origins, references, the strictness counts and the liveness counts.
  • Ablation (each run committed first, then restored to the HEAD blob with git diff HEAD empty):
    • Deleting position: translatePosition, turns i18n-security-catalog.test.ts red (2 of 10 failed).
    • Deleting the walkSecurityCatalog(config, out); call turns i18n-security-catalog-walk.test.ts red (5 of 5 failed).
  • Reverse type check: a permissionSets key under positions.sales_rep in the crm bundle fails tsc with TS2353 on the rebuilt .d.ts, then restores.

Body edited by the domain:spec seat 2 after patch round 1, as the dev proposed in 6388798472, plus the lint bullet and the round's readings.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01AwcSGXmnoviawmFu9pGgNZ

Claude-Session: https://claude.ai/code/session_01AwcSGXmnoviawmFu9pGgNZ
Co-authored-by: Claude <noreply@anthropic.com>
…ctness and liveness counts

Claude-Session: https://claude.ai/code/session_01AwcSGXmnoviawmFu9pGgNZ
Co-authored-by: Claude <noreply@anthropic.com>
…n sets into zh-CN

The new walk counts them; without these, check:i18n-coverage grows 101 -> 109 (crm) and 320 -> 349 (showcase).

Claude-Session: https://claude.ai/code/session_01AwcSGXmnoviawmFu9pGgNZ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/lint, @objectstack/spec, touching 23 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/authorable-surface/system.json, packages/spec/export-origins/system.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/data-modeling/objects.mdx (via permissionSets (symbol, a field of interface Universe))
  • content/docs/permissions/authorization.mdx (via permissionSets (symbol, a field of interface Universe))
  • content/docs/permissions/capabilities.mdx (via permissionSets (symbol, a field of interface Universe))
  • content/docs/permissions/delegated-administration.mdx (via permissionSets (symbol, a field of interface Universe))
  • content/docs/permissions/explain.mdx (via permissionSets (symbol, a field of interface Universe))
  • content/docs/permissions/permission-sets.mdx (via permissionSets (symbol, a field of interface Universe))
  • content/docs/permissions/positions.mdx (via permissionSets (symbol, a field of interface Universe))
  • content/docs/permissions/profiles.mdx (via permissionSets (symbol, a field of interface Universe))
  • content/docs/protocol/kernel/i18n-standard.mdx (via TranslationDataSchema (symbol, a top-level const), globalActions (literal, a string literal in PlatformTranslationDataSchema; a string literal in TranslationDataSchema; a string literal in TranslationItemSchema), metadataForms (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY), settingsCommon (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY))
  • content/docs/ui/translations.mdx (via globalActions (literal, a string literal in PlatformTranslationDataSchema; a string literal in TranslationDataSchema; a string literal in TranslationItemSchema), metadataForms (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY), settingsCommon (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via globalActions (literal, a string literal in PlatformTranslationDataSchema; a string literal in TranslationDataSchema; a string literal in TranslationItemSchema))
  • content/docs/releases/v17/17-0.mdx (via permissionSets (symbol, a field of interface Universe))
  • content/docs/releases/v17/17-7.mdx (via globalActions (literal, a string literal in PlatformTranslationDataSchema; a string literal in TranslationDataSchema; a string literal in TranslationItemSchema))
  • content/docs/releases/v18/18-0.mdx (via permissionSets (symbol, a field of interface Universe))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/authorable-surface/system.json, packages/spec/export-origins/system.json, …) — pages documenting those are invisible to this run
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 151 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b1790841bb0bf2c7c696e858de51e8105704471a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c5a5bbe05cdc2b19aacce1cbd75c0102fe2dadb0 — the merge of head 5ec0b745fd6c34ec295b1d97bfb25399366e69c4 into base b1790841bb0bf2c7c696e858de51e8105704471a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c5a5bbe05cdc2b19aacce1cbd75c0102fe2dadb0 && git checkout c5a5bbe05cdc2b19aacce1cbd75c0102fe2dadb0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b1790841bb0bf2c7c696e858de51e8105704471a 5ec0b745fd6c34ec295b1d97bfb25399366e69c4 && git checkout -B drift-repro b1790841bb0bf2c7c696e858de51e8105704471a && git merge --no-ff 5ec0b745fd6c34ec295b1d97bfb25399366e69c4

node scripts/docs-audit/affected-docs.mjs --json b1790841bb0bf2c7c696e858de51e8105704471a

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #25652 · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T20:16Z · head 5ec0b745fd · report 6388001086 and addendum 6388798472 (on #25652; cloud session session_01AwcSGXmnoviawmFu9pGgNZ) · seat review at TIER_DEFAULT. Clause-②: yes (widening) and the path limb hits (packages/spec/src/system/**), so a same-shape ## Contract review PASS at CONTRACT_REVIEW_TIER on the landing head is owed. needs:contract-review is added in this stroke.

  1. Shape. Draft, base main, first line Fixes #25652. The Clause-②: yes (widening) and Metadata-protocol: added lines are line-initial in the body and the changeset (@objectstack/spec, @objectstack/cli and @objectstack/lint, all minor). Ruling-ref points to triage's first grade 6385078248: a protocol addition under the standing order, not a decision card. 24 files, +734/−23. check-governed-merges --pr 25701: NOT governed, 0 of 24 paths. No content/docs/releases/**.
  2. The grade, measured on the diff. appTranslationDataShape gains positions and permissions, each a strictObject closed at label / description and keyed by the item's name. The bundle entry, the registered translation item and the platform bundle share that shape, so all three accept them. The aliases only steer the refusal's "did you mean"; the key is still refused. The changeset now says exactly that.
    • Translators: translatePosition / translatePermissionSet are registered in METADATA_DOCUMENT_TRANSLATORS. TRANSLATABLE_METADATA_TYPES derives from that table, so /meta/position and /meta/permission are localized on both transports.
    • CLI: walkSecurityCatalog uses pushOptional, and i18n/missing-position / i18n/missing-permission each get their own bucket.
    • Lint (patch round 1): translation-target-unknown resolves positions.NAME against positions[] and permissions.NAME against permissions[], sibling packages included via artifactProvidedRecords, with the audience anchors folded in. An unknown name is an error, the same verdict as the picklist list face.
  3. Changeset, checked line by line. The strictness wording matches the strictObject. "Served in the request's locale" matches the translator registration. The lint bullet matches the new leg and its anchor fold. "A key the bundle does not carry keeps the authored string" matches the translators' miss branch, pinned in i18n-security-catalog.test.ts.
  4. Gates. ci-failure --pr 25701 GREEN: 42 check-runs on 5ec0b745fd, all completed, none failed. Local, per the report and addendum:
    • 129/129 derived exit 0 at both f444163b7 and 5ec0b745f; --ran 0 NOT-MEASURED, 0 UNRUN.
    • spec 720 / 21786 and test:repo 79 / 1269; cli 329 / 4821; rest 320 / 6406 and test:repo 5 / 280; runtime 441 / 7080 and test:repo 3 / 908; platform-objects 95 / 1352; metadata-protocol 263 / 21475; lint 161 / 7274 on the final head.
    • Three ablations, each restored blob-identical: the position translator (2 of 10 red), the CLI walk (5 of 5 red), the lint leg (2 of 5 red). The reverse type check (TS2353) passed.
  5. Open questions.
    • (1) Keep the server translators past the claim's scope line, A. Declaring the groups without a reader would ship a key that parses and translates nothing, the declared-not-enforced shape Prime Directive chore: version packages #10 forbids. The datasets precedent seeds the liveness row live with its reader. The console pickers already read these two /meta lists. The claim's file surface is amended in #25652 to match.
    • (2) An objectui card for the remaining render sites, A. The seat files it at this PR's merge, scoped to wherever objectui shows a position or permission-set label from a payload other than /meta/position / /meta/permission (approval steps, sharing and assignment UIs), measured against the pin then current.
  6. Deviations and boundaries.
    • Continuing past "stop on breach" is accepted for the reason in (1). The dev named it in the report rather than doing it silently.
    • mcp_calls 0; relay writes 3.
    • Boundary, for the contract review: platform permission sets get no fold in the new lint leg, the same posture as the picklist leg. A stack bundle that translates a platform-declared permission set by name is reported as an orphan error. Measured producers: HotCRM 8a14b56 has no positions / permissions translation group, and the examples pass the gate. No carrier.
    • The stale RECORDED_DECLARED / RECORDED_WALKED sample in scripts/check-i18n-walk-parity.mjs is noted, no carrier. Its header says it is a sample, not a source of truth.
    • The PR body is edited by the seat in this stroke, as the dev proposed.
  7. Landing. Not governed, 757 changed lines. Once a CONTRACT_REVIEW_TIER PASS is on record and every check on the then-current head is green, the seat sends pr_ready + automerge_enable through the relay.

Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5ec0b745fd6c34ec295b1d97bfb25399366e69c4
Local-runs: none
Scope: packages/spec/src/system/translation.zod.ts, packages/spec/src/system/i18n-resolver.ts, packages/spec/src/system/i18n-security-catalog.test.ts + packages/cli/src/utils/i18n-extract.ts, packages/cli/src/utils/i18n-coverage.ts, packages/cli/test/i18n-security-catalog-walk.test.ts, packages/lint/src/validate-translation-references.ts, packages/lint/src/validate-translation-references.test.ts, packages/lint/src/rule-explanations.ts, packages/runtime/src/domains/meta-list-projection-parity.test.ts, packages/spec/liveness/translation.json, packages/spec/liveness/state-counts/translation.md, packages/spec/api-surface/system.json, packages/spec/authorable-surface/system.json, packages/spec/export-origins/system.json, scripts/fixtures/i18n-walk-parity/every-group.stack.json, examples/app-crm/src/translations/crm.translation.ts, examples/app-showcase/src/system/translations/index.ts (prose faces .changeset/25652-translation-positions-permissions.md, content/docs/**, docs/audits/** read, not scope)

① Derived judgments

  • Declaration is right. Clause-②: yes (widening) + Metadata-protocol: added: two new optional groups, positions and permissions, on the shared per-app translation shape (bundle entry, registered translation item, platform bundle). A bundle that parsed before still parses; the strict leaf shape and the alias refusals (name / title → label; position / permission / permissionSets → the groups) touch only keys that were refused as unrecognized before this PR. Ruling-ref: none is correct: triage first grade 6385078248 placed a label-only protocol addition under the standing order (p1), and it is not on the human-ruling floor (no security boundary, no served-set narrowing, no new semantics beyond overlay).
  • Key ships with its reader, not ahead of it. translatePosition / translatePermissionSet are registered in METADATA_DOCUMENT_TRANSLATORS; TRANSLATABLE_METADATA_TYPES is derived from that table and @objectstack/rest reads it, so GET /meta/position and GET /meta/permission (list and item, both transports) are served in the request's locale. The runtime meta-list projection census gained the two cells and pins the zh-CN label on both reads. The liveness rows are seeded live with drilled children and the reader as evidence (the datasets precedent). This is the Prime Directive chore: version packages #10 shape the claim's narrower surface would have violated; the seat amended the claim rather than the PR, which is the right order.
  • Addressing is by the item's own name (Position.name, PermissionSet.name), the key the translators look a served document up by, and the group for permission sets is spelled permissions as the stack spells the collection. A key the bundle does not carry leaves the authored string, and an untouched document comes back as the same reference (pinned).
  • The lint leg closes the silent-orphan hole. translation-target-unknown resolves positions.NAME / permissions.NAME against the declared names, sibling packages included through the artifact carrier (concat disposition), with the audience anchors everyone / guest folded in; an unknown name is an error with the nearest declared name, the picklist list face's verdict. Ablation-pinned (2 of 5 red).
  • CLI coverage reads the same face. walkSecurityCatalog scaffolds the four key shapes with pushOptional, so a string nobody wrote is never demanded; gaps report in their own buckets. The walk-parity fixture authors one member of each group. The ratchets were met by translating the examples, not by raising them.
  • Producers: HotCRM 8a14b56 carries no positions / permissions group; the examples pass the gate; nothing is converted.

② Semver level

minor on @objectstack/spec, @objectstack/cli, @objectstack/lint is correct for Metadata-protocol: added; no !, no BREAKING section owed, no dogfood battery owed. All 54 check runs on head 5ec0b745fd are success or skipped (Test Core 1–6/6, Build Core, Build Docs, Dogfood Regression Gate, Type Check ×5, Lint ×9, Check Changeset ×3, Governed Surface Queue Guard, Spec property liveness, claim gates); none red, none pending.

③ Boundary flags

  • Remaining console render sites. Anywhere objectui shows a position or permission-set label from a payload other than /meta/position / /meta/permission (approval steps, sharing and assignment UIs) is still the authored string. The seat files the objectui card at this PR's merge (seat 6389043753 open question 2); hold the seat to that filing.
  • Platform-declared permission sets get no fold in the new lint leg, so a stack bundle that translates one by name is reported as an orphan error. Same posture as the picklist leg; a platform set's copy belongs to the platform bundle. Noted, no carrier.
  • Stale sample in scripts/check-i18n-walk-parity.mjs (its header says it is a sample). Noted, no carrier.

Implemented-by: session_01AwcSGXmnoviawmFu9pGgNZ
Reviewed-by: session_01Bf4vhia9fHNXpqC1PCfxie

VERDICT: PASS

needs:contract-review removed in this stroke. Seat (domain:spec 2): pr_ready + automerge_enable when ready.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 24, 2026 21:15
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 24, 2026 21:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 47af3dd Oct 24, 2026
58 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25652-translation-positions-permissions branch October 24, 2026 21:55
os-tesla pushed a commit that referenced this pull request Oct 25, 2026
…ssions groups, at an objectstack pin that declares them (objectui#13478) (#13485)

<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DSu9uoz6bLHskQmxnyBE86T)_

Fixes #13478
Clause-②: yes
Metadata-protocol: none

objectstack's `Console Pin Gate` is red on main because app-shell no
longer type-checks against objectstack `47af3dde42`
(#25701). That change added the `positions`
and `permissions` groups to the per-app `TranslationDataSchema`, and
`TranslationPreview`'s `CATEGORY_DEFS` is exhaustive over that schema's
shape by `satisfies`. This PR adds the two rows and moves objectui's own
pin to `47af3dde42`, so objectui type-checks against the same spec. Once
it merges, objectstack can move its `.objectui-sha` past the merge
commit (objectstack's own card names this one as its blocker).

## What changed

- **`TranslationPreview.tsx`**: two `CATEGORY_DEFS` rows, in the
schema's order (after `flows`, before `metadataForms`): `positions` with
`Users` and `permissions` with `ShieldCheck`, the icons
`PositionPreview` and `PermissionPreview` already draw. Both are nested
groups (`flat: false`), so each card counts the group's top-level keys
and samples them with their inner key count, as `objects` and `flows`
do.
- **`metadata-admin/i18n.ts`** (the designer table, the only pack these
keys live in, `en` and `zh`):
`engine.translationPreview.category.positions` = "Positions" / "岗位" and
`engine.translationPreview.category.permissions` = "Permission Sets" /
"权限集", the words the same table already uses for the two metadata types.
- **Tests**: `TranslationPreview.categories-11765.test.tsx` gets a
spec-valid fixture for each new group (its first case requires one per
schema group). `adminPreviews.i18n-10862-s2.test.tsx` lists the two
headings among the rendered locale sites and reads coverage over 13
groups (`2/13 (15%)`, was `2/11 (18%)`).
- **Changeset**
`.changeset/13478-translation-preview-positions-permissions.md`:
`@object-ui/app-shell` patch.

## The pin

- `.objectstack-sha` moves from `e649c2d91e` (the 18.1.0 release commit)
to `47af3dde42bb4e77fd92025605b89b4a473295a4`. REST compare
`e649c2d91e...47af3dd`: `status: ahead`, `ahead_by: 51`, `behind_by:
0`; the bump script also reported "(on origin/main)". It is pinned at
the card's floor, not at objectstack main's head: the four commits after
it on objectstack main are left for the next bump.
- Path: `scripts/bump-objectstack.sh
47af3dd --no-commit` (built and packed
the 7 packages), then `pnpm install`.
- What moved besides the sha: three packed manifests in
`.objectstack-pin/` (`core`, `lint`, `spec`), each only `zod` `^4.6.5`
to `^4.6.6` (#25657). The lockfile already
resolves `zod@4.6.7`, so **`pnpm-lock.yaml` is unchanged** and `pnpm
install --frozen-lockfile` exits 0.
- **Why the rows and the pin land together** (the seat's hypothesis; it
held). At `e649c2d91e` the schema declares neither group: `grep -c '^
positions: z.record'` over that commit's
`packages/spec/src/system/translation.zod.ts` reads 0, and 1 at
`47af3dde42`; the same for `permissions`. Under `satisfies`, an extra
key in an object literal is TS2353 "Object literal may only specify
known properties" (probed with this repo's tsc 6.0.3 on a two-key
`Record`). So the rows alone fail at the old pin, and the pin alone
fails as the gate does.

## Clause-② reading (the claim asked for it)

The claim declares `yes` because the seat could not tell whether
`metadata-admin/i18n.ts` reaches app-shell's published entry, and the
two lines above are copied from the claim. Measured on the built
declaration closure (`packages/app-shell/dist`, walking the relative
imports from `index.d.ts`, the only `exports` entry with types):

- The closure holds 182 `.d.ts` files. `views/metadata-admin/i18n.d.ts`
**is** one of them (the positive control), but its face is `t(key:
string, …)`, `tFormat(key: string, …)` and the other helpers: the key
table is not exported, and no key is a type.
- Closure files that carry the string `translationPreview`: **0**.
`previews/TranslationPreview.d.ts` is not in the closure at all.

So the diff widens no published accept set and no public face of
app-shell, and the reading is `no`. The declaration is the seat's, so
the lines stay as claimed.

## Verification (local, this worktree, at the new pin)

- `pnpm exec turbo run build --filter=@object-ui/console...
--concurrency=2` (under the verify lock, 6144 MB heap): 35 of 35 tasks,
including `@object-ui/app-shell`'s `tsc` and the console's vite build,
SDUI manifest and plugin tsc. That is the build the Console Pin Gate
runs, here at objectui's own pin.
- `pnpm exec vitest run packages/app-shell/src/views/metadata-admin/
packages/types/` (under the lock): 980 files passed; 17091 tests passed,
1 skipped.
- The three TranslationPreview test files: 63 of 63. Reverse check: with
the two rows removed from `TranslationPreview.tsx` (on-disk count 0,
back to 2 after the restore), the `categories-11765` file goes red on
its order-and-coverage case and on every per-group case, the two new
groups included.
- Gates, each exit 0: `check:i18n-keys`, `check:i18n-designer-parity`,
`check:i18n-dead-keys`, `check:i18n-drift`,
`check:installed-pin-claims`, `check:spec-symbols`,
`check:icon-record-names`, `check:control-bytes`,
`check:new-line-citations` (0 new), `check-changeset-presence`,
`check-changeset-no-major`, and `eslint` on the four changed source
files.
- `check:spec-floors`: exit 1 with the same two `floor-too-low` findings
(`@object-ui/auth` `MEMBERSHIP_ROLE_EXTERNAL`, `@object-ui/types`
`navItemVisibleDataNeedsDataSource`) that #13472 recorded as
pre-existing on main. This diff touches neither package. The gate runs
on push and schedule with `--cross-check`, which is its measurement, and
that run is green on main `cd70b78`.

NOT MEASURED locally (left to CI): the rest of the workspace `vitest`
shards, `lint` and `type-check` across all packages, and e2e.

## Acceptance notes

- objectui#13477 (the label render of the same objectstack change) is a
different defect; it remains open and nothing here touches it.

- [x] Adds no product-specific widget, dialog, hook or string (see
AGENTS.md › Product-neutral renderer)

Implemented by the ui lane card thread, session
`https://claude.ai/code/session_01Usjyy1hT9FNxZHea9u7drp`; claim on the
card, comment 6391374949.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Usjyy1hT9FNxZHea9u7drp

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Usjyy1hT9FNxZHea9u7drp)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec/i18n: a translation bundle has no key for positions or permission sets, so their labels stay in the authoring language

3 participants