Skip to content

fix(plugin-security): the position-name fold warning says the same-name grant is removed in the next major - #24988

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24757-fold-warning-18x
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24757-fold-warning-18x

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 Five positions reach their permission sets only through a same-name match: declare permissionSets on them 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 Permission-set resolution binds by POSITION NAME while sys_position_permission_set sits near-empty — confirm name-based resolution is the intended mechanism, not an accident the empty junction hides #13419 requires: 「任何行为差异只能表现为拒绝/告警,⛔ 永不静默改变解析结果」.

「甲」 reading

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 (5db0a1f3c00a) 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


Generated by Claude Code

…me grant is removed in the next major

The position_name_fold_grant warning is re-pointed as the 18.x loud half of
the fold's retirement under the standing rule 甲: its text now says the
grant is in force only because the names match, that the name-based grant
is deprecated and removed in the next major version (after which holders of
P do not receive set P unless P's definition names it), and the fix. The
structured payload adds removedIn: 'next_major'; event, position and
permissionSet are unchanged. Trigger, cadence and every resolution result
are unchanged.

Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 3 documentable anchor(s).

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

  • content/docs/permissions/field-level-security.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/permissions/index.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/ui/forms.mdx (via SecurityPlugin (symbol, a top-level class))

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

  • content/docs/releases/implementation-status.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/releases/v18/18-0.mdx (via SecurityPlugin (symbol, a top-level class))

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
  • 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 — 16 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 78ab6474ca30bbb7c4400bae548c814da8fa57c1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3eb3e6c98b5a68075780fe8b80969f50ad9e3808 — the merge of head e41097d2334238f2bf3827241b19a89a6eaa5b11 into base 78ab6474ca30bbb7c4400bae548c814da8fa57c1, 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 3eb3e6c98b5a68075780fe8b80969f50ad9e3808 && git checkout 3eb3e6c98b5a68075780fe8b80969f50ad9e3808
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 78ab6474ca30bbb7c4400bae548c814da8fa57c1 e41097d2334238f2bf3827241b19a89a6eaa5b11 && git checkout -B drift-repro 78ab6474ca30bbb7c4400bae548c814da8fa57c1 && git merge --no-ff e41097d2334238f2bf3827241b19a89a6eaa5b11

node scripts/docs-audit/affected-docs.mjs --json 78ab6474ca30bbb7c4400bae548c814da8fa57c1

⚠️ 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 78ab6474ca30bbb7c4400bae548c814da8fa57c1 → 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 · #24757 (its 18.x half) · domain:services seat 2 · os-support-ai · session_01UmqzD97e14iws6hmVhgZ5y · 2026-10-20T22:23Z · head e41097d233. This is the review of record for claim 6301409060. Contract review: not owed. The PR touches no published schema and no governed path, and it declares Clause-②: no with no narrowing arm.

  1. Form:
    • The first line is Part of #24757: the fold's retirement is the card's 19.0 half, so the card stays open. The seat's grep of the body finds no closing keyword beside any issue number, and check-partof-closing-keyword passed on the body.
    • Clause-②: no is at line start in the body and in the plugin-security changeset (patch), with its reason.
    • 3 files, +170/−53.
    • Path face, read with the files endpoint on this head: check-governed-merges --pr 24988 reports 0 of 3 paths governed. Ordinary queue landing.
  2. Premise and fix:
  3. Gates:
    • CI on e41097d233, latest run per check name: 41 checks, 34 success, 7 skipped, 0 failed. Every skip is on the roster in scripts/pm/check-expected-skips.mjs (read from the file): the 18.x upgrade harness, Packed-tarball smoke, the Dogfood Regression Gate matrix, Dogfood Verify CLI, Build Docs, Console Pin Gate and Temporal Conformance. Lint & Repo Gates and TypeScript Type Check are both success.
    • Local: 69 derived; 67 exit 0; 2 NOT-MEASURED (check:dual-build-cjs-loads, check:i18n: exit 3, prerequisite dists absent; CI runs both).
    • The plugin-security suite: 197 files, 4078 passed. Typecheck and check:test-typecheck exit 0.
  4. Accepted deviations:
    • The suite and typecheck ran at 63246cedf, before a one-commit merge of origin/main that touched only plugin-approvals. CI's run on this head is the reading of record.
    • No explain text changed: 「甲」 lists an explain line among the loud-half forms, but triage's derivation names only the warning, and explain-engine.ts was fenced.
    • The warning's provenance (17.3) is cited from the package CHANGELOG, because the checkout is shallow.
    • Prose read sentence by sentence against the diff and the cited code: the changeset's what-it-was / what-changed / why-only-a-warning / unchanged / what-to-do bullets, and the warning text itself.
  5. Out of scope:
  6. Landing: ordinary queue landing. Every check on this head is green or an expected skip, so the seat flips it (pr_ready + automerge_enable) on this record.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 20, 2026 22:24
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 20, 2026 22:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit f90ea67 Oct 20, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24757-fold-warning-18x branch October 20, 2026 22:55
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 size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants