Skip to content

feat(spec)!: retire PositionGrantVia 'name' and the Setup by-name grant readers - #26398

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-9lmgya
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-9lmgya

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread
Fixes #26022

Clause-②: no (narrowing)
Metadata-protocol: none

维护者速读

What changes

  • @objectstack/spec: PositionGrantVia (packages/spec/src/contracts/security-service.ts) is now 'definition'. The docblock no longer describes the position-name fold. The security plugin already serves only via: 'definition' (security-plugin.ts describePositionGrants), so the wire answer does not change.
  • ADR-0087: a new D3 semantic entry position-grant-via-name-retired registers the retirement. The type describes a served answer, not a stack collection or a metadata type, so there is no D2 conversion source. The changeset carries the registered disposition.
  • Setup → Positions (PositionGrantCells.tsx): the granted-sets cell shows each set's name alone. The "(by name)" / 「(同名)」 badge, its hint, and the i18n keys engine.position.grants.byName and engine.position.grants.byNameHint (en and zh) are removed. The served-entry filter accepts only via: 'definition'.
  • Setup → Users on a managed environment (managedEnvironmentUsers.tsx): the client-side name fold is retired. A held position now counts only by the sets its definition lists. The permission-set catalogue read, catalogueRead, and PositionRegistrySource.getTypeStatus existed only for the fold. Measured: no other reader in the file or in its one caller, objectViewIdentity.tsx. All three are removed, including the caller's getTypeStatus line. The observable change: a holder of a position whose name matches a permission set the definition does not list is now marked Not authorized, unless something else grants them a set beyond the baseline. That matches what the server enforces.
  • Docblock: packages/rest/src/meta-item-read-gate.ts (describeMetaPositionList) now says via: 'definition'.
  • Fixture triage (consumer radius of the narrowed value, respelled to a declared set): packages/runtime/src/domains/meta-list-projection-parity.test.ts and ui/packages/data-objectstack/src/metadata-client.listInclude-25333.test.ts still served via: 'name' from their doubles. Both now serve { name: 'forecasting', via: 'definition' }.

Tests

  • managedEnvironmentUsers-13330.test.tsx: the name-fold pin is inverted. A holder of closer (a position that shares its name with a set but lists only a baseline set) reads baseline-only. The control holder of sales_rep reads beyond-baseline. The registry is asked only for position. The "catalogue cannot be read" case is deleted because the read it covered is gone.
  • ResourceListPage.positionGrants-25333.test.tsx: the fixture serves only declared sets. Each badge's text is exactly the set name and carries no title.
  • Ablation, one-time: re-adding a name-based count, beyondBaseline(position) || in the position branch, turns three cases red, the new one among them. The file was restored, and its blob equals HEAD (044d3b83d42f).
Local readings (head 1efc50f, then merged head 1a70ed5)
  • pnpm --filter @objectstack/spec build: exit 0. check:generated: exit 0, all 14 artifacts up to date. The api-surface is unchanged because the export name stays.
  • pnpm --filter @objectstack/spec test: 726/726 files, 21984 tests passed.
  • cd ui && vitest run on the three touched test files: 3/3 files, 25/25 tests. Ablation run: 3 failed / 13 passed, as intended, then restored.
  • cd ui && vitest run on app-shell/src/views/__tests__/, metadata-admin/ResourceListPage*, zh-base-term, all of data-objectstack, and i18n objectView-config-keys-retired-4730: 182/182 files, 1871/1871 tests, on both heads.
  • packages/runtime meta-list-projection-parity.test.ts: 1/1 file, 878/878 tests.
  • Type checks: @object-ui/app-shell type-check (both heads), @object-ui/data-objectstack type-check, @objectstack/spec typecheck, @objectstack/rest typecheck and @objectstack/runtime typecheck: exit 0. Runtime first reported TS2307 for @objectstack/driver-turso, an unbuilt dist. It was green after building that package.
  • dispatch-gates --commands on the final head 1a70ed5: 94 commands, all exit 0. --ran reconciliation: 94 derived, 94 run, 0 NOT-MEASURED, a derived zero.
Full dogfood battery (final head 1a70ed5)

pnpm --filter @objectstack/dogfood test under the verify lock: 302 files (2 failed, 297 passed, 3 skipped), 2549 tests (10 failed, 2521 passed, 18 skipped). VERDICT command-exit 1.

All 10 failures are in test/authz-probe-blind-spot.test.ts (4) and test/authz-conformance.test.ts #17111 (6). They count text in packages/rest/src/rest-server.ts and packages/rest/src/rest-route-ledger.ts, for example the control this.routeManager.register( is expected at 62 and found at 63. This PR does not touch either file or packages/qa/dogfood. git diff origin/main on all three is empty, so the same counts fail on origin/main (e931621). They are main's, not this change's.

10 reds in authz-conformance.test.ts and authz-probe-blind-spot.test.ts are main's at origin/main abb9046 (same 10, measured on a detached worktree), anchored on #26400; not this PR's.

Acceptance notes

Verification

  • Requires one CONTRACT_REVIEW_TIER contract review before the queue, as the claim records.
  • No governed surface is touched. Changed lines are about 203, under the 3,000 threshold.

Generated by Claude Code

…ders in Setup

Narrows PositionGrantVia to 'definition', registers the ADR-0087 D3 entry
position-grant-via-name-retired, drops the Setup position list's by-name
badge and its two i18n keys, and retires the client-side name fold in the
managed-environment Users marker with its pins.

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

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/spec, touching 4 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/rest/src/meta-item-read-gate.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/kernel/contracts/index.mdx (via ISecurityService (symbol, a top-level interface))
  • content/docs/kernel/runtime-services/index.mdx (via ISecurityService (symbol, a top-level interface))
  • content/docs/permissions/attachments-access.mdx (via ISecurityService (symbol, a top-level interface))

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

  • content/docs/releases/v17/17-0.mdx (via ISecurityService (symbol, a top-level interface))
  • content/docs/releases/v17/17-5.mdx (via ISecurityService (symbol, a top-level interface))
  • content/docs/releases/v17/17-6.mdx (via ISecurityService (symbol, a top-level interface))
  • content/docs/releases/v17/17-7.mdx (via ISecurityService (symbol, a top-level interface))
  • content/docs/releases/v18/18-0.mdx (via ISecurityService (symbol, a top-level interface), /api/v1/meta/position (route, a path literal in acceptanceCriteria))

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
  • 1 changed file(s) yielded no anchor (packages/rest/src/meta-item-read-gate.ts) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 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 — 149 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 e677770ae03bd7a5b20d4cd44e1f66137add1910 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json e677770ae03bd7a5b20d4cd44e1f66137add1910

⚠️ 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 e677770ae03bd7a5b20d4cd44e1f66137add1910 → 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

Claim: contract review of PR #26398 at head 1a70ed572c398971bbb57877b1eb65e7fcf6dd0d · 2026-10-27T01:59Z
Session: session_01WGufzfhobWEgUUrjz4rrbh
Account: os-support-ai
Seat: domain:spec#1 (seat post #6017)

The dispatching seat's own at-tier review: the diff touches packages/spec/src/** (non-test) and the PR body declares a narrowing, so the record is served at CONTRACT_REVIEW_TIER by an isolated at-tier subagent and lands here as one ## Contract review comment. ⛔ No label or assignee change.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1a70ed572c398971bbb57877b1eb65e7fcf6dd0d
Local-runs: none
Scope: packages/spec/src/contracts/security-service.ts, packages/spec/src/migrations/entries/semantic/18.position-grant-via-name-retired.ts, packages/rest/src/meta-item-read-gate.ts, packages/runtime/src/domains/meta-list-projection-parity.test.ts, ui/packages/app-shell/src/views/managedEnvironmentUsers.tsx, ui/packages/app-shell/src/views/tests/managedEnvironmentUsers-13330.test.tsx, ui/packages/app-shell/src/views/metadata-admin/PositionGrantCells.tsx, ui/packages/app-shell/src/views/metadata-admin/ResourceListPage.positionGrants-25333.test.tsx, ui/packages/app-shell/src/views/metadata-admin/i18n.ts, ui/packages/app-shell/src/views/objectViewIdentity.tsx, ui/packages/data-objectstack/src/metadata-client.listInclude-25333.test.ts + .changeset/26022-retire-position-grant-via-name.md, packages/plugins/plugin-security/src/security-plugin.ts (describePositionGrants, :3516-3577), packages/plugins/plugin-security/src/describe-position-grants.test.ts, packages/spec/src/migrations/entries/README.md, packages/spec/src/migrations/types.ts, packages/spec/src/migrations/entries/semantic/18.visibility-alias-fold-unexported.ts (the TS/API-surface precedent), packages/spec/src/migrations/semantic-relevance.test.ts, packages/spec/api-surface/contracts.json, scripts/check-adr-0087-registration.mjs, scripts/check-changeset-no-major.mjs, scripts/pm/clause2-line.mjs, docs/adr/0087-metadata-protocol-upgrade-contract.md, .changeset/24757-retire-position-name-fold.md, .changeset/config.json, packages/qa/dogfood/test/authz-probe-blind-spot.test.ts, packages/qa/dogfood/test/authz-conformance.test.ts, packages/qa/dogfood/test/authz-conformance.matrix.ts, ui/packages/app-shell/src/index.ts, ui/packages/app-shell/src/views/metadata-admin/index.ts, ui/packages/app-shell/package.json

Inputs: card #26022 (body and all seven comments, through the dev report 6431841169), PR #26398 (body, 12-file list, both comments, no review comments, no reviews), the net diff against main (439 lines, +107/-96), and the check-runs on the head, read 2026-10-27T02:01:25Z. Base-side code was read on the local checkout at origin/main 13136f7 (the CI base abb9046 is an ancestor; the head commit is not present locally, so the head side is the diff itself). Nothing was built, run or re-run.

① Derived judgments

  1. PositionGrantVia loses 'name' — a public-surface narrowing, named right. @objectstack/spec/contracts publishes the type; it types PositionGrantSummary.permissionSets[].via on ISecurityService.describePositionGrants. The only producer on main is security-plugin.ts describePositionGrants (:3549-3577), which pushes { name, via: 'definition' } and nothing else, pinned by describe-position-grants.test.ts:141 (every served via is 'definition'). So after the diff the type describes exactly the served accept set. api-surface/contracts.json records PositionGrantVia (type) by name only, so the snapshot does not move — the dev's "api-surface unchanged" reading is right, and check:api-surface is structurally blind to this narrowing, which is why the changeset and the D3 entry are the carriers.
  2. Nothing emits or reads via: 'name' after the diff. Measured on the base tree (origin/main 13136f7) minus the hunks: every code hit for via: 'name', via === 'name', 'definition' | 'name', grants.byName and byNameHint is inside the diff's own removals. What survives is prose only: security-plugin.ts:3535-3536 ("the contract's 'name' member is no longer served" — stale once the member is gone; see ③), describe-position-grants.test.ts:13 and :140 ("via: 'name' is never served" — still a true sentence), the pending .changeset/24757-retire-position-name-fold.md ("retired separately in spec: retire PositionGrantVia 'name' (the position-name fold's grant mark) once #24757's runtime retirement lands #26022" — accurate as a record), and ADR-0138's two historical mentions of the fold. POSITION_NAME_FOLD_EVENT and the warning are Setup: a position's same-named permission set is in force but invisible — the set's Assigned Users panel is empty and the position reads as granting nothing #24757's runtime half, outside this card. content/docs, apps/docs and skills carry no mention of the badge, the keys or the member; the docs-drift row is a RELEASE-OWNED page, read-only by rule.
  3. REST pass-through — docblock only, right. The meta-item-read-gate.ts hunk rewrites two comment lines on describeMetaPositionList; the code places the security service's answer and reads no via. No behaviour moves.
  4. PositionGrantCells.tsx — right. PositionGrantedSet.via narrows to 'definition', readGrantedSets keeps only via === 'definition', the badge text, its span and the title go. One shift to notice: the old docblock promised a by-name grant was "marked, never hidden"; the new filter hides any entry whose via is not 'definition'. Only a server older than fix(plugin-security)!: retire the position-name fold; a namesake set is reported, never granted (#24757) #26064 could serve one, the console and the spec are in the same fixed changeset group (.changeset/config.json), and the filter already dropped unknown values before this diff, so this is consistent, not a regression.
  5. managedEnvironmentUsers.tsx — the client fold retired, the unknown-stays-unknown rule kept. The ensureType('permission') read, setNames, catalogueRead and PositionRegistrySource.getTypeStatus served only the fold: the repo-wide grep finds no other reader of getTypeStatus through PositionRegistrySource, and objectViewIdentity.tsx:62 is the single UserGrantLoader construction site, whose getTypeStatus line the diff drops. After the diff a failed ensureType('position') answers [], every held position then has sets === undefined, and the user lands in uncertain — never "not authorized" — exactly as the rewritten docblock says. The 13330 pin is inverted (a closer holder reads baseline-only, a sales_rep holder beyond-baseline, the registry asked only for position); the "catalogue cannot be read" case is deleted because the read it covered no longer exists, so there is no surviving assertion to duplicate (see ③).
  6. The two i18n keys are not a published surface. engine.position.grants.byName / byNameHint leave both ENGINE_STRINGS_EN and ENGINE_STRINGS_ZH with no reader left. t leaves the metadata-admin barrel as translateMetadataAdmin, but ui/packages/app-shell/src/index.ts does not re-export it and package.json exports names dist leaves none of which is i18n; a missing key echoes the key by design. Naming them in the changeset is informative, and they rightly carry no semver weight.
  7. Fixture respellings — right. The runtime parity double (:347, :679) and the data-objectstack listInclude fixture (:38) served { name: 'sales_manager', via: 'name' }; both now serve { name: 'forecasting', via: 'definition' }, keeping each pin's shape (two positions, distinct sets) while serving only a value the type still admits.
  8. ADR-0087 — the D3 entry is registered the way the registry asks. entries/semantic/18.position-grant-via-name-retired.ts carries id position-grant-via-name-retired, so the filename follows the rule the generator enforces (the protocol major, a dot, then the id); there is no index to edit (the README: the directory listing is the index; gen:migration-registry runs on install, build, typecheck and test). The literal matches SemanticMigration and mirrors the TS/API-surface precedent 18.visibility-alias-fold-unexported.ts; it omits conversionIds and relevantWhen, which is right — the type has no stored row and no stack key to convert or to ask about, and semantic-relevance.test.ts enumerates only entries that carry a question. No D2 conversion is owed for the same reason. The alternative disposition not-required (type-surface-only ...) is closed to this PR by its predicate 2 (no spec diff) since the diff touches packages/spec/src, so registered is the one fitting disposition, and the id is new in this diff (gate rules R3/R4 satisfied). The entry leaves ADR-0087, ADR-0131 D3/D4 and ADR-0049 ids in the code. The retirement follows Permission-set resolution binds by POSITION NAME while sys_position_permission_set sits near-empty — confirm name-based resolution is the intended mechanism, not an accident the empty junction hides #13419 ruling B as the card and triage record it; it aligns with ADR-0131 rather than reversing a recorded decision, so no new ADR is owed.

② Semver level

  • Changeset .changeset/26022-retire-position-grant-via-name.md: @objectstack/spec: patch, @object-ui/app-shell: patch, both in the fixed group. Body carries **BREAKING**, Clause-②: no (narrowing), Metadata-protocol: none, a Ruling-ref: line, the FROM-to-TO migration (TS2367 on a 'name' comparison; delete the branch; name the set in permissionSets), and exactly one ADR-0087 marker reading registered position-grant-via-name-retired (spelled as the HTML comment the gate reads).
  • PR body lines 4-5: Clause-②: no (narrowing) and Metadata-protocol: none — byte-identical to the changeset and to the claim amendment 6430877545. The title feat(spec)!: carries the !; the BREAKING banner sits where the gate reads it, the changeset body, and the body's 维护者速读 states the compile break in prose.
  • Level judged right. PositionGrantVia is not authorable: no Zod schema, no authorable-surface shard, no stack key — it types a served description. So Metadata-protocol: none is the right kind, and under ruling A (check-changeset-no-major.mjs:910-919) none grades every introduced entry patch, while a non-protocol narrowing "ships patch and keeps its BREAKING banner and its ADR-0087 disposition unchanged" — which is what this changeset does. The (narrowing) arm is the ADR-0087 breaking signal and the contract-review face; both are present. The spec: retire the zero-consumer kernel/package-upgrade.zod.ts (upgrade plan, diff, conflict, snapshot, rollback) with its ADR-0087 conversion — item 3 of ruling 6259332195, the spec half of #24447 #25608 precedent took minor with Metadata-protocol: narrowed because it retired a Zod model an author writes against; the difference is the kind, not a contradiction.
  • @objectstack/rest (two docblock lines), @objectstack/runtime and @object-ui/data-objectstack (test files only) move without a changeset entry; none publishes a behaviour or a type change. Check Changeset concluded success at 01:58:24Z.

③ Boundary flags

  • Author and provenance. PR author objectstack-fleet[bot]; head repo equals base repo (not a fork); assignee os-support-ai mirrors the card; draft, auto_merge null, mergeable_state blocked — the expected state before this record. Branch claude/project-thread-9lmgya instead of claude/issue-26022-...: the claim 6430837212 records why (the platform assigns the thread's branch), and The card this PR closes must claim this branch concluded success.
  • Check-runs at 2026-10-27T02:01:25Z — 40 runs: 22 completed (16 success, 6 skipped by the fast-set filter: Build Docs, the three dogfood jobs, Temporal Conformance, the opt-in harnesses), 18 in_progress. Of the required contexts, Governed Surface Queue Guard is success; Build Core, Test Core (1-8), the Lint · and Type Check · lanes are in progress; Lint UI, Type Check UI and Build & E2E UI had not appeared yet (Build UI Deps in progress). in_progress is not a pass: this record judges the diff, and landing still waits on the required set per §7.
  • Dev deviations, all five accepted by the record. (1) Clause line no (narrowing) — the seat's amendment 6430877545 accepted it. (2) Three files beyond the first claim — the same amendment accepted them, and ① confirms each (the one caller, two fixtures). (3) Local app-shell run narrowed to 182 files — CI's UI jobs carry the rest, pending above. (4) Both entries patch — judged right in ②. (5) No reviewer request — this record is the review. open_questions is empty.
  • The dogfood reds are not this diff's. The ten failures count text in packages/rest/src/rest-server.ts (REST_SERVER_FILE, authz-conformance :51; probe :95) and packages/rest/src/rest-route-ledger.ts (:210). The diff touches neither path, and its only rest hunk is two comment lines in meta-item-read-gate.ts, a file the matrices name only inside prose strings (enforcement:, enforcedBy:) and never count. From the diff alone, nothing in it can move those counts: main-red, as the dev reports it (git diff origin/main empty on the three paths). Under the two-speeds rule the PR's dogfood jobs are skipped and the red lives in main's full battery; it is the lane's to file under the main-red convention, which the dev's out-of-scope finding (class a) hands the seat. No arming while it stands.
  • Stale docblock, security-plugin.ts:3535-3536, "the contract's 'name' member is no longer served": after this diff the member does not exist, so the sentence goes stale. Left out because PR feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 holds that file; carried in the PR's Acceptance notes with the seat as carrier. Accepted — a comment, no behaviour, and the single-writer rule decides it. The sibling prose at describe-position-grants.test.ts:13 and :140 stays true and needs nothing.
  • The deleted test case (a position held while the permission-set catalogue cannot be read leaves its user unknown) pinned a read this diff removes; the invariant it protected — a failed registry read leaves the user unknown — is still pinned by the surviving a position the registry does not define leaves its user unknown case and by the sets === undefined branch. Not a deletion of a live pin.
  • Pending stock .changeset/24757-retire-position-name-fold.md says the member is "retired separately in spec: retire PositionGrantVia 'name' (the position-name fold's grant mark) once #24757's runtime retirement lands #26022; until then the value is published but never served" — correct as a record of that change; rightly untouched.
  • Docs-drift bot: the one flagged page is content/docs/releases/v18/18-0.mdx, RELEASE-OWNED and read-only; the release compiles this changeset and the D3 entry. Nothing is owed here.
  • No governed surface in the file list; 203 changed lines, under the 3,000 threshold.

Implemented-by: claude/project-thread-9lmgya
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

Rendered by an isolated CONTRACT_REVIEW_TIER subagent of seat session_01WGufzfhobWEgUUrjz4rrbh (domain:spec#1), read-only, adversarial to the dispatch's own conclusions.

VERDICT: PASS

…aged Users marker

A holder of a position that only shares its name with a permission set is
now marked Not authorized; the registry double no longer serves the
permission catalogue or getTypeStatus, which the read no longer asks for.

Claude-Session: https://claude.ai/code/session_01WGufzgKsGKQ8LMfpA72sns
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4b0074c42c1a02c14f1be76b063706463b41e0bc
Local-runs: none
Scope: ui/packages/app-shell/src/views/ObjectView.managedEnvironmentUsers-13330.test.tsx, ui/packages/app-shell/src/views/managedEnvironmentUsers.tsx, ui/packages/app-shell/src/views/objectViewIdentity.tsx, ui/packages/app-shell/src/views/tests/managedEnvironmentUsers-13330.test.tsx, ui/packages/app-shell/src/views/metadata-admin/ResourceListPage.positionGrants-25333.test.tsx, ui/packages/app-shell/README.md, ui/content/docs/guide/console.md, ui/AGENTS.md, AGENTS.md, packages/spec/src/contracts/security-service.ts, packages/plugins/plugin-security/src/security-plugin.ts (describePositionGrants, :3522-3580), packages/plugins/plugin-security/src/describe-position-grants.test.ts, packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts, packages/qa/dogfood/test/guest-grants-channel.dogfood.test.ts, plus git diff 1a70ed57 4b0074c4 and git grep over ui and packages at 4b0074c

Addendum to record 6431990382 (head 1a70ed5): one commit, 4b0074c, touches one file (+9/-8); the first record's ①–② stand, its ③ missed a sibling pin and a stale guide sentence, and the guide sentence is what fails this head.

① Derived judgments

  1. The inverted assertion matches the head source. readUserGrantStates reads only ensureType('position'); closer lists ['member_default'], the everyone anchor lists the same, so for Eve (u_eve, no sys_user_permission_set row, one closer assignment) sets.some(beyondBaseline) is false and sets !== undefined, so she is baseline-only and the marker draws. The totals equal the row counts and both are under ROWS_PER_READ, so nothing is truncated. Expecting Eve Folded marked is right; the old expectation was the retired fold.
  2. Failed read gives unknown, never "not authorized". UserGrantLoader.flush catches a rejected readUserGrantStates and sets every id of the batch to unknown; a position the registry does not define leaves sets === undefined and lands in uncertain → unknown; UnauthorizedUserMarker draws only on baseline-only. The unit file pins each arm (:145, :159, :181, :219). The docblock's "unknown is never said as not authorized" is what the code does.
  3. Dropping the two stubs is safe. PositionRegistrySource at the head declares ensureType alone, and the one construction site (objectViewIdentity.tsx:62-64) passes only ensureType. Repo-wide at the head: no non-test source in ui/packages/*/src calls ensureType('permission'), and every getTypeStatus read in the ObjectView tree's reach is optional-chained (PageView.tsx:351, DashboardView.tsx:193, appReportIndex.ts:219), none of which this test mounts. The unit double (__tests__/...:39) already had the same shape and pins ensureType called with ['position'] only (:142).
  4. Sweep at 4b0074c over ui and packages, each hit classified. Live pins of the fold: none left. via: 'name', via === 'name', 'definition' | 'name', grants.byName, byNameHint: no code hit; the two prose hits (describe-position-grants.test.ts:13, :140, "never served") are true. getTypeStatus on PositionRegistrySource: none; the other getTypeStatus doubles belong to other components' store mocks. PERMISSION_SETS doubles: ResourceListPage.positionGrants-25333.test.tsx:53 feeds the permission-set list control, and its data-via pin (:165-170) admits definition only; deactivateConfirm-13338 and declaredSharingRules-12982 are unrelated catalogues. Positions named like a set: delegated-admin-gate.test.ts:1283, :1493, describe-position-grants.test.ts:121-134, orgless-position-name-fold.test.ts (asserts byName.get(...) is undefined), guest-grants-channel.dogfood.test.ts:109 (FoldSet named guest, pinned as never reaching the guest), position-name-fold-warning.test.ts:203 — every one pins that the name grants nothing. POSITION_NAME_FOLD_EVENT is Setup: a position's same-named permission set is in force but invisible — the set's Assigned Users panel is empty and the position reads as granting nothing #24757's warning, out of this card. The Eve Folded fixture name survives as a label only.

② Semver level

Unchanged from record 6431990382: the delta is one test file, no source, no type, no changeset. @objectstack/spec: patch and @object-ui/app-shell: patch with the BREAKING banner, Clause-②: no (narrowing), Metadata-protocol: none and the registered position-grant-via-name-retired marker still describe the diff. Nothing in ② moves.

③ Boundary flags

  • The first record missed this sibling pin. ObjectView.managedEnvironmentUsers-13330.test.tsx:218 expected Eve Folded unmarked, which is the fold said a second time, through the real ObjectView chain. The first record read only the unit file __tests__/managedEnvironmentUsers-13330.test.tsx, judged "the 13330 pin is inverted" from it, and did not sweep for a second file of the same card; the first diff did not touch the sibling, so it was invisible in the hunks. CI found it (Test UI (2/16) red on 1a70ed5); the commit 4b0074c fixes it the right way (the assertion is inverted and explained, the fold-only stubs removed). The dispatch reports the head's check-runs as 56 success, 6 skipped, 0 red; this record did not re-read them.
  • Stale user-facing guide, owed by this PR: ui/content/docs/guide/console.md:42. The Setup Positions row still says "A permission set that shares the position's name is granted even though the position does not list it, so it is shown and marked "(by name)"." This PR deletes that mark (PositionGrantCells.tsx), and ui/AGENTS.md:106 (rule ✨ Set up Copilot instructions #2, which the root AGENTS.md:796 applies on top for ui/**) reads "update package README.md and content/docs/guide/*.md. Not done until docs reflect the code." The tree is hand-written (AGENTS.md:710), so it is editable in a code PR; the app-shell README carries no mention of the mark, so this one sentence is the whole debt. The first record's ①2 said content/docs carried no mention of the badge; that grep covered the root tree and not ui/content/docs. This is the finding that fails the head: one sentence, in scope, not written.
  • Stale contract prose, pre-existing, in a file this PR owns: packages/spec/src/contracts/security-service.ts:731-732. The describePositionGrants docblock says an unknown name is described with "only what the position-name fold would grant a holder of that name". At the head the plugin serves permissionSets: [] for such a name (security-plugin.ts:3563-3576), and the same file's PositionGrantVia docblock (:127-130, this PR's hunk) says the fold is retired. Left by Setup: a position's same-named permission set is in force but invisible — the set's Assigned Users panel is empty and the position reads as granting nothing #24757, not introduced here, no behaviour; but this PR edits the file at :124-131, so no single-writer rule shields it. Cheap to carry with the sentence above; the seat decides.
  • security-plugin.ts:3535-3536 ("the contract's 'name' member is no longer served") stands as the first record left it: stale, a comment, held by PR feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337, the seat as carrier.
  • The deleted "catalogue cannot be read" case judgment stands: the surviving a position the registry does not define leaves its user unknown (__tests__/...:145) and draws nothing when the read is refused (:219) pin the invariant.
  • The dogfood main-red, the docs-drift releases page and the pending .changeset/24757-retire-position-name-fold.md are as the first record recorded them; nothing in the delta touches them.

Implemented-by: claude/project-thread-9lmgya
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

Rendered by an isolated CONTRACT_REVIEW_TIER subagent of seat session_01WGufzfhobWEgUUrjz4rrbh (domain:spec#1), read-only, adversarial to the dispatch's own conclusions.

VERDICT: FAIL

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 570809677a686ea38406d3445f309f2522614f3e
Local-runs: none
Scope: packages/spec/src/contracts/security-service.ts (:124-131, :728-745), ui/content/docs/guide/console.md:42, packages/plugins/plugin-security/src/security-plugin.ts (describePositionGrants, :3515-3580), packages/core/src/security/resolve-authz-context.ts (positionSetNamesOf :1171, positionBoundSetNames :1211, setsInForceByPosition :1294, readPositionGrantsInForce :1327, countPositionHolders :1364), ui/packages/app-shell/src/views/metadata-admin/PositionGrantCells.tsx, packages/plugins/plugin-security/src/describe-position-grants.test.ts, content/docs/kernel/contracts/index.mdx, content/docs/kernel/runtime-services/index.mdx, content/docs/permissions/attachments-access.mdx, .changeset/26022-retire-position-grant-via-name.md, plus git diff 4b0074c4 57080967 and git grep over ui/content/docs, content/docs (less content/docs/releases) and skills at 5708096

Addendum to records 6431990382 and 6432502078: one commit, 5708096, touches two files (+3/-3), both prose; the FAIL item of record 6432502078 (the stale guide sentence) is rewritten and true at the head, the flagged docblock is rewritten and true at the head, and nothing else moves.

① Derived judgments

  1. The guide sentence is true at the head. console.md:42 now reads "The column lists only the permission sets the position's definition names; a set that merely shares the position's name is not granted and not listed." PositionGrantCells.tsx at the head keeps via: 'definition' as the only member of PositionGrantedSet, readGrantedSets drops any entry whose via is not 'definition', and the badge draws set.name alone, with no hint and no mark. The server side agrees: describePositionGrants (security-plugin.ts:3550-3580) lists only what readPositionGrantsInForce reads from the definition's permissionSets (positionSetNamesOf, resolve-authz-context.ts:1171-1175), resolved by the evaluator, each pushed as via: 'definition'. The unit pin describe-position-grants.test.ts:121-142 holds sales_manager (a set of the same name exists, the definition does not list it) at the two declared sets only, and marketing_user at []. The sentence describes the column as the head draws it. The first record's "(by name)" debt is paid.
  2. The contract docblock is true at the head. security-service.ts:734-736 now says a name that names no position "is described like any other: no holders and no permission sets." At the head the path for such a name is: catalog.resolve('position', name) finds nothing, positionSetNamesOf(undefined) gives [], the name is not in disabled.positions, setsInForceByPosition sets it to [], inForce.get(name) ?? [] is [], and the evaluator's resolution of an empty list is [] (the same path the test pins for guest, declared with permissionSets: [] and not switched off, at :139). The holders map has no entry for it, so holderCount is null, which the PositionGrantSummary docblock (:138-146) defines as "no count is a fact here". "No permission sets" is what the plugin serves; the fold clause the old sentence carried is gone. The sentence no longer contradicts the PositionGrantVia docblock four hundred lines above it (:124-131, "the position-name fold is retired").
  3. The three docs-drift pages carry nothing this PR makes false. content/docs/kernel/contracts/index.mdx:43 is the one-row table entry for ISecurityService (row-level scope, field projection, effective sets, explain): generic. content/docs/kernel/runtime-services/index.mdx:68 names the contract as listed with the others: generic. content/docs/permissions/attachments-access.mdx: :83 "folded into the query" is the parent-id filter of the attachment read middleware, not the name fold; :132 "position-distributed permission set that grants sys_attachment CRUD" is distribution through a position, which is the definition binding and still true; :66 names checkControlledByParentWrite. No page mentions position grants, describePositionGrants, via, the fold or the badge.
  4. Sweep at 5708096 over ui/content/docs, content/docs (less content/docs/releases) and skills, each hit classified. Patterns: (by name), (同名), 同名, via 'name'/via: 'name', name fold, byName, "shares the position's name", "granted by/via/through name", "folded into". Hits: console.md:42 (the rewritten sentence, true); ui/content/docs/guide/dashboard-filters.md:92 and api/schema-reference.md:1035 ("by name" of dimension and prop lookups, unrelated); content/docs/api/index.mdx:37, kernel/contracts/data-engine.mdx:59,595,603, permissions/sharing-rules.mdx:240, plugins/development.mdx:193 (driver, action and rule lookups by name, unrelated); content/docs/references/ui/action-params.mdx:60 (the roles alias of positions, says never to gate on a name string, consistent with this PR). skills/ has no hit. No prose anywhere in scope still describes the fold as granting, the "(by name)" / 「(同名)」 badge, or via 'name'.

② Semver level

Unchanged from records 6431990382 and 6432502078. The delta is two prose hunks: one TSDoc comment in security-service.ts (no exported type, member or signature moves; PositionGrantVia, PositionGrantSummary and describePositionGrants? are byte-identical to 4b0074c outside the comment) and one table cell in console.md. git diff 4b0074c4 57080967 -- .changeset is empty; .changeset/26022-retire-position-grant-via-name.md on the branch still reads @objectstack/spec: patch, @object-ui/app-shell: patch, the BREAKING banner, Clause-②: no (narrowing), Metadata-protocol: none and the registered position-grant-via-name-retired marker, and still describes the diff. Nothing in ② moves.

③ Boundary flags

  • FAIL record 6432502078's blocking item is cleared. console.md:42 no longer says a same-named set is granted or marked; see ①1. That was the one sentence failing head 4b0074c.
  • The flagged docblock is carried. security-service.ts:734-736 no longer names the fold; see ①2. The two records' earlier findings on this file stand as recorded.
  • security-plugin.ts:3535-3536 ("the contract's 'name' member is no longer served") stands as the first two records left it: a comment, held by PR feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337, the seat as carrier. Not touched by the delta.
  • Not in the delta, pre-existing wording, no behaviour: "no holders" for a name that names no position. countPositionHolders counts over sys_user_position rows by heldPositionNames, so a row naming a position the catalog does not define could produce a count for it, and holderCount would then be a number rather than null. The clause "no holders" predates this PR (it was in the sentence record 6432502078 flagged, and that record flagged only the fold clause), the delta rewrites the fold clause as asked, and no unit pin covers an undefined name end to end (describe-position-grants.test.ts queries declared names only). Noted for the seat; not a block.
  • The earlier records' ① (the inverted 13330 pin, unknown never read as "not authorized", the dropped stubs, the ui/packages sweep) and ② stand; the delta touches no file they rest on.
  • The dogfood main-red, the docs-drift releases page and the pending .changeset/24757-retire-position-name-fold.md are as the first record recorded them; nothing in the delta touches them. This record did not re-read the head's check-runs.

Implemented-by: claude/project-thread-9lmgya
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

Rendered by an isolated CONTRACT_REVIEW_TIER subagent of seat session_01WGufzfhobWEgUUrjz4rrbh (domain:spec#1), read-only, adversarial to the dispatch's own conclusions.

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T03:13Z

Head 570809677a686ea38406d3445f309f2522614f3e. Card #26022, claim 6430837212 plus amendment 6430877545, dev reports 6431841169, 6432482197 and 6432564796.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: retire PositionGrantVia 'name' (the position-name fold's grant mark) once #24757's runtime retirement lands

2 participants