Repository navigation
fix(security): runtime strings state each decision in words instead of a tracker number (stage 5) - #21518
Conversation
…f a tracker number (stage 5, wip) Rewrites the plugin-security refusals, explain details, field help and log lines that cited a tracker number so each one says what was decided. Text only. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
… help carries the shared-vocabulary decision (stage 5, wip) Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…ld help (stage 5, wip) Written by node scripts/check-i18n-bundles.mjs --write --filter=plugin-security. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…s (stage 5, wip) Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…ger; plugin-security goes to zero (stage 5, wip) Shrink only: 49 lines deleted, 0 added; no other entry moves. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
Brings in stage 4 of the services lane and the plugin-security changes landed since the branch was cut. Only the prose-id ledger conflicted; it is recomputed with --census-ledger on the merged tree (shrink only: the plugin-security entries go to zero, no other entry moves). Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 10 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7ed7a07f92edeaacbd12aa74ed2c203f06128ee4 && git checkout 7ed7a07f92edeaacbd12aa74ed2c203f06128ee4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 529d9711fb1403221c2d5d641b018dda21aec5e1 cb919dc030a456c96986cfb0cb72f59dfbeb5bc3 && git checkout -B drift-repro 529d9711fb1403221c2d5d641b018dda21aec5e1 && git merge --no-ff cb919dc030a456c96986cfb0cb72f59dfbeb5bc3
node scripts/docs-audit/affected-docs.mjs --json 529d9711fb1403221c2d5d641b018dda21aec5e1
|
…n in words instead of a tracker number (stage 6) (objectstack-ai#21533) Part of objectstack-ai#20751 Clause-②: no **Stage 6 of the `domain:services` lane under the maintainer's A / A ruling (5902360492): the `plugin-sharing` strings and the one `plugin-audit` string left over from stage 4.** The card stays open for the later stages, so this PR carries no closing keyword. Text only: no status, error `code`, field, route, export or control flow moves (the AST skeleton reads SAME for 6 of 6 changed `.ts` files, below). ## What this does Eight strings in these two packages sent the reader to a tracker number for the reason behind them. In form D, as stages 1 to 5 applied it, the number goes. Where the sentence already said what was decided, only the citation goes. Where it leaned on the number, it now says the decision in words. All 8 ledgered occurrences in this stage's surface (claim `5962584533`), re-derived from the ledger on `origin/main` at `aa463223` and read again, the same eight, on `ad7c3518` before merging: `plugin-sharing` 7 (7 pairs, 3 files: `share-link-service.ts`, `sharing-rule-service.ts`, `sharing-service.ts`) and `plugin-audit` 1 (`audit-writers.ts`, the entry stage 4 left for a later stage). The file excluded at dispatch, `plugin-sharing/src/translations/serving-seam.test.ts` (PR objectstack-ai#21480, since merged), carries no ledger entry and is not touched by this diff. ### Rewritten in words Author- and administrator-visible text first, log lines last. Line numbers are at the head. | Where | Cited | The text now says | Decision read from | |---|---|---|---| | `plugin-audit` `audit-writers.ts:972-976`, the missing-table fix in the audit write-failure line | 5226 | "so on a fresh `os dev` boot the table exists in that sibling file and not in the primary one; look there before concluding it was never created" | 5226's dev measurement and the PM review: the "table never created" premise was falsified; on a fresh `os dev` boot `sys_audit_log` was in the sibling `dev.telemetry.db` (with its rows) and absent from `dev.db`, because ADR-0057 section 3.6 routes the audit lifecycle class to the telemetry datasource | | `sharing-service.ts:312`, the reason the record-share orphan sweep prints in its "revoked N rows" line | 5103 | "every share on a deleted record goes, whatever its source, so a reused record id cannot inherit it" | the ruling on 5103 (option A): a record's deletion revokes every `sys_record_share` row on it, whatever the source, plus a boot sweep by record existence; the card's hazard is a reused record id handing the old record's shares to the new one | | `share-link-service.ts:67`, the same reason for the share-link sweep | 5190 | "a share link is a bearer token, so a reused record id must not inherit it" | 5190's case and ACCEPT: a share link is an identity-less capability token, so on a reused id whoever holds it wins; a deleted record's links are revoked and resolution fails closed | | `sharing-service.ts:735-741`, the write-gate failure log line (`error`) | 6428 | "a failed lookup is a refusal, never an abstention, because an abstention would hand the row to the other write authorities, which may admit it" | 6428's tri-state write verdict (allow / abstain / deny), added because a two-state answer that merged "allow" with "no opinion" was measured failing open, and its fail-closed rule: a query failure is a deny, never an abstain (an abstain hands the row to another authority, a deny ends it) | | `sharing-service.ts:955-961`, the authored-row-write probe log line (`warn`) | 5493 | "(fail-closed: only an app-authored row-level policy that positively admits this row may lift the sharing refusal)" | the maintainer's ruling on 5493 (Q1 = A, Q2 = A1): the by-id write gate defers only on an `admit` from an app-authored, non-floor policy; abstain, a missing method or a throw leaves the refusal byte-for-byte | | `sharing-service.ts:1835-1842`, the hierarchy-scope log line (`warn`) | 5973 | "\"No org\" is not \"every org\": the IHierarchyScopeResolver.resolveOwnerIds contract makes a resolver fail closed on a missing organization" | 5973 is a pull request (merged as `abeb3751f`; its REST endpoints answer 404, its timeline reads): it made `organizationId` the authoritative, required field and wrote the fail-closed rule into `IHierarchyScopeResolver.resolveOwnerIds` | ### Citation only (the sentence already stated the decision) - `sharing-rule-service.ts:424-429`, the no-active-organization refusal (8158: "manage_sharing is an ORG-scoped capability ... answering unscoped would expose every tenant's rules. Select an active organization and retry. Platform operators ... and system contexts are unaffected"). Its `PERMISSION_DENIED` prefix is unchanged. - `sharing-rule-service.ts:529-533`, the platform-global delete refusal (7795: "requires platform authority ... Org-scoped manage_sharing does not authorize it, because this rule belongs to no organization and deleting it revokes every tenant's grants under it. It remains listable, readable and evaluable"). Its `PERMISSION_DENIED` prefix is unchanged. Every cited card (8: 5103, 5190, 5226, 5493, 5973, 6428, 7795, 8158) was read through REST, body and every comment, before its string was rewritten. 5973 answers 404 on both the issue and the pull endpoint; its timeline and its merge commit `abeb3751f` carry the decision. ### The `OrphanSweepSubject.issue` member The two sweep reasons above are the values of `OrphanSweepSubject.issue`, an exported member that `sweepOrphanedRowsByRecordExistence` appends to its "revoked N rows" warning. The member keeps its name and type (a rename is an export change, outside a text-only stage); only its doc comment changes, from "Issue reference appended to ..." to "Why the rows go, appended to ... Runtime text carries no tracker number, so this states the decision in words", so the published declaration no longer asks a caller for a tracker number. ### Translations None of these strings has a locale variant: they are refusal and log text, not translation keys. ## Ledger (`scripts/doc-authoring-prose-id.baseline.json`) The ledger is serial across every lane's stages, so this PR opened only after stage 5's PR objectstack-ai#21518 merged (`e3ad4922`). `origin/main` at `ad7c3518` (which also carries stage 4, PR objectstack-ai#21472, and another lane's stage, PR objectstack-ai#21521) was then merged in (no rebase). The ledger conflicted; main's copy was taken and regenerated on the merged tree with `node scripts/check-doc-authoring.mjs --census-ledger` (exit 0, no growth refusal). Against `main` the diff deletes 16 lines and adds none: exactly the four file blocks of this stage. A scripted key-by-key comparison of main's copy against the regenerated one reads 4 keys moved, all of them this stage's, each to absent; every other row is unchanged. No other open PR touches the file. | | before (`ad7c3518`) | after | |---|---|---| | `plugin-sharing` | 7 occurrences, 7 pairs, 3 files | 0 | | `plugin-audit` (`audit-writers.ts`) | 1 occurrence, 1 pair, 1 file | 0 | | whole ledger | 42 occurrences, 35 pairs, 10 files | 34, 27, 6 (all `service-analytics`) | `pnpm check:doc-authoring` at the head: "sibling-package prose ids hold the baseline — 26 pinned site(s) across 6 file(s), 86200 string(s) read in 1252 parsed source(s), no growth, no burn-down unrecorded". No gate is added or loosened; `scripts/check-doc-authoring.mjs` is untouched. ## Changeset `.changeset/20751-services-strings-stage6-state-the-decision.md`: `patch` for `@objectstack/plugin-sharing` and `@objectstack/plugin-audit`. Measured after building the merged tree: the new sentences are in each package's built output (`dist/index.js` and `dist/index.mjs` of both). A TypeScript scan of every string literal and template text in the built output finds 0 tracker ids in either package. Control: the same scan reads 34 in `service-analytics`' built output, the count its ledger entries carry. ## Text-only proof A TypeScript-AST skeleton of each changed `.ts` file, where every string literal and template text is a placeholder, a run of adjacent string operands of a `+` chain is one string (only its embedded expressions are kept), identifiers and numbers keep their text, and comments are never read. `aa463223` (where the branch was cut) against the head: 6 of 6 SAME, and the same against `ad7c3518` (main did not touch any of the six files). Controls on scratch copies of `sharing-service.ts`, each mutation's marker counted once on disk first: a one-identifier rename reads DIFF; a text-only change reads SAME; a re-split of one string into two concatenated pieces reads SAME. ## Pins - `sharing-service.test.ts:1616`: the write-gate failure log line is found by "an abstention would hand the row to the other write authorities" instead of the id; the `fail-closed` assertion beside it and the `deny` verdict assertions are unchanged. Reverse check, at the committed `90db7d4e` (the merge did not touch these files), through `scripts/ablation-replace.mjs` under the lock: the new clause put back to the citation form (anchor hit once, blob moved) turned exactly that case red (predicted 1, measured 1 of 131); restored byte-identical to `HEAD` with an empty `git diff HEAD`. - No other test asserts any of the eight strings by their ids. The two dogfood tests that read the no-active-organization refusal match "active organization", which stays. ## Tests All through `scripts/pm/os-verify-lock.sh`, every verdict `VERDICT command-exit 0`, at the head `2f545642` (the merged tree; `pnpm install --frozen-lockfile` first): - Build: `turbo run build --filter=./packages/* --filter=./packages/*/*` (71/71). - `@objectstack/plugin-sharing`: 38 files, 954 tests passed; `@objectstack/plugin-audit`: 38 files, 618 tests passed. - `typecheck` for both, including `check:test-typecheck: OK` for each. - The same suites and typecheck were green at `90db7d4e`, before the merge. ## Gates - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) at `2f545642` (8 paths vs merge base `ad7c3518`, 62 changed lines): 74 commands, run one at a time from the worktree after the full build, each exit code recorded before any pipe; 74 exit 0. `--ran`: "74 derived famil(ies) accounted for — 74 run, 0 NOT-MEASURED (a DERIVED zero — all 74 recorded an exit code and none of them is 3)". The same 74 also ran green at `90db7d4e`, before the merge. - `check-issue-citations`: "no issue citations added against ad7c351 (5 file(s) read)"; `check:i18n`: "OK (9 package(s) — all bundles in sync, no undeclared authoring keys)"; `check:nul-bytes`: OK, 9876 files; `check:type-check-debt`: "none above its recorded number"; `check:dual-build-cjs-loads`: 106 require entry points across 66 packages load; `check:dts-closure`: 71 built packages, 169/169; `check:sourcemap-no-sources-content`: 68 packages, 532 maps; `check:published-files`: 69 publishable packages; `check:engine-double-contract`: OK. - Outside the derived set, all exit 0 at `2f545642`: the eleven declared wide-population families (`check:init-service-contract`, `check:live-db-isolation`, `check:meta-type-normalized`, `check:optional-error-sink`, `check:resume-authority-declared`, `check:route-envelope`, `check:runner-env-posture`, `check:settings-bind-window`, `check:startup-registry-verdict`, `check:verify-stand-in`, `check:wildcard-fallthrough`), plus `check:durability-log-level` and `check:error-code-casing` (log and refusal text moved; no level or code did). - Lint, narrowed as a measurement: `eslint --no-inline-config --format json` over the 6 changed `.ts` files at `2f545642`: 6 files linted, 0 errors, 0 warnings. `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot move any untouched file's verdict. Repo-wide `pnpm lint` is CI's. - `origin/main` was still `ad7c3518` when this PR opened. ## Acceptance notes Noted, not filed: - `OrphanSweepSubject.issue` now carries a reason in words; its name still says "issue". Renaming it is an export change and belongs to no text-only stage. Carrier: none. - `plugin-sharing` test titles and comments still carry ids (`[objectstack-ai#6428]`, `objectstack-ai#5103`, `objectstack-ai#5190`, ...); test bodies and comments are outside the ledger. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_ Co-authored-by: Claude <noreply@anthropic.com>
Part of #20751
Clause-②: no
Stage 5 of the
domain:serviceslane under the maintainer's A / A ruling (5902360492): theplugin-securitystrings. The card stays open for the later stages, so this PR carries no closing keyword. Text only: no status, errorcode, field, route, export or control flow moves (the AST skeleton reads SAME for 17 of 17 changed sources, below).What this does
Some of the security plugin's strings sent the reader to a tracker number for the reason behind them: refusals, explain details, boot warnings, log lines, and the
managed_byfield help onsys_permission_set/sys_position(with theires-ES,ja-JPandzh-CNvariants). In form D, as stages 1 to 4 applied it, the number goes. Where the sentence already said what was decided, only the citation goes. Where it leaned on the number, it now says the decision in words.The stage covers all 43 ledgered occurrences in
plugin-security(claim5960178324). They were re-derived from the ledger onorigin/mainat94a8761a8, and again on the merged tree atcb919dc03(withorigin/mainf9a8eb889merged in): 43 occurrences, 27 (file, id) pairs and 11 files, in 38 string sites. The card's table reads 43, so it matches. None of them sits in the excludedshare-link-tenant-wall.test.ts(a test file, which the ledger does not read), and that file is not touched. The two PRs that landed inplugin-security/src/after this branch was cut (21488 and 21503) add no tracker-number string: the census of the merged tree finds 0 sites in the package.Rewritten in words
Author- and administrator-visible text first, log lines last. Line numbers are at the head.
security-plugin.ts:7056, the curated capability-name refusal (403 PERMISSION_DENIED)sys_capabilityrow with a name inPLATFORM_CAPABILITY_NAMES, or rename one to it), plus option 1 for installations that already collide.delegated-admin-gate.ts:624,:625, the two delegation-anchor refusalsobjectstack-ai/cloudanswers 403 to this session, andadd_repowas refused. The text states the behaviour the code enforces and what its own docblock records (delegated-admin-gate.ts:600: that card madebusiness_unit_idthe read-scope depth anchor, so "anchoring only narrows, never widens").objects/sys-permission-set.object.ts:336,objects/sys-position.object.ts:288, themanaged_byfield help, and the generatedenleavesmanaged_byis unified onsys_capability's select tri-state, and legacy values are mapped (system to platform, config to package, user to admin).bootstrap-system-capabilities.ts:758, the derived-capability boot warningmanaged_by, and a row that is "not provably ours" is left alone.bootstrap-system-capabilities.ts:749,:753, the two remediation tails of that warningsecurity-plugin.ts:2216, the public-form strip warningowner_id,organization_idor audit column is stripped at the data layer.security-plugin.ts:5148,:8279, thecontrolled_by_parentwrite-gate and master read-scope failurescontrolled_by_parentchild follows its master's ownership and share grants, on both reads and writes, and a sharing failure denies on both.security-plugin.ts:8252,:8261, the two chain guardssecurity-plugin.ts:5248, the row-level write gate's sharing-verdict failureallowreplaces only the platform's own ownership floor.security-plugin.ts:5448, the authored-policy verdict failureadmitonly when an app-authored policy matches, andabstainin every other case, a throw included.security-plugin.ts:5506, thegetReadFiltersharing-scope failuresecurity-plugin.ts:5544, the on-behalf-of read-scope refusalsecurity-plugin.ts:7672, the platform-owner wall-bypass audit lineOS_PLATFORM_OWNER_EMAILaccount; writes keep the ADR-0123 D2 refusal.cleanup-package-permissions.ts:195, the uninstall linepackage_idso authorization lapses at once), and ADR-0090 D5's "No ghost grants".unresolved-posture.ts:229,:233,:242, the three fail-closed log linesnormalize-managed-by.ts:190, the normalizer lineCitation only (the sentence already stated the decision)
explain-engine.ts:1409,:1868,:1874(4647): "the same bypass the write path consults", "the write path consults this SAME bypass", "a write bypass requires Modify All Data (modifyAllRecords), so ownership and sharing still decide". These already say what 4647 ruled (option A: explain and the write path share one bypass predicate, and View All Data alone never bypasses a write).unresolved-posture.ts:197,:202,:207(3545): each already says the access "fails CLOSED rather than defaulting to public/uncontracted".security-plugin.ts:1037,:1641,:1648, the org-scoping entitlement refusal and the two arming lines (12699). The refusal'sproblemtext already says the key is refused and resolves to "never declared" (fail closed). The arming lines already say "Layer 0 does not wall them on THIS deployment" and "membership-driven grants hand out organization_admin_no_bypass". Their[security/+ number prefix becomes[security].security-plugin.ts:8915, the invalidmaskingRulewarning (8993): it already says "applying a full mask (fail-closed)" and names the closed preset set and{keepHead, keepTail}. The prefix changes as above.security-plugin.ts:2104, the "registered security service" line (3544, 3547, 5493, 7616): the method list in the same line names what each card added (canExport,getReadableFields,checkAuthoredRowWrite,resolvePermissionSetsForContext). The ADR references stay.permission-evaluator.ts:511,:558(2565): "falling back to bootstrap/db sources" and "unresolved sets grant nothing this request" are the card's fix (warn, and stay fail-closed).Every cited card that could be read was read through REST, body and every comment, before its string was rewritten. That is 22 cards: 8552, 5876, 2747, 4647, 2920, 2565, 12699, 3544, 3547, 5493, 7616, 3022, 5386, 5492, 4467, 2852, 12974, 11082, 8993, 3545, 10401 and 10424. 11082's issue endpoint answers 404 today, so its comments were read from the timeline endpoint. cloud 830 could not be read (see the table).
Translations
managed_byhelp on both objects: thees-ES,ja-JPandzh-CNleaves are hand-written translations that never carried the number or the decision (they translate the value list only). Each now carries the same decision, using each locale's own object labels: "en el único vocabulario platform / package / admin que comparten capacidades, conjuntos de permisos y puestos", "ケイパビリティ・権限セット・ポジションで共通の platform / package / admin の語彙" and "能力、权限集与岗位共用同一套 platform / package / admin 取值". No key was added or dropped.node scripts/check-i18n-bundles.mjs --write --filter=plugin-securityrewrote the twoenleaves from the object source. Noenleaf and no source-hash digest was hand-edited. The fourmanaged_bylocale leaves have no digest entry (they are legacy-trusted), and the merge kept every hand-written value.Tests
Four assertions in three test files held the old text. Each now holds the new substance, and no
codeorstatusassertion is touched.metadata-outage-unresolved-cause.test.ts: the assertion that the outage explain detail contains the 3545 citation now assertstoContain('rather than defaulting to public/uncontracted'). The assertion that all three log lines contain the fail-closed suffix with the 3545 citation now assertstoContain('fail-closed: an unreadable posture never defaults to public').default-report-sink.test.ts: the assertion on the fail-closed suffix with the 2852 citation now asserts the new delegated-read clause.deployment-platform-global-exemption.test.ts: the "nothing to refuse means nothing to warn about" filter matched the number in the refusal warn's prefix. It now matches'org-scoping entitlement key', the refusal warn's own words. The junk-declaration cases in the same file are its positive control.Reverse check at the committed head: both sources were put back to
origin/main, then restored withgit checkout HEAD; the restore was proven by blob hash and an emptygit diff HEAD. I predicted 3 red and measured 2: the log-line pin and the delegated-read pin. The outage explain-detail pin stayed green because that sentence's substance was already there; only its citation went.At the merged head
cb919dc03, throughscripts/pm/os-verify-lock.shafter a full build (turbo build, 71/71):pnpm --filter @objectstack/plugin-security test:Test Files 164 passed (164),Tests 3527 passed | 45 skipped (3572), exit 0.pnpm --filter @objectstack/plugin-security typecheck: exit 0. This includescheck:test-typecheck: OKovertsconfig.test.json, so the re-pinned test files are compiled.In the published surface,
dist/index.jsanddist/index.mjscarry the new sentences. No dist file carries the old 3545 fail-closed suffix, the 12699 log prefix or the cloud 830 anchoring parenthesis.Ledger (
scripts/doc-authoring-prose-id.baseline.json)cc0786223).origin/main(f9a8eb889) was merged in, with no rebase. Only this file conflicted, and the conflict was resolved by taking main's copy and regenerating it, never by hand.node scripts/check-doc-authoring.mjs --census-ledgeron the merged tree. Against main's copy: 49 lines deleted, 0 added. The 11plugin-securityfiles leave the ledger (43 occurrences, 27 pairs), and no other entry moves.--census-ledgerrefuses growth, and it refused nothing.check:doc-authoring: "sibling-package prose ids hold the baseline — 34 pinned site(s) across 10 file(s), 86188 string(s) read in 1252 parsed source(s), no growth, no burn-down unrecorded".Text only
The AST skeleton compares
origin/main(f9a8eb889) with the head. Strings become placeholders, a+chain of strings counts as one string, template expressions are kept, identifiers and numbers are kept, and comments are not read. It reads SAME for all 17 changed.tsfiles. Controls on a scratch copy ofsecurity-plugin.ts, with each mutation counted once on disk: an identifier rename reads DIFF, a text-only change SAME, and a template re-split SAME. The changed lines are byte-identical before and after the merge (the-U0+/- line sets against94a8761a8and againstf9a8eb889are equal).Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsatcb919dc03derives 74 commands. All 74 were run one at a time from the worktree, with exit codes recorded before any pipe, and all 74 exit 0.--ran: "✓ dispatch-gates --ran: 74 derived famil(ies) accounted for — 74 run, 0 NOT-MEASURED (a DERIVED zero — all 74 recorded an exit code and none of them is 3)."check:i18n("all bundles in sync, no undeclared authoring keys"),check:i18n-stale-fill("no new stale fills, 0 baselined"),check:nul-bytes,check:published-files,check:dts-closure(169/169), andcheck:dual-build-cjs-loads(106 require entry points across 66 packages load).eslint --no-inline-config --format jsonover the 17 changed.tsfiles atcb919dc03reports 17 files linted (none ignored), 0 errors and 0 warnings.eslint.config.mjsenables no type-aware linting (noparserOptions.project), so this diff cannot change the verdict on any untouched file. The repo-widepnpm lintis CI's.Acceptance notes
plugin-sharing'ssharing-service.tscarries the same 5493 citation in its own abstain line. That is theplugin-sharingstage of this card, and this PR leaves it alone.managed_byleaves changed although they never carried the number, so that each locale carries the same decision asen.Generated by Claude Code