Skip to content

fix(plugin-sharing): the record-share grant names each non-user value's sharing-rule recipient, so group is sent to team - #23678

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23325-record-share-remedy-text
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23325-record-share-remedy-text

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23325

Clause-②: no

What was wrong

ISharingService.grant writes a record share for a user only (ADR-0078). It refused every other declared value with one remedy:

VALIDATION_FAILED: recipientType must be 'user' (received "group") — group/position recipients are delivered via sharing rules (ADR-0111 D7)

The sharing-rule vocabulary has no group. Neither the authoring sharedWith.type enum (ShareRecipientType, packages/spec/src/security/sharing.zod.ts:126) nor the sys_sharing_rule.recipient_type select (packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts:172) offers it. A rule spells it team: the v17 ADR-0087 entry sharing-rule-recipient-reconcile (packages/spec/src/migrations/entries/semantic/17.sharing-rule-recipient-reconcile.ts:11) records "group → team" as the ADR-0090 D3 rename. A caller or an AI that followed the remedy wrote a rule naming a recipient no rule takes. A unit_and_subordinates caller read a sentence that did not name its value.

What changed

packages/plugins/plugin-sharing/src/sharing-service.ts:

  • RULE_ROUTE_BY_RECORD_SHARE_RECIPIENT names, for each declared non-user value, the rule recipient that delivers it. It is typed against both vocabularies rather than restating either as a second runtime list:
    • keys are every RecordShareRecipientType member except user (the union with user excluded, as a TypeScript Record key type), so a record-share member added in the spec fails to compile here until it names its route;
    • values are the authorable ShareRecipientType (the list the sys_sharing_rule select is held equal to by field-recipient.test.ts), so the refusal can never name a recipient a rule cannot be written with. The reserved queue is excluded by construction.
  • The refusal keeps its code, its 400 (the VALIDATION_FAILED prefix arm, packages/rest/src/rest-server.ts:11301) and its first clause. Only the remedy after the dash changes:
received remedy, TO
group a group recipient is delivered via a sharing rule with recipient type 'team', its name since the ADR-0090 D3 rename (ADR-0111 D7)
position a position recipient is delivered via a sharing rule with recipient type 'position' (ADR-0111 D7)
unit_and_subordinates a unit_and_subordinates recipient is delivered via a sharing rule with recipient type 'unit_and_subordinates' (ADR-0111 D7)
a value no record share declares (an untyped REST body sending team, say) "team" is not a record-share recipient type (ADR-0111 D7)
  • The retired guest keeps its own prescription (ADR-0138 D8), byte for byte.
  • ⛔ No change to which values grant() accepts (user only), and none to the record-share vocabulary.

content/docs/kernel/runtime-services/sharing-service.mdx (Typical Errors, VALIDATION_FAILED) stated the old remedy. It now states the per-value mapping. This file is outside the claim's declared file surface. The dispatch asked that everything in this repo stating the old remedy be updated, and this was the one other hit.

.changeset/23325-record-share-remedy-text.md: patch on @objectstack/plugin-sharing. The sentence ships: after the build, dist/index.js and dist/index.mjs each hold the new undeclared-value text once and the old sentence zero times, with the guest prescription as positive control (1 each). files[] is ["dist","README.md","CHANGELOG.md"].

Measured before writing (on origin/main at cad343e201, re-read after merging 5477020809)

  • Premise holds. The sentence was at sharing-service.ts:1396-1397. The existing pin refuses non-user recipient types instead of persisting rows no gate reads asserted VALIDATION_FAILED only, never the remedy.
  • No declared value lacks a rule route. The record-share vocabulary is user, group, position, unit_and_subordinates (RecordShareRecipientType, and the sys_record_share select). The rule vocabulary is user, team, business_unit, position, unit_and_subordinates, field. group maps to team by the recorded rename. The other two keep their own names.
  • Where the old remedy was stated or asserted: git grep for the sentence and its phrases hits only sharing-service.ts and the docs line above in this repo. There are zero hits in the i18n bundles. In objectstack-ai/objectui at the pinned .objectui-sha b9814b334d there are zero hits. The control word recipientType does hit there, for example packages/fields/src/widgets/RecipientPickerField.tsx (8 hits). Nothing outside this repo states it.
  • On the citation. ADR-0090 D3's own rename table lists role → position, not group → team. D3's naming commandment reads "collaboration = team". The spec's ShareRecipientType docblock ("ADR-0090 D3 renamed the pre-D3 group vocabulary to team") and the v17 entry attribute the rename to D3. The remedy cites "the ADR-0090 D3 rename", as triage directed, which matches the repository's own record.

Pins

sharing-service.test.ts, describe [ADR-0111 D7] the refusal names the sharing-rule recipient that delivers each non-user value, following the pattern PR #23324 set for guest:

  • covers exactly the non-user values a record-share row declares, and routes each to a rule recipient: the table's keys equal the sys_record_share select's non-user options. Each route is an option of the sys_sharing_rule select, and group is not.
  • group → … 'team', position → … 'position', unit_and_subordinates → … 'unit_and_subordinates': the code prefix and the received value, the named rule recipient, the (ADR-0111 D7) ending, and that nothing is written.
  • group's remedy cites the rename and never names a group rule recipient. The other two carry no rename clause.
  • a value no record share declares is told so, and is named no route: team, the exact sentence, no "delivered via".
  • The guest pin's guard widens from /sharing rules/ to /sharing rule/, so that it still catches a remedy naming a rule route under the new singular wording. The guest message itself is unchanged.

Ablation (one-off, committed fix first; nothing left in the tree)

node scripts/ablation-replace.mjs swapped + nonUserRecipientTypeRemedy(recipientType), for the old sentence. It recorded anchor x1 → x0, replacement x0 → x1, blob 52704b19f94e → 97ac1ab79692. It then ran vitest run src/sharing-service.test.ts -t 'inert grants', with the test importing ./sharing-service.js from source (no dist in the path):

  • Mutated: 5 failed. The three per-value cases, the rename case, and the undeclared-value case. For example: Received: "VALIDATION_FAILED: recipientType must be 'user' (received "team") — group/position recipients are delivered via sharing rules (ADR-0111 D7)".
  • Green under the mutation, as predicted: the vocabulary-coverage case (it reads the selects, not the message), the pre-existing refusal pin, and the guest pin.
  • Restored: git checkout HEAD -- …. Blob after restore 52704b19f94e = HEAD blob, git diff HEAD empty. Old-sentence count 0, new-call count 1.

Tests (at 6f42b8d08a, the head this PR opens with)

  • pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2: 46 files, 1067 tests passed.
  • pnpm --filter @objectstack/plugin-sharing typecheck (tsc, tsconfig.scripts.json, check:test-typecheck): exit 0. sharing-service.test.ts is in the tsconfig.test.json program (--listFilesOnly: 1 hit, 46 test files), with no debt entry.
  • Dependency closure pnpm --filter '@objectstack/plugin-sharing^...' build: exit 0. It was rebuilt after the merge of origin/main, which brought a packages/core change.
  • The require entry dist/index.js loads (require() → 62 exports).

Gates

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 6f42b8d08a: 95 commands for 4 paths. Each was run byte for byte, with its exit captured before any pipe.

  • 92 exit 0 on the first pass. 3 exit 3 (PREREQUISITE NOT MET):
    • pnpm --filter @objectstack/spec run check:skill-examples needed client/client-react dist. Built, re-run: exit 0, "263 prose examples type-check across 3 surface(s)".
    • pnpm check:i18n needed the CLI's extract closure. Built with the command the gate prints, re-run: exit 0, "OK (10 package(s) — all bundles in sync, no undeclared authoring keys)".
    • pnpm check:dual-build-cjs-loads NOT MEASURED: it needs every package's dist (57 named missing). That whole-tree build is CI's. The one package this diff touches was measured by hand above: its require entry loads, and the diff adds only type-only imports, which are erased.
  • --ran reconciliation: "95 derived, 94 run, 1 NOT-MEASURED, 0 UNRUN".
  • One UNRECOGNISED line, verbatim, from pnpm check:engine-double-contract (a verdict, not a failure): UNRECOGNISED [engine-double-contract]: 22 construct(s) in packages, examples declare a scanned verb (delete, update, findOne) alongside engine siblings, and this gate could not read the implementation -- so they are in NEITHER the pinned population nor the ledger. 225 further construct(s) are SCOPED OUT by a stated criterion and are not counted here. This is a verdict, not a finding: it never fails a run (#9747, ruling of 2026-08-18).
  • CI's own families are read from this PR's check-runs.

Acceptance notes

  • A caller that sends a rule spelling to the record-share door (team, business_unit, field) is told the value is not a record-share recipient type. It is deliberately not routed: the card scopes the remedy to declared record-share values, and routing undeclared ones would mean a second read of the rule vocabulary at this door.
  • A sharing rule shares the records its criteria match. A caller who meant one record for a team needs a criteria naming that record. The remedy names the rule recipient only, as the card asks.
  • SharingRuleService.defineRule does not check the recipient type itself; it relies on the sys_sharing_rule select at insert. This is read from source only and not measured at a public door, so it is not filed.

Generated by Claude Code

…'s sharing-rule recipient, so group is sent to team

grant() refuses every recipientType but user (ADR-0078) with one remedy,
"group/position recipients are delivered via sharing rules", and the rule
vocabulary has no group: a rule spells it team since the ADR-0090 D3
rename (the ADR-0087 entry sharing-rule-recipient-reconcile). Each declared
non-user value now names its own rule recipient from a table typed against
both vocabularies (keys: every non-user RecordShareRecipientType member;
values: the authorable ShareRecipientType), and a value no record share
declares is told so instead of being named a route. The code, the status,
the accepted values and the guest prescription are unchanged.

Claude-Session: https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ
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 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-sharing, touching 4 documentable anchor(s).

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

  • content/docs/kernel/runtime-services/sharing-service.mdx (via unit_and_subordinates (literal, a string literal on a changed line))
  • content/docs/permissions/administrator-guide.mdx (via unit_and_subordinates (literal, a string literal on a changed line))
  • content/docs/permissions/index.mdx (via unit_and_subordinates (literal, a string literal on a changed line))
  • content/docs/permissions/permissions-matrix.mdx (via unit_and_subordinates (literal, a string literal on a changed line))
  • content/docs/permissions/positions.mdx (via unit_and_subordinates (literal, a string literal on a changed line))
  • content/docs/permissions/sharing-rules.mdx (via unit_and_subordinates (literal, a string literal on a changed line))
  • content/docs/protocol/objectql/security.mdx (via unit_and_subordinates (literal, a string literal on a changed line))

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

  • content/docs/releases/implementation-status.mdx (via unit_and_subordinates (literal, a string literal on a changed line))
  • content/docs/releases/v13.mdx (via unit_and_subordinates (literal, a string literal on a changed line))
  • content/docs/releases/v17/17-0.mdx (via ShareRecipientType (symbol, a top-level type), unit_and_subordinates (literal, a string literal on a changed line))
  • content/docs/releases/v17/17-3.mdx (via ShareRecipientType (symbol, a top-level type))

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
  • 2 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 — 9 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 8d1fb1fb2ddbd12a0c492bdfe1dca3cbe8190461 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 8d1fb1fb2ddbd12a0c492bdfe1dca3cbe8190461

⚠️ 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 8d1fb1fb2ddbd12a0c492bdfe1dca3cbe8190461 → 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

席位核对

domain:services seat 1 (#6021) · os-bill · session_017WLmd5in9RXu7AjyHR8fKQ · 2026-10-15T13:25Z · reviewer of record for #23325 (claim 6191854844; dev report on the card). Read on GitHub and on origin/main, ⛔ not from the report.

Verdict: ACCEPT at head 6f42b8d08a. Landing waits on every check green.

  1. Shape. Draft, base main, first line Fixes #23325, and no other card number beside a closing keyword. Assignee os-bill = the card's. 4 files, +149/−7.
  2. Premise and change, read at the head.
    • Each declared non-user value now names its rule recipient: group → team (citing the ADR-0090 D3 rename, per triage), position → position, unit_and_subordinates → unit_and_subordinates.
    • An undeclared value is told it is not a record-share recipient type and is given no route.
    • The route table is typed against both vocabularies. Its keys are the non-user RecordShareRecipientType members, so a new member fails to compile until it names a route. Its values are the authorable ShareRecipientType, so the reserved queue cannot be named. It is not a second runtime list.
    • Unchanged: what grant() accepts (user only), the VALIDATION_FAILED code and its 400, guest's prescription, and the vocabulary.
    • Ablation: the old sentence back reddens the 5 remedy pins, as predicted. The restore was proven.
  3. Gates.
    • Locally: 95 derived, 94 run, 1 NOT-MEASURED (check:dual-build-cjs-loads, which needs a whole-tree build; CI runs it).
    • On this head at this read: 24 success and 5 skipped. Build Docs runs here because a docs page changed. Test Core 1–6/6, Lint & Repo Gates and two Type Check families are in progress. None failed.
    • ⛔ No landing act until every check, Build Docs included, is green.
  4. Accepted deviations.
    • content/docs/kernel/runtime-services/sharing-service.mdx, outside the claim's surface. It was the one other line in this repo stating the old remedy, and the dispatch asked for it to be updated. It is not a governed path, and not content/docs/releases/.
    • The guest pin's guard widened from /sharing rules/ to /sharing rule/, so it still catches a route under the new wording. The guest text is unchanged.
    • The route values are typed as the authorable ShareRecipientType, the stricter set.
    • No label write, and model-free commit trailers.
  5. Out of scope, routed. Noted, not filed, as the dev wrote them:
    • A rule spelling sent to the record-share door (team, business_unit, field) is named no route. That is by the card's scope.
    • defineRule relies on the select at insert to check recipientType. That is read, not measured.
    • A provenance observation: ADR-0090 D3's rename table is said to list role → position. The repository attributes group → team to D3 in the spec's ShareRecipientType docblock and in the v17 entry 17.sharing-rule-recipient-reconcile.ts. This PR follows triage's ruling and that record. If the attribution is wrong, the fix is in that spec text (domain:spec), ⛔ not here.
  6. Landing clause. No governed path, and Clause-②: no (message text only), so CI plus this read is the review. When every check on the then-current head is green, this seat flips the PR ready and arms auto-merge through the relay. It then confirms MERGED from the queue membership and origin/main, and clears pm:dispatched and the assignee on plugin-sharing: the record-share grant refuses a group recipient by naming sharing rules as its route, and the rule vocabulary has no group #23325.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 15, 2026 13:47
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 15, 2026 13:47
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit c245986 Oct 15, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23325-record-share-remedy-text branch October 15, 2026 14: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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-sharing: the record-share grant refuses a group recipient by naming sharing rules as its route, and the rule vocabulary has no group

2 participants