Repository navigation
Commit f90ea67
fix(plugin-security): the position-name fold warning says the same-name grant is removed in the next major (#24988)
Part of #24757
Clause-②: no (a runtime warning's wording plus one added field in its
log payload; no accept set, served shape, resolution result or error
code moves)
This is the card's **18.x half** under the standing rule 「甲」: the
existing `position_name_fold_grant` runtime warning in
`plugin-security`, re-pointed as the loud half of the position-name
fold's retirement. The fold itself stays. Its removal is the card's 19.0
half, and #24757 remains open for it.
## What changed
- **Text.** The warning now says three things. ① Permission set `P` is
in force for holders of position `P` only because the two share a name;
`P`'s definition does not name it in `permissionSets`. ② This name-based
grant is deprecated and is removed in the next major version. From then
on, holders of `P` do not receive set `P` unless the position's
definition names it. ③ The way out: `permissionSets: ['P']` on position
`P` to keep the grant, or a rename if the shared name is a coincidence.
The string carries no tracker id and no version number
(`check:doc-authoring`; the docblock says why).
- **Payload.** `{ event, position, permissionSet }` gains `removedIn:
'next_major'`. The three existing fields are unchanged, because a log
consumer may key on them.
- **Docblocks.** `POSITION_NAME_FOLD_EVENT` and
`reportNameFoldCollisions` now say this is the 18.x loud half under 「甲」
and cite the ruling records. The stale "junction table is the governed
channel" wording is replaced with ADR-0131's definition
`permissionSets`. The "HotCRM NOT MEASURED" boundary note is replaced
with the card's measurement (5 of 12 positions at `f22dfa52`, declared
by objectstack-ai/hotcrm#2200). A "When it fires" section records the
cadence measured below.
- **Unchanged.** The trigger predicate, the once-per-position cadence,
and every resolution result. A holder of `P` still receives set `P` in
18.x, as 要点 5 of ruling B on #13419 requires: 「任何行为差异只能表现为拒绝/告警,⛔
永不静默改变解析结果」.
## 「甲」 reading
- **The rule** (#6015, ruling record `6292005854`), verbatim: "No
conversion can carry it: 18.x ships only the loud half (a warning, an
explain line, a reported event). The refusal is filed for 19.0."
- **Triage's derivation for this card** (`6292072572`), verbatim:
"**18.x:** objectstack-ai/hotcrm#2200 (HotCRM declares `permissionSets`
on its five positions), and the existing runtime warning re-pointed as
the loud half. **19.0:** the retirement itself: the fold is removed, and
the delegation walk, explain and the collision check follow it."
- **Ships in 18.x (this PR):** the warning text and its reported event
(the structured payload with `removedIn`). The rule lists three
loud-half forms; triage's derivation for this card names the warning.
`explain-engine.ts` is fenced out of this claim, so no explain line
changes here.
- **Ships in 18.x, in HotCRM:** objectstack-ai/hotcrm#2200, HotCRM's
half. It declares `permissionSets` on its five same-name positions. That
silences this warning for them (pinned below as the declared-pair case)
and makes Setup's two panels show the binding.
- **Waits for 19.0, on this card:** deleting the position-name request
in `resolvePermissionSetsForContextUnmemoized` and
`dropDeactivatedFoldedSets`, and the follow-ups in the delegated walk,
explain and the collision gate. That half deletes this warning together
with the fold.
## Measured (PM mechanism assumptions)
Every reading below is from `origin/main` `b811c7369`, this branch's
base, by source reading.
- **The warning exists as assumed.** `reportNameFoldCollisions` emits
`position_name_fold_grant` through `logger.warn` with `{ event,
position, permissionSet }`. It reports a pair `(position P, set P)` only
when set `P` resolved, `P` is not among the explicitly requested sets
(the definition's `permissionSets` and direct grants, §6b), and `P` is
not a baseline name. It shipped in 17.3.0 (changeset `936aa2d` in the
package CHANGELOG).
- **Assumed gap confirmed.** The old text was "This grant is in force
and ungoverned: no position declares it. Declare it explicitly … or
rename one of the two." It said nothing about the retirement or what
holders lose.
- **Cadence.** Lazy: it fires on the first resolution that meets an
undeclared pair, never at boot. It fires once per position NAME per
plugin instance (`nameFoldWarned`). That means not per holder and not
per organization, because the key is the name alone. A restart reports
again.
- **Consumers of the payload.** None beyond the log line. `git grep
position_name_fold_grant` finds only the pin and docblock/CHANGELOG
prose. The core logger (`packages/core/src/logger.ts` `write`) spreads
`meta` into the JSON line, so the new field arrives in the log as a
top-level key beside `event`. No new channel, endpoint or gate is added.
## Tests
`packages/plugins/plugin-security/src/position-name-fold-warning.test.ts`,
29 cases (28 before):
- **MUST FIRE, for the census's two cross_scope folds** (`sales_rep`,
`sales_manager`, each bound to `crm_sales_user`): the payload is exactly
`{ event, position, permissionSet, removedIn: 'next_major' }`, and ⛔ the
holder still resolves the same-name set.
- **New: the payload as data.** The exact key set is `event`,
`permissionSet`, `position` and `removedIn`, so a renamed legacy field
fails as loudly as a missing retirement field.
- **The text** pins clauses ① ② ③ above, no
`sys_position_permission_set`, and no `#` followed by a digit.
- **Once per position:** three distinct contexts give one report, and
each request still resolves `sales_rep`.
- **MUST NOT FIRE (unchanged cases):** the 7 inert positions, the
`org_admin` near-miss, the 13 declared bindings, a declared pair
(`permissions: ['sales_rep', …]`, the shape hotcrm#2200 produces), a
baseline-named position, and no positions.
**Ablations**, each through `scripts/ablation-replace.mjs` in WRAP mode,
run against the warning file. For each one the tool's own evidence reads
`ok mutation landed` and `ok restored: blob == HEAD (5db0a1f) and
git diff HEAD is empty`. The plugin's tests import
`./security-plugin.js` from `src`, so no `dist/` leg applies.
| leg | mutation (forbidden behaviour put back) | red |
|:--|:--|:--|
| A1 | drop `removedIn` from the payload | 4 (both MUST FIRE, payload
keys, once) |
| A2 | put back the 17.x "in force and ungoverned" wording instead of
the retirement sentence | 1 (text) |
| A3 | a tracker id in the string | 1 (text) |
| A4 | stop treating explicitly requested sets as declared | 1 (declared
pair) |
| A5 | stop treating baseline names as declared | 1 (baseline) |
| A6 | drop the once-per-position dedupe | 1 (once) |
| A7 | predicate "is this position bound to anything?" | 2 (both
bound-to-another-set folds) |
| A8 | remove the position-name request (the 19.0 removal early) | 5
(every MUST FIRE case) |
| A9 | keep the warning but drop the undeclared same-name set after it
(the refusal shipped early) | 4, each on the grant assertion: `expected
[ 'crm_sales_user', 'member_default' ] to deeply equal [ …(3) ]` and
`expected [ 'member_default' ] to include 'sales_rep'` |
A9's first attempt was a no-op: its replacement contained the anchor,
and the tool refused with "the anchor count moved 1 -> 1" and restored.
The leg was re-run with a replacement that does not contain the anchor.
The table reports the second run.
**Suite and typecheck**, at `63246cedf` (the merge of `origin/main` that
followed touched only `plugin-approvals`, outside this package's
dependency and devDependency closure):
- `pnpm --filter @objectstack/plugin-security exec vitest run
--maxWorkers=2`: 197 files passed, 4078 tests passed, 45 skipped.
- `pnpm --filter @objectstack/plugin-security run typecheck` (tsc, the
scripts project and `check:test-typecheck`): exit 0, with `0 file(s) / 0
error(s) / 0 pinned signature(s)` in `test-typecheck-debt.json`. The
test file is in the `tsconfig.test.json` program (`--listFilesOnly`: 1).
## Gates
These were run locally at `e41097d23`, the head this PR opens on, from
the `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derivation at that head: 69 commands, the
same 69 the PM's reading at `6440cf163` listed. Each exit code was
captured before any pipe.
- **67 exit 0.** Among them: `check:doc-authoring` (sibling-package
prose ids hold the empty baseline), `check:nul-bytes`,
`check:position-name-fold-loaders` and its `--self-test`,
`check:issue-citations`, `check:changeset-no-major` and
`check:adr-0087-registration` (both `--base origin/main`),
`check:empty-changeset`, `check:published-files`, `check:dts-closure`,
`check:test-source-alias`, `check:type-check-coverage` and
`check:type-check-debt`.
- **2 exit 3, PREREQUISITE NOT MET: NOT MEASURED locally.** `pnpm
check:dual-build-cjs-loads` and `pnpm check:i18n` read every package's
`dist/`. This worktree built only `plugin-security` and its dependency
closure. CI's fresh build runs both.
- **Reconciliation.** `dispatch-gates --ran` with the exit-coded record:
"69 derived famil(ies) accounted for — 67 run, 2 NOT-MEASURED (2 DERIVED
from a recorded exit 3)".
- **Roster gates with a roster beside this diff,** read rather than
trusted silent: `check-changeset-fixed.mjs`, `check:authz-resolver`,
`check:error-code-casing` and `check:filter-alias-parity`, all exit 0.
- **Left to CI:** the rest of the farm (`pnpm lint`, the type-check
lanes, Test Core, Build Core).
## Acceptance notes
- **Per-name cadence across organizations (observation, not filed).**
`nameFoldWarned` keys on the position name alone and the payload names
no organization. On a multi-organization deployment, an undeclared pair
`P` in a second organization is not reported until restart once the
first organization's `P` has warned. This is not reproduced through a
public door; it is a source reading. Carrier: #24757's 19.0 half, which
removes the warning.
- **Lazy firing (observation).** The warning speaks only when a holder
of `P` makes a request. A deployment where no such request has happened
since boot has not been told. The readings report on the card noted that
a boot-time pass over the registry would be the cheaper place for this.
That would be a new trigger, outside this dispatch's "keep the trigger
conditions". Carrier: #24757's 19.0 half.
- **Stale cross-reference outside the claimed regions.** The
`nameFoldWarned` docblock links `reportNameFoldCollision` (singular),
which names no member. It is outside this claim's file surface, so it is
left as it is.
- **Explain line.** 「甲」 lists "an explain line" among the loud-half
forms. Triage's derivation for this card names only the warning, and
`explain-engine.ts` is fenced out of this claim, so this PR ships the
warning and its reported event.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent e4d604e commit f90ea67
3 files changed
Lines changed: 170 additions & 53 deletions
File tree
- .changeset
- packages/plugins/plugin-security/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
14 | 30 | | |
15 | 31 | | |
16 | 32 | | |
| |||
19 | 35 | | |
20 | 36 | | |
21 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
22 | 41 | | |
23 | 42 | | |
24 | | - | |
| 43 | + | |
25 | 44 | | |
26 | 45 | | |
27 | 46 | | |
| |||
46 | 65 | | |
47 | 66 | | |
48 | 67 | | |
49 | | - | |
50 | | - | |
| 68 | + | |
| 69 | + | |
51 | 70 | | |
52 | 71 | | |
53 | 72 | | |
54 | 73 | | |
55 | 74 | | |
56 | | - | |
57 | | - | |
| 75 | + | |
| 76 | + | |
58 | 77 | | |
59 | 78 | | |
60 | 79 | | |
| |||
139 | 158 | | |
140 | 159 | | |
141 | 160 | | |
142 | | - | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
143 | 166 | | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
148 | 171 | | |
149 | 172 | | |
150 | 173 | | |
151 | 174 | | |
152 | 175 | | |
153 | 176 | | |
154 | | - | |
| 177 | + | |
155 | 178 | | |
156 | | - | |
| 179 | + | |
157 | 180 | | |
158 | 181 | | |
159 | | - | |
160 | | - | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
161 | 185 | | |
162 | 186 | | |
163 | 187 | | |
164 | 188 | | |
165 | | - | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
166 | 200 | | |
167 | 201 | | |
168 | | - | |
169 | | - | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
170 | 214 | | |
| 215 | + | |
171 | 216 | | |
172 | | - | |
173 | | - | |
| 217 | + | |
| 218 | + | |
174 | 219 | | |
175 | 220 | | |
176 | 221 | | |
| |||
180 | 225 | | |
181 | 226 | | |
182 | 227 | | |
183 | | - | |
| 228 | + | |
184 | 229 | | |
185 | | - | |
| 230 | + | |
| 231 | + | |
186 | 232 | | |
187 | | - | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
188 | 236 | | |
189 | 237 | | |
190 | 238 | | |
| |||
219 | 267 | | |
220 | 268 | | |
221 | 269 | | |
222 | | - | |
223 | | - | |
224 | | - | |
225 | | - | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
226 | 276 | | |
227 | 277 | | |
228 | 278 | | |
| |||
0 commit comments