test(sharing): hold the built-in rules table to all three locales (#809) - #1240
Conversation
The two rules over the Built-in sharing rules table read the English page alone, while #790 had already given the related-list rule a per-locale sibling and widened the OWD guard to all three pages. They are now one per-locale block over the existing PAGES ledger. Every fact a row states stays derived from the compiled stack: the roster is `sharingRules` itself, and the object, access level and position cells must be the ones the rule really grants. Only the language is authored -- `ROW_LABEL` for the object column on the Chinese pages (the stack's own label on the English one, which is stronger), and a new ACCESS_WORD map for the Grants column. No per-object, per-locale fact about sharing classification is copied in, so a reclassification stays one edit in one place (#549 is undecided and must not cost three). This is red on main, on purpose: #1096 added `case_unassigned_triage_sharing` and its English row on 2026-08-12 and never touched the Chinese pages, so both list nine of the app's ten rules. The two missing rows are a separate docs correction -- #809 remains open there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzS6NUXo5A5KEwj1KjvPHk
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
…e pages (#1239) The app ships ten built-in sharing rules; both Chinese Sharing & Security pages listed nine. `4a0e1de` (#1096) added `case_unassigned_triage_sharing` and its English table row in one commit and never touched `sharing-and-security.zh-Hans.mdx` or `.zh-Hant.mdx`. The undocumented rule is the app's only grant over records with no owner at all: `service_agent` gets EDIT on open, unowned cases (`record.owner_id == null && record.is_closed == false`). A Chinese-reading admin auditing unassigned case intake was shown a complete-looking nine-row table that omitted a real access grant. Every cell is taken from `src/sharing/case.sharing.ts`: object `crm_case` (工单 / 案件, the word each page already uses), access level `edit` (编辑 / 編輯), position `service_agent`, and the criteria stated in each page's own vocabulary — 负责人 / 負責人 is the Owner term those pages define, 未关闭 / 未關閉 the word their sibling escalation rows already use. The rule name stays English, as every other row does. The rows are appended after `Escalated Cases — Service Director` so the order matches the English page. No sharing rule changed. This unblocks the all-locales guard in PR #1240 (#809), which is red on `main` on exactly this drift and needs no edit. Claude-Session: https://claude.ai/code/session_01WzS6NUXo5A5KEwj1KjvPHk
PM note — the blocker landed. Base refreshed; this PR's red should now clear with zero edits to the guard, which is the whole point.#1239 merged as Verified on against 10 shipped rules in What this PR proved, and why the red matteredThis PR was dispatched on a card that described a dormant coverage gap. It was not dormant: the guard fired on its first run against unmutated ⛔ The red was never something to clear by weakening the guard, narrowing it to English, or exempting a locale. It was the deliverable. Two PRs were kept deliberately separate so that this stayed reviewable: the guard here, the two doc rows in #1241, and the red-to-green transition run in both directions on #1241 against a byte-identical copy of this file ( Recorded because it is the reusable part: a coverage card whose premise is "this gap is dormant" is worth re-measuring before it is treated as low-value work. This one had already fired. Once CI settles I will confirm Generated by Claude Code |
Fixes #809
Description
test/sharing-coverage.test.tshad three doc-reading rules over the sharing-and-security pages. #790 gave the third one a per-locale sibling and widened the Org-Wide Defaults guard to all three locales, but the two that read the Built-in sharing rules table — the roster of what the app widens out of the box, and the object / access / position each row claims — still readSHARING_DOC, the English page, alone. This widens them over the existingPAGESledger so all three pages are held to the same truth.The card filed this as a dormant coverage gap, on a 2026-08-05 row-by-row check that found both Chinese tables clean. It is no longer dormant. Re-measured on
main@966cc23:4a0e1de(2026-08-12, PR #1134 for #1096) addedcase_unassigned_triage_sharingand its English row in one commit and never touched the two Chinese pages — the exact mechanism this card predicted one week earlier. For ten days both Chinese pages have shown nine of the app's ten sharing rules, and the missing one is the app's only grant over records with no owner at all: every holder of the Service Agent position has edit access to open, unowned cases, on the app's own security page.The guard therefore fails on
mainwith no mutation at all, naming both pages and the rule. The two doc rows are#1239, deliberately not in this PR — the dispatch on #809 scoped this totest/sharing-coverage.test.tsonly, because a docs correction and a guard widening in one PR make each other unreviewable. This PR is Blocked by #1239 and cannot go green until it lands; #1239 remains open until then.Type of Change
Related Issues
Fixes #809
Related to #790, #791, #725, #592, #1096
Blocked by #1239 (the two missing doc rows this guard reports)
Changes Made
the built-in rules table lists exactly the shipped sharing rules,every documented rule states the object, access level and position it really grants) and replaced them with one per-locale block overPAGES. Nothing was dropped — the English page is that ledger'senentry, proven by reverse-verification leg D3 below.LocalePagegainsrulesHeading(### Built-in sharing rules/### 内置共享规则/### 內建共用規則).ACCESS_WORDmap — three locales × two access levels (Read/Edit,读取/编辑,讀取/編輯). This is the one genuinely new thing the card predicted. An unmapped access level throws by design, the conventionowdCellandCOUNT_WORDalready use.sharingRules.length, and a per-page "the table still parses" check.Built so a future classification change is one edit
Per the dispatch's warning about #549 (undecided, proposing
crm_contract→controlled_by_parent): no per-object, per-locale fact about sharing classification is copied into this block. Every fact a row states is derived from the compiled stack — the roster issharingRulesitself, and the object, access level and position must be the ones the rule really grants. Adding or removing a sharing rule costs zero ledger edits here. Only the language is authored, once per word: the object column reusesROW_LABEL(the ledger the OWD table already reads) on the Chinese pages and the stack's ownlabelon the English one, which is a derivation and stronger than a ledger. A reclassification movesACCOUNT_CHILD_COVERAGE— one entry, one place — and nothing here moves with it.Testing
npm test) — see the exact status belownpm run lint)npm run build)Gates, at commit
74c2460(the pushed head; the run below is that tree)pnpm verifychains eight. Exit codes captured by redirect, never through a pipe.pnpm validatepnpm typecheckpnpm lintpnpm lint:i18n-gatepnpm hygienepnpm hygiene:tokens✓ source token ratchet cleanpnpm buildpnpm test1 failed | 116 passed (117)filesBoth failures are this PR's new guard reporting #1239, and nothing else in the repo moved:
Also run:
node scripts/check-stackblitz-lock.mjs→ exit0, "package-lock.json is in sync with package.json (v3)". That is CI's first step after install and is not part ofpnpm verify.Token ratchet — before == after, by construction
This branch changes
test/sharing-coverage.test.tsand one.changeset/file; the ratchet measuressrc/**/*.tsonly, andgit diff --name-only origin/main..HEAD | grep -c '^src/'is 0. Measured at74c2460, unchanged frommain:No ceiling raised.
Reverse verification — five directions, each predicted before it was run
Every mutation was applied by an anchored replace that asserts it matched exactly once (a zero-hit
sedexits 0 and reads like a successful ablation), proven on disk with agrep -cof both the injected and the removed text plusgit diff --numstat, and restored under atrap … EXIT INT TERMso a foreground-cap SIGTERM could not leave a mutated tree behind for the next measurement. Restores proven by an emptygit status --porcelain.main2 failed | 45 passed, both namingUnassigned Cases — TriageRetired Partner Portal Sharingrow to zh-Hant2 failed | 45 passed; zh-Hant now reportsrow naming a rule this app does not ship: Retired Partner Portal Sharing编辑→读取on one rowACCESS_WORDis really consulted per locale3 failed | 44 passed:Account Team Sharing: row grants "读取", the rule grants "编辑"Account→Opportunityenrow-facts rule — proves retiring the two English-only rules did not lose English coverage3 failed | 44 passed:sharing-and-security.mdx (en) … row says object "Opportunity", the rule targets "Account"案件→客戶案件— a word onlyROW_LABELsupplies, so it proves the Chinese object column is answered by the ledger and not by the page against itself3 failed | 44 passed:Escalated Cases Sharing: row says object "客戶", the rule targets "案件"2 failed | 45 passed,git status --porcelainemptyEvery failure names the file, the locale and the row, which was the requirement — a guard that fires for real has to be actionable without re-deriving what drifted.
Checklist
.changeset/sharing-rules-table-guard-every-locale.mdAdditional Notes
The three PM mechanism assumptions were measured rather than trusted:
SHARING_DOCalone, exactly as described. But the Chinese tables have drifted since 2026-08-05, so this stopped being a dormant coverage gap and became a live defect. That is the loud part of this PR.PAGESis the right ledger andROW_LABELcovers what you need" — confirmed, no second ledger needed.ROW_LABELcarries客户/客戶,商机/商機,工单/案件,营销活动/行銷活動— the four objects the table names — and they match the pages exactly. Position names are language-invariant inside backticks and needed no ledger at all. Only the Grants column needed something new, and it is the three-line map the card predicted.Account Team Sharing…Live Campaigns — Marketing Director). The count in that assumption is what moved: it is ten rules now, not nine, which is the drift itself.Sanitizer note on the card: #809's body is truncated where it proposes the map — it reads
ACCESS_WORD: Record>, the<…>type parameters having been eaten after the comment was stored. The intent was recoverable from context.Generated by Claude Code