Skip to content

docs(spec): the security-catalog migration entry and the D6 gloss stop saying a permission set holds one name per deployment - #33445

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-deqozc
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-deqozc

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #33388

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:安全目录覆盖迁移条目(security-catalog-environment-overlay-refused)的 reason 开头不再说「岗位、权限集、能力每个部署一个名字」,改为该条目真正的主题「环境目录和包从不共持一个名字」;ADR-0146 D6 分类依据的注释改为「安全目录不接受覆盖」;权限集指南的单持有者清单补上「环境」,「授予按名字指向权限集」改为「按包和名字」。
  • 为什么改:feat(core, objectql, metadata, metadata-protocol, plugin-security): a permission-set name is held once per namespace, judged by one predicate (ADR-0150 child B) #32742 之后权限集名字按命名空间持有,原句对权限集已不成立;docs(spec): the permission set name describe and the permission sets page say one holder per namespace #33384 改了 describe 和指南主段,这里收掉剩下的三处文字。
  • 风险与回滚:只改说明文字和注释,不动 schema、规则、迁移条目的 id/surface/replacement 或运行时;spec patch changeset。回滚即 revert。
  • 先看:packages/spec/src/migrations/entries/semantic/18.security-catalog-environment-overlay-refused.ts 的 reason 开头两行;其次 content/docs/permissions/permission-sets.mdx 的两处改动。
  • 两分钟验证:git grep -n "hold one name per deployment" -- packages/spec/src,预期零命中。
  • 需要你做的:无,席位落地。

What changes

  • packages/spec/src/migrations/entries/semantic/18.security-catalog-environment-overlay-refused.ts, reason only. FROM "Positions, permission sets and capabilities hold one name per deployment, and a package registering a name the environment catalog already holds was already refused on a hot install." TO "The environment catalog and a package never hold one name: a package registering a name the environment catalog already holds was already refused on a hot install." The rest of reason and every other field (id, surface, replacement, acceptanceCriteria) are byte-identical.
  • packages/spec/src/kernel/customization-package.ts, the adr_0146_d6_security_catalog line of the OverrideClassificationBasis doc comment. FROM "D6: one name per deployment (permission sets, positions, capabilities)" TO "D6: the security catalog takes no override (permission sets, positions, capabilities)". Comment only.
  • content/docs/permissions/permission-sets.mdx (review N4/N5 of docs(spec): the permission set name describe and the permission sets page say one holder per namespace #33384, added to the card by the seat):
    • the one-holder list docs(spec): the permission set name describe and the permission sets page say one holder per namespace #33384 added now names the environment: "…a platform package (base, system, sys), the environment and the platform-requested guest_portal keep one holder", the same list and order as the PermissionSetSchema.name describe;
    • the "One authoritative store" paragraph: FROM "a grant names its set by name (sys_user_permission_set.permission_set)" TO "a grant names its set by its package and name (sys_user_permission_set.permission_set_package and permission_set)", matching "each granted by its own package and name" below it and the grant object's (package, name) key.
  • .changeset/33388-security-catalog-entry-reason.md: @objectstack/spec patch, since the entry's reason ships in spec dist (dist/migrations/index.{js,mjs}) and in the generated upgrade guide.

Why

Since PR #32742 a permission-set name has one holder per namespace, so "permission sets hold one name per deployment" is false in present tense. The migration entry's subject (an environment-wide row and a package never share a name) still holds, so the reason now opens with that. ADR-0146 D6's reason a permission set takes no override is unchanged, so the gloss names it instead of the old name rule.

Tests

  • No behaviour changes, so no test is added and nothing is ablated. Neither old sentence is pinned: on origin/main, git grep "one name per deployment" hits packages/spec/src only at the two edited lines and at permission.zod.ts:1209 (positions and capabilities, still true), and no test asserts either edited text (the test hits are refusal sentences of other types, comments and one test title; see Acceptance notes).
  • Projections: every projection of the entry is gitignored. After turbo run build --filter=@objectstack/spec…, the regenerated packages/spec/src/migrations/registry.ts and packages/spec/dist/migrations/index.{js,mjs} carry the new sentence and no longer carry the old one. Nothing to commit.
  • On ab082955: pnpm --filter @objectstack/spec typecheck exit 0; pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/migrations src/kernel/customization-package: Test Files 6 passed (6), Tests 263 passed (263) (lock VERDICT command-exit 0).

Acceptance notes

Left as they are, outside this card's surface:

  • packages/objectql/src/registry.ts:2818 and :2880 (the cold-boot and hot-install refusal sentences of SecurityCatalogNameConflictError) still say "Positions, permission sets and capabilities each hold one name per deployment: an assignment names a position or a permission set by its bare name". The refusals stay right where they fire for a permission set (an environment item, co-owners of one namespace, a platform holder), but the stated reason no longer is, since a grant carries permission_set_package. Runtime text in @objectstack/objectql; reported to the seat.
  • ADR-0048's 2026-10-08 addendum, ADR-0146 D6 text (:263, :472), and test comments (showcase-security.ts:32, showcase-d7-default-profile.dogfood.test.ts:65, protocol-boot-hydration-scoped.test.ts:217) and the served-override.test.ts:70 test title: decision records and comments, not touched.
  • Released packages/spec/CHANGELOG.md entries carry the old sentence: release-owned, not touched.

Verification

On ab082955: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 103 commands, run one by one with timeout 900, each exit code captured before any pipe.

  • 101 exited 0 in the loop.
  • pnpm --filter @objectstack/spec run check:skill-examples exited 3 in the loop (PREREQUISITE NOT MET: packages/client-react/dist held no declarations). After building the client and client-react closures it exited 0 ("344 prose examples type-check across 3 surface(s)").
  • pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: it needs every package's dist; only the spec, lint, objectql, client and client-react closures were built), so it is NOT MEASURED here and left to CI.
  • --ran reconciliation: "103 derived famil(ies) accounted for — 102 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".
  • Also run: pnpm --filter @objectstack/spec check:generated exited 1 only because check:test-typecheck's tsc -p died "without diagnostics" under load; check:test-typecheck alone then exited 0 (52 files / 240 errors / 131 pinned signatures held). The other 13 artifacts read up to date, so nothing checked in moves. node scripts/check-nul-bytes.mjs exit 0.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (No formatter ran; pnpm lint is CI's run, not run locally.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed (none checked in moves; see Verification).
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done. (Left to the seat's review.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5prrsuzKUWCmEzwMEb

…saying a permission set holds one name per deployment

The semantic migration entry's reason now opens with its own subject (the
environment catalog and a package never hold one name), the D6 basis gloss
says the security catalog takes no override, and the permission sets guide
lists the environment among the one-holder cases and says a grant names its
set by its package and name.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5prrsuzKUWCmEzwMEb
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/kernel/customization-package.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/kernel/customization-package.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 172 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 ba35dfd78283d5f839eaaaab24764322ae3e263a → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ab082955615e4f4a65dd2287ecf7177aa88009ab
Local-runs: none
Scope: packages/spec/src/kernel/customization-package.ts, packages/spec/src/migrations/entries/semantic/18.security-catalog-environment-overlay-refused.ts + none

Inputs read: card #33388 (body and comments 6882204943, 6884033142, 6884395326), PR #33445 (body, 4-file list, net diff against main), the check-runs on this head. No file in the diff implements or enforces the two hits files (the other two are a guide page and the changeset), so the + side is none; the enforcing code was read on main, not reviewed.

Check-runs on this head when read (2026-11-14): 24 success, 10 skipped, 31 still in progress, 0 failed. Concluded green include Check Changeset, Spec property liveness, Type Check · source gates, Lint · ESLint, Lint · docs & code guards, Build Docs, Check Documentation Links, Governed Surface Queue Guard, PR-body guards, Dogfood Regression Gate. Not concluded: Build Core, Test Core (1..8/8), Test UI (1..16/16), Type Check · workspace / debt ledger / consumer gates, Type Check UI, Build & E2E UI, Live E2E UI, three Lint families. This record judges the contract; it does not stand in for the runs that have not concluded, and the merge waits for them.

① Derived judgments

  • Accept-set: unchanged. Right. The diff touches no Zod schema, no type union member and no constant: in customization-package.ts only two lines of the OverrideClassificationBasis doc comment move; OverrideClassificationBasis, OVERRIDE_TYPE_CLASSIFICATION (permission, position, capability all { disposition: 'excluded', basis: 'adr_0146_d6_security_catalog' }) are byte-identical.
  • Public surface: one published string changes, the reason of the semantic migration entry security-catalog-environment-overlay-refused (ships in spec dist/migrations and in the generated upgrade guide). Right that it is published, right that it is prose only. id, surface, replacement, acceptanceCriteria and the header comment are unchanged in the diff (the hunk is lines 27-29 of reason alone); from "A cold boot now refuses it too" to the end the reason is the same text.
  • Migration matching: the entry's id is what a consumer keys on; it does not move, so nothing that selects or reports the entry changes.
  • No generated artifact checked in carries the old sentence: on main it appears only at the edited line and in released CHANGELOG files (release-owned). Spec property liveness and Type Check · source gates are green.
  • Guide page (content/docs/permissions/permission-sets.mdx): two sentences, both in the card's added scope (6882204943, N4 and N5). Judged in ④ because they state the same contract.

② Semver level

.changeset/33388-security-catalog-entry-reason.md: @objectstack/spec patch. Right: the only published change is the wording of one migration entry's reason plus a doc comment; no accept-set, type, id or behaviour moves, so neither minor nor major applies, and skip-changeset would be wrong because the string ships in dist. No other package publishes anything from this diff (the guide page belongs to no package). Check Changeset is green.

Clause-②: no on the PR body and in the changeset: right, nothing an author could write is newly accepted or refused. Metadata-protocol: none: right, no protocol surface, wire shape or door is touched. The title carries no !, and none is owed.

③ Boundary flags

  • Dev report 6884395326 open_questions: empty. Nothing to answer.
  • Deviation 1 (PR body footer in the harness form): no contract effect; the seat's call.
  • Deviation 2 (Human Minimum item 3 and "Before Submitting" unchecked, pnpm lint left to CI): Lint · ESLint and Lint · docs & code guards are green on this head; three other Lint families had not concluded when read.
  • Out-of-scope finding, class a: packages/objectql/src/registry.ts coldBootConflictMessage (:2818) and the hot-install text of SecurityCatalogNameConflictError (:2880) still say "Positions, permission sets and capabilities each hold one name per deployment: an assignment names a position or a permission set by its bare name". Confirmed on main. The refusals fire correctly; the stated reason is false for permission sets since a grant carries permission_set_package. Correctly left out of this card (runtime text of another package). ESCALATED to the seat: after this PR the migration entry and the runtime refusal it records give different reasons, so this needs its own card if it has none.
  • Out-of-scope finding, no carrier (ADR-0048 addendum, ADR-0146 D6 text, test comments, released CHANGELOGs): accepted as left. Decision records and release-owned text are not this card's to rewrite. Note that ADR-0146 D6 itself still gives "They hold one name per deployment" as its reason; the gloss no longer repeats it, which is the card's TO.

④ Design judgment

18.security-catalog-environment-overlay-refused.ts, reason. New opening: "The environment catalog and a package never hold one name: a package registering a name the environment catalog already holds was already refused on a hot install."

  • True at the hot-install door: refuseSecurityCatalogNameConflicts asks securityCatalogHoldersOtherThan with environment: true, and the ADR-0150 namespace narrowing in add() filters kind === 'package' holders only (permissionSetHoldersMeet "judges package holders alone: the environment's own set and a built-in name are refused by each door as before"). So an environment holder meets every package, permission sets included.
  • True at the cold boot: environmentHeldSecurityCatalogConflicts reports every package-held position or permission-set name the bare slot also holds, with no namespace test, and the boot is refused.
  • True at the save door: permissionSetNameCollisionRefusal, meets() returns true when either side is the environment (holder === null || written === null).
  • Attempt to break it, built-in names: the item seam passes environment: !BUILT_IN_SECURITY_CATALOG_NAMES[type].has(baseName) and the cold boot skips built-in names, so an environment item can sit under a built-in name. That is the environment beside the PLATFORM's declaration, which the code classes kind: 'built-in', not a package ("a built-in name is never available to a package"), and the entry's surface is rows under a name "a configured package declares". The sentence holds as written. The sentence it replaces had the same edge and was additionally false for permission sets.
  • "was already refused on a hot install" and "A cold boot now refuses it too": the pronoun still resolves to "a package registering a name the environment catalog already holds". No drift against surface or acceptanceCriteria ("refused with NAMESPACE_CONFLICT (422) naming the environment catalog as the holder" matches existingHolder: { kind: 'environment' }).
  • Matches the card's TO ("the environment catalog and a package never hold one name").
  • F1, nit: the second fragment of the opening is one 165-character source line (line 28) where every neighbour wraps near 100. Source layout only; the concatenated string is correct (spacing checked) and Lint · ESLint is green. Fixing it would move the head; leave it.

customization-package.ts, adr_0146_d6_security_catalog gloss. "D6: the security catalog takes no override (permission sets, positions, capabilities)".

  • Against the ADR: ADR-0146 D6 bullet "Permission sets, positions and capabilities are not overridable." The gloss now states the decision; it no longer states the ADR's own (now partly stale) reason. True.
  • Against the constant it annotates: exactly the three rows permission, position, capability carry this basis, all excluded; customization-package.test.ts:184 pins that. The parenthesis lists the same three.
  • Against the first consumer: protocol-packaged-base-refusal.ts maps the basis to the citation 'ADR-0146 D6' and refuses a declared override of an excluded type 403 NOT_OVERRIDABLE. "Takes no override" is what the gate does.
  • Verbatim the card's TO. No finding.

permission-sets.mdx (not a hits file; read because it states the same contract).

  • "a grant names its set by its package and name (sys_user_permission_set.permission_set_package and permission_set)": both columns exist and are what the resolver reads (resolve-authz-context.ts GRANT_ROW_COLUMNS; stored-package-references.ts R1). True, and now agrees with "each granted by its own package and name" below it.
  • One-holder list gains "the environment": true at both doors (above), and the list is now the same members in the same order as the PermissionSetSchema.name describe on main ("two packages of one namespace, a package with no namespace, a platform package (base, system, sys), the environment and the platform-requested guest_portal"), which is permissionSetHoldersMeet plus the environment. No finding.

Findings: F1 nit. No must-fix, no should-fix. The round ends on nits.

Implemented-by: claude/project-thread-deqozc
Reviewed-by: session_01FEkQ5mswnWm181XgYcCUag

VERDICT: PASS

Seat note: the escalated registry.ts refusal sentences (~:2818, ~:2880) are already card #33389 (p3, server lane).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag

ACCEPT · PR #33445 (#33388) · head ab082955615e4f4a65dd2287ecf7177aa88009ab · 2026-11-14T21:47Z

席位核对

  • 形态: 4 files, +18/−7 (read from the PR's file list, not the report): the reason of packages/spec/src/migrations/entries/semantic/18.security-catalog-environment-overlay-refused.ts (every other field byte-identical), the adr_0146_d6_security_catalog gloss comment in packages/spec/src/kernel/customization-package.ts, two lines of content/docs/permissions/permission-sets.mdx, one @objectstack/spec patch changeset. No governed path, under 3000 lines: Tier S, seat lands.
  • 前提实测: the head is the one the contract review read; no push since. The premise on the claim holds: the old "hold one name per deployment" sentence was on main in both files until this PR.
  • 门禁 check-runs: on this head 68 runs: 58 success, 10 skipped, 0 failed. The one gate the dev could not run locally (check:dual-build-cjs-loads, needs every dist) is answered by CI's build and type-check runs on this head.
  • 契约复审: CONTRACT_REVIEW_TIER PASS, record 6884489966 on this head (isolated reviewer; Ruling-ref 「你有 fable ,车道契约复审可以自己做」 2026-11-07). One nit, no must-fix or should-fix.
  • 接受的偏差: F1 (one 165-character source line in the entry's reason; the string is right and ESLint is green; fixing it would only move the head). The PR body ends with the harness footer rather than os-dev.md's session-URL form (dev's own note); no contract effect. Clause-②: no / Metadata-protocol: none on PR body and changeset.
  • 范围外去向: the registry.ts refusal sentences (~:2818, ~:2880) that still say a permission set holds one name per deployment: card objectql: the security-catalog name-conflict refusals still say a permission set holds one name per deployment #33389 (p3, server lane), filed before this PR.
  • 落地条款: ready + auto-merge now under the maintainer's standing order 「按照 pm-dispatch 规则复核过的都授权」 (2026-10-25T11:35Z) and 「你应该使用 pm-dispatch 技能审核和落地pr」 (2026-11-07T16:59Z). K = 12 commits on main past this head's affected-set base ba35dfd7 (< 15, no merge of main needed).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 21:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit b392ff7 Nov 14, 2026
69 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-deqozc branch November 14, 2026 22:17
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/s tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: the security-catalog migration entry and the D6 override gloss still say a permission set holds one name per deployment

1 participant