Skip to content

feat(spec)!: retire AdminScope.manageBindings and authorEnvironmentSets — delegation rights no door honoured (#23488) - #23601

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23488-retire-adminscope-binding-keys
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23488-retire-adminscope-binding-keys

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23488
Clause-②: yes

Executes ruling of record 6181988435 on #23488 (letter A, the maintainer's 「同意」): AdminScope.manageBindings and AdminScope.authorEnvironmentSets retire inside the 18 major by the spec-property-retirement route, the delegated-admin gate's strict-containment comparison stops reading them, and the text that described the removed gates is corrected. The changeset declares Clause-②: yes (narrowing), BREAKING, ADR-0087 disposition registered.

The ADR half (ADR-0090 D12 status line, ADR-0086 line 446) is a separate Tier H docs PR on branch claude/issue-23488-adr-delegation-rights-retired, as ruling item 5 requires. The objectui half (Studio's two switches, help strings, i18n keys, pin test) is objectstack-ai/objectui#12406, filed by the seat (ruling item 4); this PR does not touch objectui.

What changes

1. The retirement kit (@objectstack/spec).

  • Two retiredKey() tombstones on AdminScopeSchema (security/permission.zod.ts). Each prescription says what the ruling binds: delete the key; a delegate's scope is placement, assignment and the allowlist. Both close with the house os migrate meta --from 17 sentence.
  • DelegableAdminScope (contracts/security-service.ts) drops both fields.
  • D2 conversion permission-admin-scope-binding-authoring-removed in MAJOR_18_CONVERSIONS (order 65, retiredFromLoadPath: true, retiredAfter: '17.7.0'). It strips both keys from every permissions[].adminScope, whatever they hold, one notice per key, and the notice path names the set. Both keys carried .default(false), so a 17.x parse materialized them in every declared scope. The rehydration seams replay this entry over stored rows and built artifacts (the email_template.isSystem precedent; acceptRetiredDefaultResidue is not adopted).
  • RETIRED_KEYS_BY_MAJOR[18]: security/AdminScope:manageBindings, security/AdminScope:authorEnvironmentSets.
  • D3 semantic entry permission-admin-scope-binding-authoring-retired, which states the containment change, plus its STEP18_RATIONALE fragment (order 97).
  • The form's help text, the generated reference page, authorable-surface/security.json (two rows become RETIRED), authorable-defaults/security.json (two default rows go), the liveness note (liveness/permission.json, re-verified, still live), and the removal-version pin's JOIN row.

2. The containment change (@objectstack/plugin-security, ruling item 2).

  • definitionContainsStrictly compares includeSubtree, manageAssignments and the allowlist only. resolveHeldScopes and describeDelegableScope drop both flags.
  • Case (ii) of reading 6172654646 becomes a refusal: a delegate whose own scope carries an inert true can no longer hand out a scope otherwise equal to its own. It answers 403 PERMISSION_DENIED.
  • New pins in delegated-admin-gate.test.ts, under the describe [#23488] the retired binding/authoring flags count on no containment axis:
    • case (ii) refused, asserting code + status + the message's first clause;
    • a CONTROL: the same delegate grants a scope narrower on a real axis;
    • case (i) reversed: the granted side's retired true no longer out-reaches the delegate.
  • Mechanism assumption 1 holds: the gate fixture set both flags true on both sides (EAST_SCOPE), so these pins are new, not flipped assertions. The six other plugin-security fixtures drop the keys.

3. The legacy-row ceremony (@objectstack/core, ruling item 3) — measured, and the mapper drops the keys.

  • The measurement used a scratch test, not committed, in packages/metadata-protocol at 43c50c843. It took permissionSetDefinitionFromRow over a 17.x-shaped row (admin_scope carrying both keys as false) and passed the body to ObjectStackProtocolImplementation.saveMetaItem. That is the door os migrate organization-ownership composes (packages/cli/src/commands/migrate/organization-ownership.ts:567-578).
    • The door answered {"ok":false,"code":"INVALID_METADATA","status":422} at adminScope.manageBindings, with the tombstone's prescription.
    • CONTROL: the same row without the two keys saved, {"ok":true,"success":true}.
    • So the save does not replay the conversion. That matches conversions/stored.ts's header: "The write path must NOT use this: saveMetaItem rejects off-spec bodies with the current schema".
  • Fix: permission-set-row-definition.ts drops both keys from the parsed admin_scope (RETIRED_ADMIN_SCOPE_KEYS).
  • New pin: the body without the keys passes PermissionSetSchema for false, true and a non-boolean value. A CONTROL shows the same body with the key kept is refused at ['adminScope','manageBindings'].
  • packages/cli/src/utils/organization-ownership-apply.ts is read only, not edited (mechanism assumption 4 holds).

4. Position 3 text (docs accuracy, no contract change).

  • content/docs/permissions/delegated-administration.mdx: the action-flag and strict-containment rows.
  • content/docs/permissions/permission-sets.mdx: the strict-containment sentence.
  • The showcase's seven sentences: five in examples/app-showcase/src/security/permission-sets.ts (:26-27, :166-167, :357-362, :384-385, and the two keys deleted) and two in examples/app-showcase/src/docs/showcase_tour_security.md.
  • The spec JSDoc (AdminScopeSchema header, assignablePermissionSets, PermissionSet.adminScope).
  • The ledger note.
  • The two plugin-security comments: delegated-admin-gate.ts scopesCoverUser and the placement comment, and objects/sys-user-permission-set.object.ts.
  • packages/metadata-core/src/meta-write-capability.ts:184-185: the security plugin's data door no longer asks the walled verdict. Measured: git grep -n 'walledCatalogWriteVerdict(' -- packages finds no caller under packages/plugins.
  • docs/design/permission-model.md (the delegated-admin bullets and the authoring sample).
  • packages/platform-objects' four metadata-forms bundles:
    • en was regenerated with pnpm i18n:extract;
    • the three translated helpText values (zh-CN, ja-JP, es-ES) were edited by hand, since AGENTS.md allows translated-locale values to be hand-written;
    • the adminScope path has no source-hash entry, so it is legacy-trusted.
  • packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts now asserts both keys are absent from the served definition.

5. Tree-scoped absence pin. packages/spec/src/security/admin-scope-binding-authoring-retirement.test.ts (registered in vitest.repo-tests.json) pins:

  • the refusal at every door: schema, the permission write-door binding, and defineStack with STACK_SCHEMA_INVALID / 422;
  • the tsc channel (@ts-expect-error);
  • the D2 strip: stored-row rehydration, the artifact before and after, idempotence, and that it is retired from the load path;
  • the registration;
  • an absence walk over the five roots already declared for @objectstack/spec in scripts/cross-package-test-inputs.mjs.

File surface vs the claim

These are outside the claim's list, each with the reason:

  • docs/design/permission-model.md: the reading (Reading 5) lists it, and it named both keys as authorable.
  • examples/app-showcase/src/docs/showcase_tour_security.md: two of "the showcase's seven" (ruling item 5).
  • packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts: it asserted manageBindings === false.
  • packages/plugins/plugin-security/src/*.test.ts fixtures.
  • packages/spec/src/shared/retired-key-removal-version.pin.test.ts: its guard requires a JOIN row for every newest-stable retirement.
  • packages/spec/vitest.repo-tests.json.
  • The generated authorable-* and reference files.

Mechanism assumption 3 does not hold: no generator wrote under skills/**, so this PR touches no governed surface.

Verification

Pre-merge head 0221b165f. Each command ran through scripts/pm/os-verify-lock.sh, with the exit code captured before any pipe:

  • pnpm --filter @objectstack/spec test: 677 files passed, 1 skipped; 20729 tests passed.
  • pnpm --filter @objectstack/spec test:repo: 62 files, 1051 tests passed.
  • pnpm --filter @objectstack/spec typecheck: exit 0. That includes check:test-typecheck, so the two @ts-expect-error lines are compiled and used.
  • pnpm --filter @objectstack/plugin-security typecheck: exit 0.
  • pnpm --filter @objectstack/core test: exit 0.
  • pnpm --filter @objectstack/platform-objects test: exit 0.
  • plugin-security, 7 targeted files (delegated-admin-gate*.test.ts, granted-by-writer-provenance, reserved-identity-names, can-write-object-admission): 206 passed.
  • pnpm --filter @objectstack/spec check:generated: one stale artifact (content/docs/references/**), regenerated with gen:docs. Every other gate was green.

Reverse verification, at 0221b165f, both restored and proved by blob hash and an empty git diff HEAD:

  • Gate. delegated-admin-gate.ts at the branch point fa6a6085c was written over the file. Of the four selected cases, 2 failed (case (ii) and case (i) reversed) and 2 passed (the CONTROL and the existing equal-scope refusal). That is the expected direction.
  • Mapper. scripts/ablation-replace.mjs removed the withoutRetiredAdminScopeKeys(...) call (anchor 1 → 0, blob 03f32ba6f610 → 01d09ba3e389). The mapper pin failed (1 failed, 12 passed), and the file was restored to the HEAD blob.

objectui at the pin (b9814b334d), measured by reproduction, not by a full objectui build.

  • I typechecked a copy of PermissionAdvancedFacets.tsx's typing against this branch's built @objectstack/spec dist. The copy covered the AdminScope import, Partial alias, setScopeDetail, the three switches' scope[key] and the { [key]: !!v } as Partial write, plus DelegableAdminScope. It used objectui's compiler options (strict, moduleResolution: bundler) and TypeScript 6.0.3, and exited 0.
  • CONTROL: a literal { businessUnit: 'x', manageBindings: false } typed as AdminScope fails TS2322 (the retired mark), and DelegableAdminScope['manageBindings'] fails TS2339. So the reproduction reads the new types.
  • A full objectui build at the pin is NOT MEASURED.

Merged head 3854d1403 (origin/main merged through scripts/pm/os-regen-merge.sh): the closure rebuild, the derived gate families, the consumer typechecks and the dogfood zoo file were in progress when this PR opened. Their readings go into the os-dev-report comment on #23488, and CI's check-runs on this PR are authoritative.

Acceptance notes

  • Not edited: packages/plugins/plugin-security/src/__tests__/legacy-catalog-objects.testkit.ts:333-336. It describes the retired sys_permission_set.admin_scope column as it was, both keys included. That is accurate for a model of a retired object; carrier: none.
  • Left as they are (PROSPECTIVE, not false): three sentences describe a door that ADR-0138 D3 item 4 assigns to the bind door when it is built: packages/plugins/plugin-security/src/builtin-positions.ts:92, packages/core/src/security/resolve-authz-context.ts:1099-1100, and content/docs/permissions/permission-sets.mdx "the wildcard ban is the stricter guest tier's rule". Carrier: none.
  • Release-owned, not touched: content/docs/releases/v13.mdx:91 and the CHANGELOG.md entries.
  • Out-of-repo stored rows and consumers: NOT MEASURED, as the reading stated.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/core, @objectstack/metadata-core, @objectstack/platform-objects, @objectstack/plugin-security, @objectstack/spec, touching 29 documentable anchor(s). ⚠️ 8 changed file(s) yielded no anchor (packages/metadata-core/src/meta-write-capability.ts, packages/spec/authorable-defaults/security.json, packages/spec/authorable-surface/security.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/data-modeling/objects.mdx (via SysUserPermissionSet (symbol, a top-level const object))
  • content/docs/permissions/authentication.mdx (via adminScope (literal, a string literal in sections))
  • content/docs/permissions/authorization.mdx (via adminScope (literal, a string literal in sections))
  • content/docs/permissions/delegated-administration.mdx (via DelegatedAdminGate (symbol, a top-level class), authorEnvironmentSets (symbol, a field of interface DelegableAdminScope), manageBindings (symbol, a field of interface DelegableAdminScope), adminScope (literal, a string literal in sections), authorEnvironmentSets (literal, a string literal in RETIRED_ADMIN_SCOPE_KEYS; a string literal in apply; a string literal in summary), manageBindings (literal, a string literal in RETIRED_ADMIN_SCOPE_KEYS; a string literal in apply; a string literal in summary), sales_user (literal, a string literal in fixture), /api/v1/security/my-delegable-scope (route, the route ledger binds it to client method security.describeDelegableScope))
  • content/docs/permissions/explain.mdx (via adminScope (literal, a string literal in sections))
  • content/docs/permissions/index.mdx (via adminScope (literal, a string literal in sections))
  • content/docs/permissions/permission-metadata.mdx (via adminScope (literal, a string literal in sections), sales_user (literal, a string literal in fixture))
  • content/docs/permissions/permission-sets.mdx (via adminScope (literal, a string literal in sections), sales_user (literal, a string literal in fixture))
  • content/docs/permissions/permissions-matrix.mdx (via adminScope (literal, a string literal in sections))
  • content/docs/permissions/positions.mdx (via adminScope (literal, a string literal in sections))
  • content/docs/permissions/profiles.mdx (via PermissionSetSchema (symbol, a top-level const))

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

  • content/docs/releases/v12.mdx (via PermissionSetSchema (symbol, a top-level const))
  • content/docs/releases/v13.mdx (via DelegatedAdminGate (symbol, a top-level class), PermissionSetSchema (symbol, a top-level const), authorEnvironmentSets (symbol, a field of interface DelegableAdminScope), manageBindings (symbol, a field of interface DelegableAdminScope), adminScope (literal, a string literal in sections), authorEnvironmentSets (literal, a string literal in RETIRED_ADMIN_SCOPE_KEYS; a string literal in apply; a string literal in summary), manageBindings (literal, a string literal in RETIRED_ADMIN_SCOPE_KEYS; a string literal in apply; a string literal in summary))
  • content/docs/releases/v14.mdx (via adminScope (literal, a string literal in sections))
  • content/docs/releases/v17/17-0.mdx (via AdminScopeSchema (symbol, a top-level const), PermissionSetSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-5.mdx (via retiredAfter (symbol, a field of const object permissionAdminScopeBindingAuthoringRemoved), retiredFromLoadPath (symbol, a field of const object permissionAdminScopeBindingAuthoringRemoved))

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
  • 8 changed file(s) yielded no anchor (packages/metadata-core/src/meta-write-capability.ts, packages/spec/authorable-defaults/security.json, packages/spec/authorable-surface/security.json, …) — pages documenting those are invisible to this run
  • 9 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 150 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 16abea88450be3c453a9e769445af341a956bd17 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4cc310b88f0b85ef8a88cce2753420b8e06a16e4 — the merge of head 3854d1403f268d66b6ce39f7859e1fd52bbdccfe into base 16abea88450be3c453a9e769445af341a956bd17, 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 4cc310b88f0b85ef8a88cce2753420b8e06a16e4 && git checkout 4cc310b88f0b85ef8a88cce2753420b8e06a16e4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 16abea88450be3c453a9e769445af341a956bd17 3854d1403f268d66b6ce39f7859e1fd52bbdccfe && git checkout -B drift-repro 16abea88450be3c453a9e769445af341a956bd17 && git merge --no-ff 3854d1403f268d66b6ce39f7859e1fd52bbdccfe

node scripts/docs-audit/affected-docs.mjs --json 16abea88450be3c453a9e769445af341a956bd17

⚠️ 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 16abea88450be3c453a9e769445af341a956bd17 → 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 · domain:spec seat 2 (#18549) · session session_01LFqdRsuM5nkqoJdHd5ztVk · 2026-10-15T04:38Z

This is the seat review of record for #23488 (claim 6182195274, ruling 6181988435 letter A, dev report 6184471100) on head:

3854d1403f268d66b6ce39f7859e1fd52bbdccfe

It is not the contract review. That review is owed at CONTRACT_REVIEW_TIER because packages/spec/src/security/**, contracts/security-service.ts and the registry are published schema.

  1. Form.
  2. Ruling items against the diff.
  3. Gates.
    • Check-runs on this head: 41 in all, 36 success, 5 skipped, none red or pending. That includes Lint & Repo Gates, TypeScript Type Check and Test Core 1–6.
    • Locally the dev ran 116 of 118 derived families. The other 2 are NOT MEASURED, each with a reason.
    • Reverse verification: the gate at the branch point turned the two new pins red. The mapper ablation turned its pin red. Both were restored to the HEAD blob.
  4. Accepted deviations.
    • Files beyond the claim's list are each named in the PR body with a reason: docs/design/permission-model.md, showcase_tour_security.md, the dogfood zoo test, the plugin-security fixtures, the removal-version pin, vitest.repo-tests.json and the generated files.
    • The changeset adds core and platform-objects patch rows because both publish changed content.
    • Declared narrowing: test:repo after the seat's lock note, and consumer whole suites, are left to CI. Test Core is green.
    • This record amends the claim's surface to the PR's file list.
  5. Out-of-scope findings.
    • (1) check:platform-checklist red on main: dropped. The daily watchdog owns it (check:platform-checklist is red on main #23593).
    • (2) The legacy-catalog testkit's column description: Acceptance notes. It accurately models a retired object.
    • (3) Three prospective guest-bind sentences: Acceptance notes. ADR-0138 D3 item 4 schedules that door.
  6. Prose checked against the diff.
    • Changeset: the BREAKING header; "a delegate who 'may bind' or 'may author' was granted nothing"; the FROM → TO table; the quoted prescription; "describeDelegableScope() … no longer reports either flag".
    • delegated-administration.mdx: "manageAssignments … is the scope's only action right", and the strict-containment row now reads "Granting a set that itself carries an adminScope — assigning a position that distributes it, or granting it directly".
    • permission-sets.mdx: "writing a set's body or a position's sets is a metadata write, judged by manage_metadata, never by an adminScope".
    • The remaining sys_position_permission_set mentions in permission-sets.mdx:242 and positions.mdx:11 say "retired", which is correct.
    • The bump levels (spec and plugin-security minor, BREAKING; core and platform-objects patch) are the contract review's ②.
  7. Landing terms.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3854d1403f268d66b6ce39f7859e1fd52bbdccfe
Local-runs: none

Inputs, and nothing else: card #23488 (its body and all ten comments; the ruling of record is 6181988435, letter A, items 1–5; the dev's report is 6184471100), PR #23601's body and its 38-file list, the net diff of this head against main (merge-base 16abea884; +1016 / −112), and the 45 check-runs on the head. Published schema is in the file list (packages/spec/src/security/permission.zod.ts, contracts/security-service.ts, conversions/registry.ts), which puts the face at this tier; no governed path is in the list. The ADR half is PR #23602 and is not judged here. Read-only: the diff was read with git diff against fetched refs, the GitHub records over REST; nothing was built, run or re-run.

Gates, read off the head: all 45 runs concluded — 37 success, 8 skipped, 0 failed. The five required contexts are success (Lint & Repo Gates, TypeScript Type Check, Test Core with all six shards, Build Core, Governed Surface Queue Guard), as are Check Changeset, Spec property liveness, Type Check · consumer gates, Build Docs, Check Documentation Links, the single-writer and claim guards and the docs-drift flag. Skipped are the full-battery and opt-in jobs only: Console Pin Gate, the Dogfood Regression Gate shards, Dogfood Verify CLI, Temporal Conformance, Packed-tarball smoke.

① Derived judgments

Accept-set changes the diff implies, each judged:

  1. AdminScopeSchema.manageBindings and AdminScopeSchema.authorEnvironmentSets become retiredKey() tombstones on the strictObject: every value, the former default false included, is refused at the key's own path with the prescription, and the key's input type is the retired mark. RIGHT — ruling item 1. The prescription carries the ruled sentence verbatim ("Delete the key; a delegate's scope is placement, assignment and the allowlist"), opens with the backticked fully-qualified key, names @objectstack/spec 18, ADR-0049 and ADR-0090 D12, closes with the house os migrate meta --from 17 sentence, and carries no tracker number. A parsed scope no longer materializes either default (pinned in permission.test.ts; the dogfood zoo asserts the served definition carries neither key).
  2. Every door that parses a permission set narrows with it: the permission registry binding (PUT /api/v1/meta/permission/:name validates against it), defineStack({ permissions }) with the STACK_SCHEMA_INVALID / 422 envelope, and definePermissionSet(). RIGHT — pinned in the new retirement test, each with a CONTROL that the same body without the keys passes.
  3. D2 conversion permission-admin-scope-binding-authoring-removed in MAJOR_18_CONVERSIONS: order 65 (the previous maximum is 64), inserted at its identifier's sort position and defined directly above the entry that follows it; retiredFromLoadPath: true; retiredAfter: '17.7.0', the value the other newest-stable retirements in the 18 list carry. It strips both keys from every permissions[].adminScope whatever they hold, one notice per key, the notice path naming the set; the fixture carries four sets and expectedNotices: 4. RIGHT — ruling item 1's registered strip. A live author is refused, never rewritten (pinned); a stored permission row replays clean through applyConversionsToStoredItem and a built artifact through applyConversions with retired entries included (pinned, with a lit control that a scope which was always wrong is refused on both sides); idempotent by construction (stripKeys) and pinned anyway. The acceptRetiredDefaultResidue stage is not adopted, with the email_template.isSystem precedent named — right for a key whose residue the replayed strip heals.
  4. RETIRED_KEYS_BY_MAJOR[18] gains security/AdminScope:manageBindings and security/AdminScope:authorEnvironmentSets (two files under migrations/entries/retired-keys/, named from the ids, spelled as authorable-surface/security.json spells them); one D3 semantic entry permission-admin-scope-binding-authoring-retired states the containment change with non-empty reason and acceptanceCriteria and no backtick in surface; one STEP18_RATIONALE fragment at order 97 (the previous maximum is 96), at its id's sort position. RIGHT — the generated registry.ts is gitignored, so nothing more is owed in the diff; the registration pins and both merge tests run under Test Core.
  5. DelegableAdminScope (@objectstack/spec/contracts) and plugin-security's DelegableScopeReport drop both fields, so GET /api/v1/security/my-delegable-scope reports manageAssignments as the scope's one action right. RIGHT — ruling item 1. The changeset's FROM → TO table names the endpoint; the api-surface snapshot is unaffected by design (it records export presence, not member sets); every Type Check lane and the consumer gates are green, and a tree-wide grep of the head finds no remaining typed reader of either field (the client dialog reads placeableBusinessUnitIds and assignablePositions only).
  6. definitionContainsStrictly compares includeSubtree, manageAssignments and the allowlist only, on both sides. Two accept-set moves on the delegated sys_user_position and sys_user_permission_set writes: case (ii) — a delegate whose own scope carries an inert true can no longer hand out a scope otherwise EQUAL to its own; it answers 403 PERMISSION_DENIED, closing the live loosening the ruling named; case (i) reversed — a granted scope carrying a retired true (a stored row the strip has not yet rewritten) no longer out-reaches a delegate that lacks it. RIGHT — ruling item 2. Case (ii) is pinned on code, status and the message's first clause, beside a CONTROL in which the same delegate grants a scope narrower on the subtree axis; case (i) reversed is pinned too. The gate fixture EAST_SCOPE set both flags true on both sides before this diff, so the pins are new, not flipped assertions. resolveHeldScopes and describeDelegableScope stop reading the keys, and the scopesCoverUser JSDoc now says what the code does (it reads no flag).
  7. permissionSetDefinitionFromRow (@objectstack/core) drops both keys from a legacy row's admin_scope, whatever they hold, before the ceremony's body reaches saveMetaItem. RIGHT — ruling item 3 made this conditional on a measurement, and the report supplies it: the save replays no conversion (a 17.x-shaped row answered 422 INVALID_METADATA at adminScope.manageBindings; the same row without the keys saved). Pinned for false, true and a non-boolean value, with a CONTROL that the body with the key kept is refused at exactly ['adminScope', 'manageBindings']. The fixed two-name list is documented as this retirement's own D2 strip applied at the one seam the seam table does not reach, naming the conversion id, and the comment says why it is written down rather than derived. organization-ownership-apply.ts is untouched, as the claim required.

Public-surface and generated-surface changes, each judged:

  1. authorable-surface/security.json: both rows become [RETIRED]; authorable-defaults/security.json: both default rows go. RIGHT for the tombstone route (the row stays, marked). authorable-surface.base.json still lists both keys unmarked — RIGHT: that file is the deletion gate's anchor, written only by --update-base, never by a retirement PR.
  2. liveness/permission.json: adminScope stays live, its evidence rewritten to the two assignment doors and the three containment axes, with a RE-VERIFIED note. The ledger holds no drilled rows for the scope's keys, so no dead row is owed and none is orphaned. RIGHT — Spec property liveness is success.
  3. The generated reference content/docs/references/security/permission.mdx is regenerated: both rows read never / [REMOVED] with the prescription, and the adminScope type string on PermissionSet narrows to the four live keys. RIGHT — hand-edits are absent, as the auto-gen boundary requires.
  4. permission.form.ts help text names the four live keys; the en bundle is regenerated from it, and the zh-CN, ja-JP and es-ES helpText values are hand-edited. RIGHT — translated-locale values are hand-written by rule, no key was added or dropped, and check:i18n sits inside the green Lint & Repo Gates. The form keeps its one adminScope json widget; there was no per-key input to cut.
  5. Position 3 text, docs accuracy with no contract change: the two docs sentences (delegated-administration.mdx action-flag and strict-containment rows; permission-sets.mdx strict-containment paragraph), the showcase's seven sentences plus its two deleted keys, the spec JSDoc and .describe() strings, the ledger note, the two plugin-security comments, the meta-write-capability.ts docstring, and docs/design/permission-model.md. RIGHT. The strict-containment row now states the three axes exactly as the code compares them. The showcase's new claim that showcase_guest_portal reaches people through the client_portal_user position is TRUE on the head (examples/app-showcase/src/security/positions.ts, lines 98–101 declare that position with permissionSets: ['showcase_guest_portal']). docs/design/ is not a governed surface.
  6. The tree-scoped absence pin admin-scope-binding-authoring-retirement.test.ts, registered in vitest.repo-tests.json (so it runs under test:repo, which Test Core runs): it walks the five roots with the same seed spelling as the rls-tags precedent, and its radius is declared for @objectstack/spec in scripts/cross-package-test-inputs.mjs and mirrored in turbo.json (packages/** per extension, examples/*/src/**/*.ts, the non-code examples/** extensions, content/**, skills/**, scripts/**). It excludes only the kit's own files, each with its reason and no allowlist file; the matcher has anti-vacuity cases and the walk has visit-count floors. RIGHT. A tree-wide grep of the head finds no authoring of either key outside the kit; the remaining mentions are prose, the governed ADR-0086 row (PR docs(adr): ADR-0090 D12 status line and ADR-0086 row — the binding and env-set authoring rights retired (#23488) #23602's) and one string literal in the legacy-catalog testkit, which the walk's string masking ignores by design.
  7. The two @ts-expect-error lines are in a file the spec typecheck script compiles (check:test-typecheck runs inside it); TypeScript Type Check is success, so they are not phantom checks. RIGHT.

Nothing in the diff was judged WRONG.

② Semver level

.changeset/23488-admin-scope-binding-authoring-retired.md: @objectstack/spec: minor, @objectstack/plugin-security: minor, @objectstack/core: patch, @objectstack/platform-objects: patch. RIGHT. The two ruled packages publish a BREAKING narrowing, and minor is the house level inside the 18 pre-window (check-changeset-no-major.mjs refuses major; a yes declaration takes at least minor); the BREAKING banner, the FROM → TO table and the one-line fix are present; exactly one ADR-0087 disposition marker, registered, names the D2 and the D3 ids. core publishes the mapper fix and platform-objects the four help texts — both changed content, both patch. metadata-core changes a docstring only and rightly carries no row. No skip-changeset. Check Changeset is success.

Clause-②: line — the PR body declares Clause-②: yes; the changeset carries Clause-②: yes (narrowing), where the ADR-0087 gate reads the arm. Both are well-formed under the closed grammar and match what the diff publishes: a published spec accept-set narrows. The claim (6182195274) and the ruling both say yes (narrowing). RIGHT.

③ Boundary flags

The report's open_questions is empty. Its deviations, acceptance notes and out-of-scope findings, each answered or escalated:

  1. File surface beyond the claim's list (docs/design/permission-model.md, the showcase tour doc, the dogfood zoo test, six plugin-security fixtures, the removal-version pin's JOIN row, vitest.repo-tests.json, the generated authorable-* and reference files) — ANSWERED: each is a necessary carrier of the ruled change or a generator's output, each with its reason in the PR body; none widens scope.
  2. Changeset rows beyond the two ruled packages — ANSWERED: see ②.
  3. PR-body Clause-②: yes without the arm — ANSWERED: see ②; the arm lives in the changeset, where the gate reads it.
  4. Local verification narrowed after the lock-pressure note (consumer suites, the cli integration tier, metadata-core, the showcase validate were not run locally) — ANSWERED: the per-PR check-runs cover them; Test Core 6/6, every Type Check lane and the consumer gates are success.
  5. Ruling item 4, objectui: Studio's two switches, help strings, i18n keys and pin test are Studio: retire the permission set's two delegated-admin switches (manageBindings, authorEnvironmentSets) that objectstack#23488 retires from the spec objectui#12406, not this PR, and this PR moves no pin. The report's typing reproduction of PermissionAdvancedFacets.tsx at pin b9814b334d against this branch's spec dist compiled, with a lit CONTROL (a literal retired key fails TS2322; DelegableAdminScope.manageBindings fails TS2339). A full objectui build at the pin is NOT MEASURED, and Console Pin Gate is full-battery only (skipped on this head). ESCALATED AS A WATCH, not a block: the ruling itself sequences the objectui landing "before or with the .objectui-sha move", so this head may land first; until objectui#12406 lands, a Studio switch flip meets the schema's refusal with the prescription (422), the ruled loud behaviour; the first full-battery Console Pin Gate on main after landing is the spec lane's to read.
  6. Out-of-repo stored rows and consumers — NOT MEASURED, as every reading on the card said; in-repo coverage is complete: the D2 strip on the rehydration seams, the mapper on the ceremony seam, the tombstones at every authoring door.
  7. The dogfood showcase-permission-zoo file is not in per-PR CI (the shards are skipped); the report ran it on the merged head, 10 passed. ANSWERED; the full battery on main is its carrier.
  8. check:platform-checklist reds with 8 ABSENT SYMBOL anchors — ANSWERED: reproduced identically on main without this diff, none of the named anchors is in this PR's file list, and the daily watchdog is the carrier; outside this card's three classes.
  9. legacy-catalog-objects.testkit.ts still names both keys in a column description — ANSWERED: it models the retired sys_permission_set column as it was, and it is a string literal the absence walk masks, so it is not an authoring.
  10. Three PROSPECTIVE sentences about a guest bind door (builtin-positions.ts, resolve-authz-context.ts, the permission-sets.mdx wildcard-ban line) left in place — ANSWERED: ADR-0138 D3 item 4 assigns that door for later; they are not false on main.
  11. One note of this review's own, NOT MEASURED by either side: Setup's adminScope json widget derives its nested face from the schema node, and whether a never-typed tombstone still renders as a switch is not in evidence here. A flip would meet the parse refusal with the prescription, loud by construction, and the same shape holds for every tombstone on a json-widget form; no carrier is owed by this card.

Implemented-by: claude/issue-23488-retire-adminscope-binding-keys
Reviewed-by: session_01LFqdRsuM5nkqoJdHd5ztVk

VERDICT: PASS

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 38684646959 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (6/6) — 失败步骤: Run this shard's tests

    @objectstack/runtime:test:  FAIL   local  src/package-uninstall-shared-object.integration.test.ts > [#23473] DELETE /packages/:id of a package whose object another stored row also holds > without keep
      ↳ 失败原因: @objectstack/runtime:test: Error: Cannot layer object 'probe_ticket': the overlay is bound to package 'com.probe.q', but the object is owned by package 'com.probe.p'. An object has exactly one registr
    @objectstack/runtime:test:  FAIL   local  src/package-uninstall-shared-object.integration.test.ts > [#23473] DELETE /packages/:id of a package whose object another stored row also holds > the way out,
      ↳ 失败原因: @objectstack/runtime:test: Error: Cannot layer object 'probe_ticket': the overlay is bound to package 'com.probe.q', but the object is owned by package 'com.probe.p'. An object has exactly one registr
    @objectstack/runtime:test:  FAIL   local  src/package-uninstall-shared-object.integration.test.ts > [#23473] DELETE /packages/:id of a package whose object another stored row also holds > the way out,
      ↳ 失败原因: @objectstack/runtime:test: Error: Cannot layer object 'probe_ticket': the overlay is bound to package 'com.probe.q', but the object is owned by package 'com.probe.p'. An object has exactly one registr
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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/xl tests tooling

Projects

None yet

2 participants