Skip to content

feat(spec)!: retire the permission set's authored managedBy, a provenance key nothing read - #23989

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23891-permission-set-managedby-retired
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23891-permission-set-managedby-retired

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23891
Clause-②: yes

Retires the AUTHORED key PermissionSetSchema.managedBy (remedy 1 of the card; the liveness ledger's dead verdict of 2026-10-11, ADR-0049 enforce-or-remove) with the full spec-property-retirement kit. The key's Setup form row, its help text, its three option labels and their translations in four locales leave with it. The sys_permission_set.managed_by COLUMN, its seeders and the ownership ceremony are untouched, and so is ObjectSchema.managedBy.

What changes for an author

Measured before removal (base 6d1660dc3b)

  • Readers of the authored key: none. The retired bootstrapDeclaredPermissions stamped the column and never read ps.managedBy. packaged-permission-set-lock.ts decides provenance from the artifact. database-loader.ts:906 maps the sys_metadata record envelope's managed_by, not a set body. permissionSetDefinitionFromRow (core) excludes the column from the body.
  • Writers in a permission-set body: the example apps: 0. plugin-security's default sets: 0. Every defineStack fixture: 0 (the tombstone typed every authoring site, and spec's typecheck and the platform-objects typecheck are green). HotCRM 4.0.1 at 1d7148bf2d (the v4.0.1 tag): 0 in its seven *.profile.ts sets, against the control objects: at 7. The one writer was Setup's form row, into stored rows, which the D2 strip heals.
  • objectui at the pin daeb77fc84 (.objectui-sha unchanged since dispatch): PermissionMatrixEditor.tsx:1365 and :1606 read draft.managedBy for the Platform / Package / Custom badge; permission-slice.ts:62 declares managedBy?: string on objectui's OWN PermissionSetDraft type; permission-set-clone.ts:13 lists it in a comment; PermissionMatrixEditor.badge.test.tsx:56-74 and PermissionMatrixEditor.provenanceBadge-7611.test.tsx test the badge. None imports the spec's type for that field, so the pinned objectui build does not break (Post-Task Checklist step 4). Its reader is a display, and its other two inputs (packageId, codeIsArtifact) are the real provenance. No objectui file is touched; the follow-up card is the seat's to file at landing.

The kit

  • packages/spec/src/security/permission.zod.ts: the tombstone managedBy: retiredKey(PERMISSION_SET_MANAGED_BY_REMOVED) with its TSDoc; the packageId TSDoc no longer cites the key.
  • packages/spec/src/security/permission.form.ts: the managedBy row and its decision-record bullet leave, and a one-line note stays in their place.
  • D2 permission-set-managed-by-removed (conversions/registry.ts, MAJOR_18_CONVERSIONS order 67, defined above recordChatterPositionVocabulary per the merge rule); D3 permission-set-managed-by-retired (migrations/entries/semantic/); RETIRED_KEYS_BY_MAJOR[18] security/PermissionSet:managedBy (migrations/entries/retired-keys/); a STEP18_RATIONALE fragment (order 99) in registry.ts.template; a JOIN row in retired-key-removal-version.pin.test.ts.
  • liveness/permission.json: the row stays (dead, a REMOVED note, verifiedAt: 2026-10-16). The status does not move, so the README's per-type counts do not either.
  • Generated: authorable-surface/security.json (one row becomes [RETIRED]) and content/docs/references/security/permission.mdx (gen:docs). The four *.metadata-forms.generated.ts bundles were regenerated by node scripts/check-i18n-bundles.mjs --write, which drops 4 lines per locale, all generator-owned. Nothing generated was hand-edited.
  • Hand-written: content/docs/permissions/permission-sets.mdx (the clone door's dropped-key list no longer names managedBy). In platform-objects, two pins follow the bundle: setup-copy-vocabulary.test.ts drops the leaf from its help-text population and dark leg, and object-lifecycle-panel-echo-decisions.test.ts moves its catalog count from 674 to 673 for the one label that left.
  • packages/spec/vitest.repo-tests.json: the new pin's tree walk reads outside the package, so it runs under the repo project (check:cross-package-test-inputs).
  • .changeset/23891-permission-set-managed-by-retired.md: @objectstack/spec minor (BREAKING, FROM → TO, the one-line fix) and @objectstack/platform-objects patch, with the ADR-0087 registered marker.

The four ratchets: what the route predicts, and what was read

On the tombstone route, the key stays in the walked shape and no def is deleted. The route predicts a [RETIRED] mark on one authorable-surface row and no change to the other three. Read on 076958cb: authorable-surface +1/−1 (the mark); api-surface, api-surface-signatures and json-schema.manifest unchanged; check:generated reports all 14 artifacts up to date.

Pins, and the ablations that name them

packages/spec/src/security/permission-set-managed-by-retirement.test.ts (17 cases): the refusal at the key's path with the prescription, for every value; the permission write door; the defineStack envelope (STACK_SCHEMA_INVALID, 422); a @ts-expect-error at an authoring site; the form offers no row; a stored row replays clean through applyConversionsToStoredItem; an installed package's artifact is refused before the conversion and accepted after it, with a lit control; strip, idempotence, and retirement from the load path; ADR-0087 registration; and a tree-scoped absence walk. Because ObjectSchema.managedBy is live, the walk flags managedBy only in an object literal that also carries a key only a permission set declares, and it states that bound.

Ablations (scripts/ablation-replace.mjs, each restored to the HEAD blob with git diff HEAD empty):

  • The key restored as z.enum(['package','platform','user']).optional(): 7 tests go red (the parse refusal, every value, the write door, defineStack, the tsc-site parse leg, the artifact before/after, and the removal-version JOIN row).
  • The D2 conversion unwired from MAJOR_18_CONVERSIONS: 4 go red (the stored row, the artifact, strip/idempotence, chain wiring).
  • The @ts-expect-error deleted: check:test-typecheck goes red, naming the file. The type channel is really compiled.

Verification (on 076958cb, after merging main)

  • @objectstack/spec: build exit 0; typecheck exit 0; check:generated exit 0; check:liveness, check:empty-state, check:exported-any, check:dual-source-exports and check:variant-docs exit 0. Vitest runs: src/ in four batches, 681 files and 20,447 tests, all passing. After the merge: src/security, src/conversions, src/migrations and src/shared plus the two major-18 merge tests and scripts/liveness, 83 files and 2,445 tests, all passing.
  • @objectstack/platform-objects: vitest 73 files and 1,136 tests, all passing; typecheck exit 0. @objectstack/runtime permission-set-clone.test.ts: 23 passing. check:i18n exit 0.
  • dispatch-gates --commands on this diff: 113 families; --ran: 113 accounted, 110 run, 3 NOT MEASURED. NOT MEASURED (prerequisites of the local tree, not readings of this diff): check:plugin-teardown-shape --self-test (exit 3, shallow clone), check:skill-examples (exit 3, client-react declarations not built), check:dual-build-cjs-loads (exit 3, packages never built here). check:dts-closure exited 1 on 54 packages missing .d.ts, which an earlier OS_SKIP_DTS=1 closure build left in this tree. None of these is a package this diff changes, and CI checks out fresh.

Acceptance notes

  • packages/runtime/src/domains/permission-set-clone.ts still lists managedBy in PERMISSION_SET_CLONE_DROPPED_KEYS. After this PR, no set body that reaches it can carry the key, so the entry is inert, not wrong. It is outside this card's file surface and is left alone. Carrier: none.
  • objectui's provenance badge reader (the sites listed above) is the seat's follow-up card at landing.
  • The out-of-repo consumer population of @objectstack/spec beyond HotCRM 4.0.1 is NOT MEASURED.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01D3Ur6XgfkkaDUCCTkviXAr

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 13 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/authorable-surface/security.json, packages/spec/liveness/permission.json, packages/spec/src/migrations/entries/retired-keys/18.security__PermissionSet__managedBy.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/data-modeling/fields.mdx (via managedBy (literal, a string literal in apply; a string literal in sections; a string literal in summary))
  • content/docs/data-modeling/objects.mdx (via managedBy (literal, a string literal in apply; a string literal in sections; a string literal in summary))
  • content/docs/data-modeling/seed-data.mdx (via managedBy (literal, a string literal in apply; a string literal in sections; a string literal in summary))
  • content/docs/permissions/authentication.mdx (via managedBy (literal, a string literal in apply; a string literal in sections; a string literal in summary))
  • content/docs/permissions/profiles.mdx (via PermissionSetSchema (symbol, a top-level const))
  • content/docs/permissions/system-context.mdx (via managedBy (literal, a string literal in apply; a string literal in sections; a string literal in summary))

⛔ 7 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), managedBy (literal, a string literal in apply; a string literal in sections; a string literal in summary))
  • content/docs/releases/v13.mdx (via PermissionSetSchema (symbol, a top-level const))
  • content/docs/releases/v16.mdx (via managedBy (literal, a string literal in apply; a string literal in sections; a string literal in summary))
  • content/docs/releases/v17/17-0.mdx (via PermissionSetSchema (symbol, a top-level const), managedBy (literal, a string literal in apply; a string literal in sections; a string literal in summary))
  • content/docs/releases/v17/17-5.mdx (via retiredAfter (symbol, a field of const object permissionSetManagedByRemoved), retiredFromLoadPath (symbol, a field of const object permissionSetManagedByRemoved))
  • content/docs/releases/v17/17-6.mdx (via managedBy (literal, a string literal in apply; a string literal in sections; a string literal in summary))
  • content/docs/releases/v17/index.mdx (via managedBy (literal, a string literal in apply; a string literal in sections; a string literal in summary))

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
  • 5 changed file(s) yielded no anchor (packages/spec/authorable-surface/security.json, packages/spec/liveness/permission.json, packages/spec/src/migrations/entries/retired-keys/18.security__PermissionSet__managedBy.ts, …) — pages documenting those are invisible to this run
  • 8 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 — 142 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 022c6e799e88ae192e124c80e1cc343bda54d7bf → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 022c6e799e88ae192e124c80e1cc343bda54d7bf

⚠️ 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 022c6e799e88ae192e124c80e1cc343bda54d7bf → 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: CONTRACT_REVIEW_TIER
Head-sha: 076958cb25d7092d7ebbb6151497719893f97247
Local-runs: none
Scope: packages/spec/src/conversions/registry.ts, packages/spec/src/migrations/entries/retired-keys/18.security__PermissionSet__managedBy.ts, packages/spec/src/migrations/entries/semantic/18.permission-set-managed-by-retired.ts, packages/spec/src/migrations/registry.ts.template, packages/spec/src/security/permission.form.ts, packages/spec/src/security/permission.zod.ts + packages/spec/liveness/permission.json, packages/spec/authorable-surface/security.json, .changeset/23891-permission-set-managed-by-retired.md, packages/spec/src/security/permission-set-managed-by-retirement.test.ts, packages/spec/src/shared/retired-key-removal-version.pin.test.ts, packages/spec/vitest.repo-tests.json, packages/platform-objects/src/apps/translations/{en,es-ES,ja-JP,zh-CN}.metadata-forms.generated.ts, packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts, packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts, content/docs/permissions/permission-sets.mdx, content/docs/references/security/permission.mdx, packages/spec/src/shared/retired-key.ts, packages/spec/src/conversions/apply.ts, packages/spec/src/conversions/stored.ts, packages/spec/src/conversions/walk.ts, packages/spec/src/conversions/retired-after.census.test.ts, packages/spec/src/kernel/metadata-type-schemas.ts, packages/spec/src/stack.zod.ts, packages/metadata-core/src/artifact-forward-conversion.ts, packages/metadata/src/plugin.ts, packages/metadata/src/loaders/database-loader.ts, packages/metadata-protocol/src/protocol.ts, packages/runtime/src/app-plugin.ts, packages/runtime/src/load-artifact-bundle.ts, packages/runtime/src/domains/permission-set-clone.ts, packages/objectql/src/plugin.ts, packages/objectql/src/registry.ts, packages/objectql/src/engine.ts (on origin/main at d050a8fc63, the #23962 door), packages/core/src/security/permission-set-row-definition.ts, packages/plugins/plugin-security/src/objects/default-permission-sets.ts, packages/plugins/plugin-security/src/packaged-permission-set-lock.ts, .claude/skills/spec-property-retirement/SKILL.md, .github/workflows/ci.yml, .github/workflows/lint.yml, and in objectui at the pin daeb77fc84: packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx, permission-slice.ts, permission-set-clone.ts. The rest of the diff is the lane seat's ACCEPT at TIER_DEFAULT.

Inputs: card #23891 (body; triage 6215910642, claim 6217680203, dev report 6219988389), card #23904 (body, its seven comments, its PR #23962 landed as d050a8fc63), PR #23989 (body, 20-file list, net diff against merge-base 022c6e799e, 685+/47-), and the 41 check-runs on 076958cb25: 36 success, 5 skipped (Console Pin Gate, Dogfood shards, Packed-tarball smoke, Temporal Conformance: each skipped by its own pull_request trigger condition, not by this diff), 0 failure. Read-only: git fetch plus git show / git grep / git diff at the sha, and unauthenticated REST.

① Derived judgments

Refused by name at every authoring, save and publish door — RIGHT. managedBy: retiredKey(PERMISSION_SET_MANAGED_BY_REMOVED) on PermissionSetSchema, which is a strictObject. retiredKey() (shared/retired-key.ts:185) is z.never({ error }).optional(): every value is refused at path [managedBy] with the prescription, and z.input types the key never so tsc fails first. The doors that parse with that schema, all read at the sha: definePermissionSet() (permission.zod.ts:1067, a direct .parse); defineStack / os validate / os build (stack.zod.ts:804, permissions: z.array(PermissionSetSchema); the D2 entry is retiredFromLoadPath, so the authoring funnel never strips it, pinned by the "retired from the load path" case); PUT /api/v1/meta/permission/:name and the publish and restore doors (metadata-type-schemas.ts:166, permission: PermissionSetSchema, the entry saveMetaItem validates against; pinned by identity in the new test). The prescription follows the five house conventions (fully-qualified key first, "was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0086 D3)", a why clause, "Delete the key" plus the mechanism that really decides provenance, the fixed os migrate meta --from 17 sentence); retired-key-migrate-sentence.test.ts is in the Test Core shards, green.

A stored row and an installed artifact still load, by the existing ADR-0087 seams — RIGHT, and the seams are named.

  • Stored permission rows: applyConversionsToStoredItem('permission', row) (conversions/stored.ts) maps permission to the permissions collection (shared/metadata-collection.zod.ts:86), wraps the row as { permissions: [row] } and replays with includeRetired: true; mapCollection(stack, 'permissions', …) plus stripKeys(ps, ['managedBy']) deletes the key whatever it holds, one notice per set, named by name. Its non-test callers at the sha: metadata-protocol/src/protocol.ts:6519 (the metadata read path Setup and Studio read through), metadata/src/loaders/database-loader.ts:884 (the stored loader), objectql/src/plugin.ts:2841. So the one measured writer, Setup's form row into stored rows, is healed on read, and the console's draft never carries the key to a save.
  • Installed artifacts: applyArtifactForwardConversions (metadata-core/src/artifact-forward-conversion.ts) replays every retiredFromLoadPath entry when the artifact's engines.protocol floor is below the runtime label (18.0.0 for 18.0.0-next.2; any 17.x build qualifies, HotCRM 4.0.1 included), and the per-entry half opens for a floor at or below retiredAfter: '17.7.0', and the major-step half for a bare ^18 floor. Callers: metadata/src/plugin.ts:1155 (_convertArtifactForward, the artifact door), runtime/src/app-plugin.ts:317 (forwardConverted, the artifact boot), runtime/src/load-artifact-bundle.ts:72 and :75. After this branch merges main, the entry also rides fix(spec,objectql): installed pages carrying a retired declaration convert at load instead of stopping the kernel build #23962's ObjectQL.convertPackageBodyForward (objectql/src/engine.ts at d050a8fc63, registerApp and the manifest service for a body declaring a range): same function, same registry, no second table, nothing for this PR to add.
  • The p0 Kernel build refuses an INSTALLED package page with isDefault on a non-record page (a69a40a572 / #23708): every hosted environment with HotCRM 4.0.1 fails to boot #23904 class on the chains fix(spec,objectql): installed pages carrying a retired declaration convert at load instead of stopping the kernel build #23962 did NOT window (sys_packages hydration registry.installPackage(rec.manifest) at service-package/src/index.ts:715, protocol.installPackage; the seat's Package doors outside #23904's window still meet a retired page declaration unconverted: sys_packages boot hydration and POST /api/v1/packages call registry.installPackage directly, and the marketplace install's side readers read the raw ledger entry #23974): packages/objectql/src/registry.ts carries no permissions handling and no PermissionSetSchema reference in non-test code, and getMetadataTypeSchema has no caller in objectql, so no kernel registration strict-parses a permission set; the key is inert there, not a refusal. packaged-permission-set-lock.ts reads ql.registry.listItems('permission') and judges provenance from packageId and the artifact, never the key (card body, confirmed by grep). plugin-security/objects/default-permission-sets.ts parses its own sets at module load; none authors the key (the tree walk asserts it reached that tree, securitySources above 50; tsc is green). No boot built from an installed package or a stored row stops on this declaration.
  • The new pin covers each leg: stored-row replay through applyConversionsToStoredItem with the named notice; artifact refused before and accepted after applyConversions({ includeRetired: true }), with a lit control that stays refused on both sides; strip, idempotence, load-path retirement; and the ablations in the PR body (key restored: 7 red; D2 unwired: 4 red; @ts-expect-error deleted: check:test-typecheck red) name what each piece carries. The test is declared in vitest.repo-tests.json for the tree walk (check:cross-package-test-inputs, lint.yml:3573, green).

The column, its seeders, the ownership ceremony and ObjectSchema.managedBy untouched — RIGHT. The file list touches nothing under packages/plugins/plugin-security, packages/core, packages/platform-objects/src/**/sys_permission_set* or data/object.zod.ts. core/src/security/permission-set-row-definition.ts builds the ceremony's body from a fixed candidate list (name, label, description, the five facet columns, admin_scope) and only then filters by the schema's shape keys, so managed_by never travels into a body and the tombstone staying in the walked shape changes nothing there (the PR body's claim, verified). database-loader.ts:906 maps the sys_metadata envelope's managed_by, not a set body. The clone door's dropped-key list (runtime/src/domains/permission-set-clone.ts:107) still names managedBy; inert after this PR, see ③.

Ledger row follows the tombstone route — RIGHT. The key stays in the walked shape, so the row stays (dead, verifiedAt: 2026-10-16) with the playbook's note template: REMOVED date, tombstoned at the schema, stripped by the protocol-18 conversion, "the entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent)", what to do instead. Status unchanged, so the README counts are unchanged; "Spec property liveness" is green.

The four ratchets read as the route predicts — RIGHT. Tombstone route: authorable-surface/security.json gains exactly the [RETIRED] mark on one row (+1/-1); api-surface, api-surface-signatures and json-schema.manifest record exported names and def existence, and no def leaves, so all three are byte-identical: none appears in the file list, and the gates that would red on drift (check:authorable-surface at lint.yml:5457, check:api-surface at :6333, check:generated --reconcile-only at :5336) are green. The generated reference row (permission.mdx:121) carries the tombstone's [REMOVED] text.

ADR-0087 registration — RIGHT. D2 permission-set-managed-by-removed: toMajor: 18, retiredFromLoadPath: true, retiredAfter: '17.7.0' (the census rule under an 18.0.0-next label is the newest stable, 17.7.0; retired-after.census.test.ts is rule-based and green), surface in prose, fixture disjoint, expectedNotices: 2 equals the key count. Inserted in MAJOR_18_CONVERSIONS at its identifier's sort position (permissionR… before permissionS… before recordChatter…), order: 67 = base max 66 + 1, defined directly above recordChatterPositionVocabulary, the entry that follows it. RETIRED_KEYS_BY_MAJOR[18] entry security/PermissionSet:managedBy as its own file. One D3 semantic entry for the family, reason and acceptanceCriteria non-empty, naming the D2 id. STEP18_RATIONALE fragment at its id's sort position, order: 99 = base max 98 + 1. JOIN row in retired-key-removal-version.pin.test.ts. Changeset marker adr-0087: registered permission-set-managed-by-removed, permission-set-managed-by-retired.

Form, bundles, docs — RIGHT. The managedBy row with its help text and three options leaves permission.form.ts with a one-line note in place; the four bundles drop exactly the generator-owned managedBy: { label, helpText } block (4 lines per locale; the option labels were never in the bundles, so "their translations in four locales" in the PR body overstates by the labels only); check:i18n (lint.yml:6550) green; the two platform-objects pins follow (help-text leaf list, catalog 674 to 673). The tree-scoped absence walk declares its radius (the five roots plus example src/) and its bound (a managedBy key beside a set-only key), with anti-vacuity counts; no allowlist file.

② Semver level

@objectstack/spec: minor, with the BREAKING banner, a FROM to TO table, the one-line fix and the ADR-0087 marker: what the diff publishes (a tombstone that refuses a previously accepted authored key is a narrowing, and major is refused in the release window by check-changeset-no-major). @objectstack/platform-objects: patch, for the four regenerated bundles whose output moved. No other published package's output moves (content/docs is not a package; packages/runtime is untouched). Clause-②: yes (narrowing) in the changeset; the PR body carries Clause-②: yes, the claim's verbatim line. Correct.

③ Boundary flags

  1. PR body premise now stale, no code effect. "Kernel build refuses an INSTALLED package page with isDefault on a non-record page (a69a40a572 / #23708): every hosted environment with HotCRM 4.0.1 fails to boot #23904 has no PR yet" was true when written; PR fix(spec,objectql): installed pages carrying a retired declaration convert at load instead of stopping the kernel build #23962 landed as d050a8fc63 (17:06Z), after this head was pushed. d050a8fc63 is on origin/main and not on this branch; git merge-tree of the two is clean. After the merge, order: 67 is shared by three entries (page-is-default-off-record-page-removed, page-template-full-width-to-default, this one), which the list's own rule allows ("two retirements in flight may take the same number; they then apply in id order"); the three walk disjoint collections (pages vs permissions), so order among them is unobservable. scripts/conversions-major18-merge.test.ts and step18-rationale-merge.test.ts hold the shape on the queue's speculative merge. Seat: merge main before queueing, or let the queue leg prove it; the sentence in the body may be left.
  2. Dev deviations, each answered. content/docs/permissions/permission-sets.mdx (the clone door's dropped-key list loses one word): the playbook's docs row requires it, in scope. The two platform-objects pins and vitest.repo-tests.json: consequences of the listed surface, in scope. The tree walk's declared bound (live ObjectSchema.managedBy): a stated limit, with tsc and the parse doors owning the residue; correct. Full spec vitest split into batches: CI's six Test Core shards ran the whole set, green. Changeset vs body Clause-② spelling: both present, same clause.
  3. Open question 1 (the isDefault row alone at colSpan 1): A, no layout change on this card; cosmetic, outside the ask.
  4. Open question 2 (PERMISSION_SET_CLONE_DROPPED_KEYS still names managedBy): A. After this PR no body that reaches the door can carry the key (stored rows are stripped on read; the door drops before it parses), so the entry is inert defence and the doc's list, which no longer names it, describes observable behaviour. The constant and the doc now differ by one word; the seat may fold the constant's removal into the objectui follow-up card or leave it.
  5. objectui at the pin daeb77fc84 (the seat's follow-up card at landing): nothing in this PR makes the pinned console build or behave worse than "the Platform badge arm becomes unreachable". Verified: the only non-test sites are readers (PermissionMatrixEditor.tsx:1365 and :1606, draft.managedBy === 'platform' / === 'package' || packageId || codeIsArtifact), the type on objectui's own PermissionSetDraft (permission-slice.ts:62) and a comment (permission-set-clone.ts:13); no writer sets the key on a draft or a save body, and the drafts it reads come through protocol.ts:6519, which strips the key, so no Studio save can carry it to the refusing door. The package arm is redundant with packageId || codeIsArtifact, so packaged sets keep their badge. The Setup form row is served from spec's form definition and simply disappears. No spec type is imported for that field, so the pinned build is unaffected (Type Check lanes green). The Console Pin Gate check is skipped on every pull_request by its own condition (ci.yml:2867), not by this diff.
  6. Not measured, declared: the out-of-repo consumer population beyond HotCRM 4.0.1, stated in the changeset's warning; the three locally NOT MEASURED families (check:plugin-teardown-shape --self-test, check:skill-examples, check:dual-build-cjs-loads) ran in CI (lint.yml:6487 among them), green.
  7. Escalation to the seat: none blocking. Two items for the landing act: (a) merge main (fix(spec,objectql): installed pages carrying a retired declaration convert at load instead of stopping the kernel build #23962) before the queue and watch the two step-18 merge tests; (b) the objectui follow-up card should also cover permission-slice.ts:62 and the badge tests the PR body lists, so the Platform arm leaves with its type.

Implemented-by: claude/issue-23891-permission-set-managedby-retired
Reviewed-by: session_01HjR8RUwTPQ3TgbfKihmxp9

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · PR #23989 @ 076958cb25 (#23891) · domain:spec seat 1 · os-elon-musk · session session_01HjR8RUwTPQ3TgbfKihmxp9 · 2026-10-16T17:40Z

Reviewer of record. Every item below was checked against GitHub and the code, not against the report (6219988389 on #23891).

At landing, this seat: files the objectui card for the provenance badge (PermissionMatrixEditor.tsx:1365 / :1606, permission-slice.ts:62, permission-set-clone.ts:13, and the badge tests). The Platform arm becomes unreachable, and the badge's real provenance is packageId / the artifact.

The PR goes to the merge queue in this act. The cloud dev session is archived at this ACCEPT.


Generated by Claude Code

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

Projects

None yet

2 participants