Skip to content

spec(security): refuse a blank AdminScope businessUnit anchor at parse - #19864

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-19461-admin-scope-nonblank-anchor
Sep 24, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-19461-admin-scope-nonblank-anchor

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #19461

Clause-②: yes

Executes maintainer ruling 5793356837 (decision batch #217 item 1, letter A, 「217 同意」): tighten the declaration and leave stored rows untouched. The reading of the changeset rule is in its own section below.

What changed

AdminScopeSchema.businessUnit (packages/spec/src/security/permission.zod.ts) is the delegated-admin scope's one required key. It now refuses an empty value and a whitespace-only value at parse, at the key's own path:

FROM  AdminScopeSchema.safeParse({ businessUnit: '' })     ->  { success: true }
TO    AdminScopeSchema.safeParse({ businessUnit: '' })     ->  { success: false, issues: [ ONE issue:
        code 'custom', path ['businessUnit'],
        message 'A blank businessUnit is not a delegation boundary: businessUnit is the
                 sys_business_unit.name (machine name) of the business unit at the root of the
                 subtree this scope delegates, ...' ] }
  • Non-transforming. The key is z.string().refine(NON_BLANK_STRING, ...), using the shared predicate from shared/refinement-projection.ts. There is no .trim() and no transform: saveMetaItem persists the submitted body verbatim, so a transform would validate one string and store another. A real name, padding included, parses byte-identical.
  • Declared equals enforced. NON_BLANK_STRING is a declared projectable refinement, so the published JSON Schema for security/AdminScope now carries minLength: 1 and a non-whitespace pattern. The .describe() text states the rule, and the regenerated reference page carries it.
  • The absent key is unchanged. It is still exactly one invalid_type issue at businessUnit, because the refinement never runs on a non-string.
  • No export added. The message constant is module-private, like the file's existing refinement helper. AdminScope / AdminScopeParsed types are unchanged.

Stored rows (ruling item 2): not rewritten, refused on the next write, no skip path

I re-checked the consumer trace on today's main (8cbc3c0084), by symbol. Three plugin-security paths re-parse a STORED scope through PermissionSetSchema inside saveMetaItem. The refusal reaches all three through the existing parse, with no consumer edit:

path (in permission-set-projection.ts) what a stored blank anchor now does
boot reconciliation backfill, reconcilePermissionSetProjection the row is counted in backfillFailed and reported through the existing ADR-0094 D4 durability ERROR (first failure carries the 422 naming adminScope.businessUnit; the summary names the record). A valid sibling still backfills.
restore leg, createPermissionSetWriteThrough restore op the engine un-trash runs; the missing definition is reported at ERROR (NOT re-authored into metadata), carrying the same 422.
data-door edit merge, createPermissionSetWriteThrough update op a label-only edit throws 422 INVALID_METADATA with one issue at adminScope.businessUnit, for a legacy record-only row and for a definition already stored in sys_metadata. Nothing is saved.

Reads are unaffected: the rehydration seams do not parse, and metadata-protocol saveMetaItem has no early return before its resolveOverlaySchema(...).safeParse. No path crashes, swallows the error silently or skips the row.

ADR-0087 (ruling item 3)

New semantic entry packages/spec/src/migrations/entries/semantic/18.admin-scope-business-unit-blank-refused.ts, with registry.ts regenerated by gen:migration-registry (not hand-edited). Its prose says plainly that stored rows are not rewritten, have no D2 conversion (the root cannot be inferred), and are refused on their next write. It also says that a clean boot is not a completed sweep, because a definition already stored in sys_metadata says nothing until it is written again.

Changeset, and how I read the rule

.changeset/19461-admin-scope-business-unit-blank-refused.md: @objectstack/spec minor, body carrying BREAKING for authored metadata, the FROM → TO migration table and one-line fix, the ADR-0087 disposition marker registered admin-scope-business-unit-blank-refused, and the Clause-②: yes line.

AGENTS.md's changeset rule: yes takes at least minor; a narrowing is BREAKING, so the changeset must carry its migration and exactly one ADR-0087 disposition marker; major is refused in the launch window (check-changeset-no-major). That is the same shape as the $between precedent (#18012 / PR #19066): Clause-②: yes, minor, a **BREAKING for authored metadata** banner, registered disposition. The breaking signal that check:adr-0087-registration reads here is the banner: it reports [BREAKING] registered admin-scope-business-unit-blank-refused (new here).

⚠️ One reading to flag, not resolved here. The Clause-② line is copied verbatim from the claim and the ruling (yes). scripts/pm/clause2-line.mjs reads a bare yes as a widening and spells a pure narrowing no (narrowing). This diff widens nothing. I did not rewrite the ruling's declaration; the report carries it as an open question.

Tests

  • packages/spec/src/security/permission.test.ts: '', ' ' and a tab are refused as one custom issue at businessUnit, with the message naming sys_business_unit.name. The same refusal reaches through PermissionSetSchema.adminScope at ['adminScope', 'businessUnit']. A real name parses, and a padded one is kept byte-identical (this pins that there is no transform). The absent key stays one invalid_type at businessUnit.
  • Firing control. With permission.zod.ts restored on disk to today's main blob 0e6d6902b063 (tree only, hash-verified), the 6 refusal pins go red (6 failed | 86 passed). Restore was verified: blob back to HEAD 0dddd0bdb439, git diff HEAD empty, porcelain clean. On HEAD the file is 92 passed.
  • plugin-security, read-only (no file edited): permission-set-projection, packaged-permission-set-lock, delegated-admin-gate, delegated-admin-gate-cross-organization, security-plugin, bootstrap-seed-round-trips, invitation-placement, resolve-permission-sets-for-context.pin pass (477 passed), against a spec dist/ built from this branch. A scratch probe, not committed, drove the three stored-scope paths above with '', ' ' and a tab through the real registered permission schema: 10 passed. A real anchor control passes all three.
  • @objectstack/spec whole package at a5a53acaf0: pnpm test gives 524 passed files, 15444 passed | 1 todo. pnpm typecheck passes (tsc --noEmit, scripts typecheck, and test-typecheck held at its ledger).
  • Lint, narrowed and measured at a5a53acaf0: eslint (--no-inline-config, --format json) reported 4 of the 6 changed paths, with 0 errors and 0 warnings. The .md changeset and the .mdx reference page match no files entry in eslint.config.mjs. That config never enables type-aware linting (it says so itself), so this diff cannot move the verdict on any untouched file.
  • Gates: dispatch-gates --ran accounts for all 110 derived families. 108 ran green. 2 are NOT MEASURED with PREREQUISITE NOT MET (exit 3), because both need a whole-workspace build: check:dual-build-cjs-loads and check:type-check-debt. CI runs both.
  • origin/main moved 3 commits past the base (2548ba57de, 863a775872, 0e90a8d1c5). None touches a path in this diff or the three stored-scope paths, so I did not merge them in; the queue rebuilds onto current main.

Generated files that moved

  • packages/spec/src/migrations/registry.ts (gen:migration-registry)
  • content/docs/references/security/permission.mdx (gen:schema + gen:docs: the businessUnit description row, twice)

check:generated reports all 15 generated artifacts up to date against a freshly built dist/. spec-changes.json, protocol-upgrade-guide.md, authorable-surface/, api-surface/ and export-origins/ did not move.

Acceptance notes

  • businessUnit with surrounding whitespace around a real name (' north_america ') is still accepted and stored as written; the gate's exact lookup resolves it to nothing. The ruling scoped this card to blankness. Noted, not filed.
  • Out of scope, per the dispatch: no consumer edit (plugin-security, plugin-auth, packages/lint), no data migration, no other key of AdminScopeSchema.

Generated by Claude Code

AdminScopeSchema.businessUnit is the scope's one required key, and a
bare string accepted '' and whitespace-only values, satisfying the
requirement while naming no business unit. It now carries the
non-transforming NON_BLANK_STRING refinement with a message naming what
a valid anchor is (the sys_business_unit.name of the subtree root). A
real name parses byte-identical; the absent key keeps its own
invalid_type refusal.

Stored scopes are not rewritten: the ADR-0087 semantic entry
admin-scope-business-unit-blank-refused records that a stored blank
anchor is refused on its next write.

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
…ank-anchor entry

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
…nk businessUnit rule

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/releases/v17/17-0.mdx (via AdminScopeSchema (symbol, a top-level const))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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 — 136 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 0e90a8d1c5f3cd3312730a0c9d7f206b3e887314 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 0e90a8d1c5f3cd3312730a0c9d7f206b3e887314

⚠️ 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 0e90a8d1c5f3cd3312730a0c9d7f206b3e887314 → 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

Contract review

Served-tier: 91/91 CONTRACT_REVIEW_TIER
Head-sha: a5a53acaf09d2203735cc86d9ce57e5301c20b54

Isolated at-tier reviewer subagent, run by the domain:spec seat-4 session on the maintainer's instruction in that session (「帮我处理」, with the landing route chosen there); every one of its 91 transcript turns served at the tier the constant names. Adopted by the seat 2026-09-24T05:34Z. The record below is the reviewer's, unedited except the two header lines.

① Derived judgments

Inputs read: card #19461 body + all 7 comments (ruling = comment 5793356837, batch #217 item 1, letter A: non-transforming refinement refusing empty/whitespace-only businessUnit at parse with the path named and a message naming sys_business_unit.name; stored scopes not rewritten, no skip path, refused on next write / boot backfill ERROR, reads unaffected; ADR-0087 semantic entry; pins for '', ' ', a tab, absent key unchanged). PR #19864 body, pulls/19864/files (6 files: +287/-4), the diff, 1 issue comment (docs-drift bot), 0 review comments, 0 reviews. git fetch origin claude/issue-19461-admin-scope-nonblank-anchor:refs/review/pr-19864 -f gives a5a53acaf09d2203735cc86d9ce57e5301c20b54; merge-base with origin/main 8cbc3c0084a3c3c8ef5e59763ab3be96aa1a6f07; 4 commits (cf0b3cd, 6c85021, bac9876, a5a53ac). git diff --stat origin/main...refs/review/pr-19864 = the same 6 files.

Every accept-set / public-surface / runtime change the diff produces, judged against ruling A:

  1. Accept-set narrowing (RIGHT). refs/review/pr-19864:packages/spec/src/security/permission.zod.ts:681-682 reads businessUnit: z.string().refine(NON_BLANK_STRING, { message: ADMIN_SCOPE_BUSINESS_UNIT_BLANK }), import at :10. origin/main:packages/spec/src/shared/refinement-projection.ts:263-264 defines NON_BLANK_STRING as source.trim().length > 0 registered via declare(...): a boolean predicate, no .trim() transform, no value rewrite. It refuses '', spaces, '\t' (all ECMA-262 WhiteSpace/LineTerminator), and accepts a real name byte-identical, padding included. The base at origin/main:packages/spec/src/security/permission.zod.ts:645 was a bare z.string().describe(...), as the ruling measured. Message constant at :630 names sys_business_unit.name and the remove-adminScope alternative. Ruling item 1 satisfied in every clause (refinement, non-transforming, path, message).

  2. Absent key unchanged (RIGHT). The refinement runs only on a string, so a missing key stays one invalid_type at ['businessUnit']; pinned at refs/review/pr-19864:packages/spec/src/security/permission.test.ts:95-101 (length 1, code invalid_type, message does not contain sys_business_unit.name).

  3. Reach through every parse door (RIGHT, no other spelling admits it). AdminScopeSchema is a strictObject (unknown keys such as business_unit refused), embedded at refs/review/pr-19864:packages/spec/src/security/permission.zod.ts:942 as adminScope: AdminScopeSchema.optional(); PermissionSetSchema is the registered permission type (origin/main:packages/spec/src/kernel/metadata-type-schemas.ts:164) and the stack-manifest element type (origin/main:packages/spec/src/stack.zod.ts:461). git grep -n -E "AdminScopeSchema\.(partial|pick|omit|extend)|PermissionSetSchema\.(partial|pick|omit|extend)" origin/main -- 'packages/**' 'apps/**' returns nothing, so no derived schema re-declares the key loosely; git grep -l AdminScopeSchema origin/main outside packages/spec/ returns nothing. The one write-side parse is origin/main:packages/metadata-protocol/src/protocol.ts:16075-16077 (resolveOverlaySchema(...).safeParse(request.item)), throwing INVALID_METADATA / 422 at :16099-16100 with structured issues; there is no actor or flag bypass inside that block. The snake_case row spelling admin_scope is mapped to adminScope before the parse at origin/main:packages/plugins/plugin-security/src/permission-set-projection.ts:409, so the stored spelling does not evade it. Pinned: refs/review/pr-19864:packages/spec/src/security/permission.test.ts:54-90 (3 blanks via AdminScopeSchema, 3 via PermissionSetSchema at ['adminScope','businessUnit'], each exactly one custom issue at :65 and :79; padded name byte-identical at :92).

  4. Public surface (RIGHT, declared). No export added: the message constant is module-private; AdminScope / AdminScopeParsed types unchanged. The published JSON Schema for security/AdminScope.businessUnit gains minLength: 1 plus pattern \S because NON_BLANK_STRING is a declared projectable arm: origin/main:packages/spec/scripts/lib/refinement-projection.ts:234-235 dispatches non-blank-string to emitNonBlankString (:128-138), NON_BLANK_PATTERN = '\\S' at refinement-projection.ts:203. The tree is gitignored (origin/main:.gitignore:63 packages/spec/json-schema/), so nothing checked in is stale; check:authorable-surface, check:docs, check:api-surface live in Type Check · source gates, success at head. The .describe() edit is carried to the generated reference: content/docs/references/security/permission.mdx rows at :30 and :181 in the diff; git grep -n "Delegation boundary: sys_business_unit.name of the subtree root" refs/review/pr-19864 | grep -v "Required and" returns nothing, so no stale copy of the old text remains at head.

  5. Stored rows (RIGHT, ruling item 2). No plugin-security, plugin-auth or lint file is in the diff, and no D2 conversion was added (packages/spec/src/conversions/registry.ts untouched). The refusal reaches stored scopes only through the existing saveMetaItem parse: boot backfill origin/main:packages/plugins/plugin-security/src/permission-set-projection.ts:1569-1606 (increments backfillFailed at :1577, first-failure ERROR with warn fallback, names the record), restore leg :1195-1206 (un-trash stands, ERROR NOT re-authored into metadata), data-door update :1306-1348 (touchesDefinition routes any definition edit through saveMetaItem at :1348). Read side: origin/main:packages/metadata-protocol/src/metadata-diagnostics.ts:97-99 runs safeParse(candidate) and returns a verdict / _diagnostics, never throws. No skip path anywhere. Check-runs at head (commits/a5a53acaf09d2203735cc86d9ce57e5301c20b54/check-runs): 35 total, 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke opt-in), 0 failure, including Test Core 1-6, Build Core, Lint & Repo Gates, Type Check (workspace / source gates / consumer gates / debt ledger), Check Changeset, Spec property liveness, Dogfood Regression Gate. Not re-run locally.

  6. ADR-0087 (RIGHT, ruling item 3). New entry refs/review/pr-19864:packages/spec/src/migrations/entries/semantic/18.admin-scope-business-unit-blank-refused.ts carries the five SemanticMigration fields (origin/main:packages/spec/src/migrations/types.ts:35-46), states no D2 conversion, stored rows not rewritten, next-write refusal, and that a clean boot is not a completed sweep. registry.ts is regenerated, not hand-edited: the entry lands in the step18 block between admin-export-wildcard-removed and advanced-plugin-lifecycle-config-retired (alphabetical; PR ref :5489-5555), and the generator copies the entry's leading comment exactly as it does for the 48 other entries carrying the "No backticks in surface" comment at origin/main (the $between precedent at origin/main:packages/spec/src/migrations/registry.ts:8473-8475). git show refs/review/pr-19864:packages/spec/src/migrations/registry.ts | grep -c "id: 'admin-scope-business-unit-blank-refused'" = 1; at origin/main = 0; entry file absent at origin/main. check:migration-registry runs in Lint & Repo Gates (origin/main:.github/workflows/lint.yml:384), success. docs/protocol-upgrade-guide.md and spec-changes.json are not owed: the checked-in guide's last section is ## Protocol 16 → 17 (origin/main:docs/protocol-upgrade-guide.md:20) and contains 0 hits for the precedent id filter-between-blank-endpoint-refused; the precedent commit 176b03582 (PR spec(data): $between requires two non-blank endpoints (#18012) #19066) touched the same six-file shape and neither artifact; check:spec-changes / check:upgrade-guide (lint.yml:5500-5503) success at head.

Required and missing: nothing. Ruling items 1-3 are each implemented and pinned. Beyond the ruling: nothing. The .describe() sentence and the regenerated mdx document the rule the ruling ordered; the JSON-Schema keywords are a projection of the same predicate and are declared in the changeset. No file outside the dispatch surface (permission.zod.ts + its test, one semantic entry, regenerated registry, .changeset/, regenerated reference page).

② Semver level

refs/review/pr-19864:.changeset/19461-admin-scope-business-unit-blank-refused.md: line 2 '@objectstack/spec': minor; line 5 opens **BREAKING for authored metadata**; line 7 Clause-②: yes; lines 27-34 a FROM → TO table plus the one-line fix; line 39 exactly one HTML-comment marker reading adr-0087: registered admin-scope-business-unit-blank-refused.

Against AGENTS.md at origin/main: :1084-1085 (yes takes at least minor; (narrowing) is BREAKING), :1086-1087 (breaking changesets carry FROM → TO and the one-line fix), :1095-1096 (exactly one ADR-0087 disposition marker in the body that also carries the Clause-② line). Launch window: origin/main:scripts/check-changeset-no-major.mjs:5-6, 47-48 (breaking ships as minor; major refused). Gate reading: origin/main:scripts/check-adr-0087-registration.mjs:632 matches the banner with /\*\*BREAKING/i, so the changeset is judged breaking; the marker regex at :1924 and the registered parse at :1931-1935 accept the spelling; registered requires every id to resolve at HEAD and at least one to be new in the diff, which the counts above satisfy. Check Changeset and Lint & Repo Gates success at head. Level minor + BREAKING banner + one registered marker is the correct launch-window shape for a published-face narrowing, and matches the $between precedent (#18012 / PR #19066).

③ Boundary flags

  • non-blocking, dev-declared (report 5795215725 open question): Clause-②: yes on line 7 of the changeset and in the PR body. origin/main:scripts/pm/clause2-line.mjs documents bare yes as "a widening" and no (narrowing) as the spelling of a pure narrowing; readClause2Line returns arm: null for this line (:445-448). This diff widens nothing. No gate is weakened (the BREAKING banner supplies signal 2 of breakingDeclaration; the level axis is satisfied by minor), and the value was fixed by the ruling itself (5793356837 "Execution": Clause-②: yes). The spelling class belongs to the ruling holder, for this PR and spec(data): $between requires two non-blank endpoints (#18012) #19066 alike; not a defect of the implementation.
  • non-blocking, published prose over-broad on one axis: the changeset sentence "A Setup or data-door edit ... answers 422 INVALID_METADATA" and the entry's acceptance text hold wherever the metadata parse runs. Two data-door writes never reach it and are pre-existing: a system-context write, origin/main:packages/plugins/plugin-security/src/permission-set-projection.ts:1078 if (opCtx?.context?.isSystem) return next();, and a single-store kernel with no overlay protocol, :1087 if (!capable) { ... return next(); }. Both write the driver row directly, and admin_scope is Field.textarea (origin/main:packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts:292), so the engine validator does not parse the JSON. A row stored that way is named only by the next boot backfill (:1569-1606), and only if it has no metadata definition. This is outside the ruling's write surface (ruling forbids consumer code and a skip path), so it is a lead for the ruling holder, and a scoping clause in the changeset ("where the metadata parse runs") would make the sentence exact.
  • non-blocking: "whitespace-only" is ECMA-262 WhiteSpace ∪ LineTerminator (the trim set, pinned equal to \S by packages/spec/scripts/refinement-projection.test.ts). U+200B zero-width space is outside that set and still parses; the ruling's scope is empty or whitespace-only, so this is a note, not a gap.
  • non-blocking, dev-declared: base 8cbc3c0084 sits behind origin/main (c1641868a3 at review time); the three-dot diff is still the 6 claimed files and the PR reports mergeable_state: clean. The PR is a draft (draft: true), as the governed-surface rule requires until a tier record exists.
  • Pins: the six refusal pins and the padded-name pin assert toHaveLength(1) and the exact path, so a transform or a second issue would fail them; the firing control (6 red against base blob 0e6d6902b063) is the dev's declaration, not re-run here; Test Core success at head stands for the green side.
  • Files outside the claim: none. The release-owned page content/docs/releases/v17/17-0.mdx named by the docs-drift bot was correctly left untouched.

Implemented-by: claude/issue-19461-admin-scope-nonblank-anchor
Reviewed-by: session_019c3Hi6ZMU1p6m6aA6Bz45d

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 24, 2026 05:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit 77f54bf Sep 24, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19461-admin-scope-nonblank-anchor branch September 24, 2026 06:01
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

1 participant