Skip to content

fix(spec,plugin-sharing)!: sys_record_share no longer offers a guest recipient, which no grant honoured (ADR-0138 D8) - #23324

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23228-record-share-guest-recipient
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23228-record-share-guest-recipient

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23228
Clause-②: no (narrowing: a declared recipient value no grant honours is refused)

ADR-0138 execution card E4, decision D8: sys_record_share's guest recipient is removed. It was declared in two places and honoured in none.

What changed

  • packages/spec/src/contracts/sharing-service.ts: RecordShareRecipientType loses 'guest' (now 'user' | 'group' | 'position' | 'unit_and_subordinates'). The TSDoc says why it went and where guest access lives.
  • packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts: the recipient_type select loses its guest option (a cross-lane touch, declared in the claim).
  • packages/plugins/plugin-sharing/src/translations/{en,es-ES,ja-JP,zh-CN}.objects.generated.ts: regenerated with node scripts/check-i18n-bundles.mjs --write --filter=plugin-sharing. Exactly the four guest option labels drop. Nothing was hand-edited.
  • packages/plugins/plugin-sharing/src/sharing-service.ts: grant() still refuses a 'guest' that arrives untyped (a REST body). Its sentence now carries the prescription: the value was removed, guest access flows through the guest position and share links (ADR-0090 D11), and the caller should use a public form or a share link. Before, a guest got the generic sentence, which says sharing rules deliver non-user recipients. That was never true of a guest, because the sharing-rule vocabulary retired the same recipient. The prefix (VALIDATION_FAILED), the 400 status and every other value's sentence are unchanged.
  • .changeset/23228-record-share-guest-recipient-retired.md: minor for both packages, with the BREAKING banner, the Clause-② line and the ADR-0087 disposition not-required (no-migration-prescription). The gate admits it (see Gates).

Measurements (at c0569797a, re-checked on the merged head)

  • H1, the declaration sites. Confirmed across the whole tree, examples and templates included: two declarations (the select option and the union member) and four generated translation entries. Nothing else declares the value. git grep for a recipient_type/recipientType key written as 'guest': 0 hits. Control, the same pattern with 'user': 110 hits.
  • H2, where the grant refuses it. SharingService.grant, packages/plugins/plugin-sharing/src/sharing-service.ts (the recipientType !== 'user' branch). The refusal is generic: every non-user value takes it. That branch stays live for group / position / unit_and_subordinates, so the removal makes no branch dead.
  • Readers that branch on the record-share guest recipient: none. plugin-sharing/src/** reads recipient_type only as 'user' in the gates and as rule-vocabulary values in the rule service. explain-engine.ts's 'guest' is principalKind. rest-server.ts's guest reads are the guest_portal set and the guest entry, and the share routes pass recipientType through untouched. platform-objects/** has only the anchor positions. No branch was removed, and no live behaviour changed apart from the guest sentence above.
  • H3, which doors judge the value, before and after.
    • The grant door (POST /data/:object/:id/shares, grant()): refused before and after. Now the refusal carries its prescription.
    • The row door (a system-context engine write: a seed, a plugin, a migration script; a user-context write is already refused by the engine-owned write guard). Before: the select admitted guest, so the write stored a row no gate reads. After: VALIDATION_FAILED, an invalid_option error on recipient_type that lists the four values a row may carry.
    • Route choice: the spec-property-retirement playbook's value-retirement route (enumWithRetiredValues) applies to a z.enum, and neither declaration is one. The union is a TS type and the option belongs to a platform object's select. So the prescription is carried by the union's TSDoc (the compile-time channel), the grant's refusal (the runtime channel every caller reaches), and the changeset. The row door names the legal values: the select has no per-option retirement slot, and adding one would be a new mechanism outside this card.
  • H4, the census. No in-repo producer writes the value: seeds, fixtures, examples, templates, migrations or test rows. No seed dataset targets sys_record_share at all. Control: the examples seed sys_user and sys_business_unit. The tree has no *.seed.* files. Seed data is the one stored metadata surface that can carry a row value, and it carries none, so the disposition is the record's expected not-required (no-migration-prescription), not registered. objectui at the pinned .objectui-sha (1fb1ced36) has no consumer either: RecipientPickerField maps no guest. Control: position 6 hits in the same file.
  • H5, semver and disposition. Pre mode (.changeset/pre.json, tag next). check-changeset-no-major refuses major, so this is minor with the BREAKING banner. check-adr-0087-registration refuses no-migration-prescription when the body carries a migration prescription (an arrow, a FROM/TO block or a framed rewrite table). So the changeset states what a caller now sees, in prose, and carries no FROM/TO block. The dispatch asked for FROM/TO; that is the one departure, recorded in the report. The accepted record's expected disposition and the gate's rule decide it.

Pins, each with a negative control

Pin File Proves
grant door packages/plugins/plugin-sharing/src/sharing-service.test.ts, "[ADR-0138 D8] refuses the retired 'guest' recipient with its prescription" the VALIDATION_FAILED prefix, the prescription's first sentence and remedy, no "sharing rules" misdirection, no row written
row door packages/plugins/plugin-sharing/src/record-share-guest-recipient-retired.test.ts (real ObjectQL + SqlDriver, the real SysRecordShare) the select offers exactly four values; a system insert of guest is refused (VALIDATION_FAILED, invalid_option on recipient_type, options listed) and nothing lands; control: the same insert with user lands
census packages/spec/src/contracts/record-share-guest-recipient-census.test.ts (repo tier, added to vitest.repo-tests.json) over the radius declared for @objectstack/spec: no producer, no select option, no union member. Each leg has planted positive controls and a lit control on the real tree (the user producers, the user option, the user member)
type packages/spec/src/contracts/sharing-service.test.ts @ts-expect-error on assigning 'guest' to RecordShareRecipientType, compiled by check:test-typecheck

Ablation (on f0d76f257, via scripts/ablation-replace.mjs, one lock acquisition, each leg restored and proven blob == HEAD, git diff HEAD empty)

Leg Mutation (anchor hit 1, landed 1 to 0, blob moved) Went red Restored
A1 the select offers 'guest' again (sys-record-share.object.ts) row door: 2 of 3 (options equals the four; "the guest row was admitted"), and the user control stays green. Census: leg 2, "expected [...] to not include 'guest'" b2faffca45e1
A2 grant() loses the guest arm (const retired = undefined) grant door pin: its first-sentence assertion fails 0f9c977f5745
A3 the union names 'guest' again (contracts/sharing-service.ts) census leg 3; check:test-typecheck reports 3 type errors in src/contracts/sharing-service.test.ts (the @ts-expect-error and the Assert pin) 26a2e9669ed3

No ablation needed a rebuild. Each subject is read from source: the plugin-sharing test imports the object by relative path and @objectstack/objectql resolves to src; the census reads source text; check:test-typecheck compiles src.

Gates (final head 04410745d, which is git rev-parse --short HEAD at the time of these runs)

origin/main was merged twice through scripts/pm/os-regen-merge.sh (merge commits, no rebase): at 48949dd35, and again at 04410745d after #23222 landed in plugin-sharing/src/sharing-service.ts, in a different region from this change. Each merge was followed by the AGENTS.md §9 steps: rebuild what it touched and rm -rf packages/runtime/.objectstack. The branch's delta against main stays the 13 files listed above.

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 89 families, the same set on 48949dd35, f0d76f257 and 04410745d. On 04410745d all 89 exited 0 on the first pass. --ran: "89 derived famil(ies) accounted for — 89 run, 0 NOT-MEASURED". The first pass on 48949dd35 had two outcomes worth recording:
    • check-comment-mask-adoption went red: the census had a private comment stripper, now routed through scripts/js-comment-mask.mjs.
    • Two PREREQUISITE NOT MET refusals (exit 3) were re-run after real builds, never counted.
  • check-adr-0087-registration: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition", with [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription). check-changeset-no-major and check-empty-changeset: exit 0.
  • pnpm --filter @objectstack/spec check:generated, after a real spec build: "All 14 generated artifacts are up to date". No artifact moved.
  • pnpm --filter @objectstack/plugin-sharing test: 43 files, 1029 tests passed. typecheck: OK (test-typecheck debt unchanged).
  • pnpm --filter @objectstack/spec test:repo: 59 files, 1011 tests passed. typecheck: OK (check:test-typecheck 52 files / 246 errors / 135 signatures held, unchanged).
  • pnpm --filter @objectstack/spec test (local tier), on f0d76f257: 667 files passed, 1 skipped; 20230 tests passed. The merge to 04410745d brought no change to this branch's spec files.
  • The census's first full repo-tier run (on ebcdce5d3) timed out at 60 s on a loaded box: it masked comments in almost every file for its lit control. Since f0d76f257 it masks only files that spell the key, and it runs in about 3 s.
  • Not run locally, and declared to CI: pnpm lint (repo-wide).

Acceptance notes

  • content/docs/kernel/runtime-services/sharing-service.mdx lists the grant's VALIDATION_FAILED reasons without naming guest, so it needed no change. No docs page lists the record-share recipient types by name.
  • The four tracked-ratchet artefacts (api-surface, authorable-surface, json-schema.manifest, signatures) are byte-identical. That is expected for a value narrowing (the playbook's route table), and check:generated is green with zero regeneration.
  • The census matcher also reads sys_sharing_rule rows, because the key is shared. That vocabulary retired the same value earlier, so a hit there would be a resurrection too. Its declarations were not touched.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-sharing, @objectstack/spec, touching 6 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/vitest.repo-tests.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/data-modeling/objects.mdx (via sys_record_share (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/administrator-guide.mdx (via unit_and_subordinates (literal, a string literal in RecordShareRecipientType; a string literal in SysRecordShare; a string literal in sys_record_share))
  • content/docs/permissions/index.mdx (via unit_and_subordinates (literal, a string literal in RecordShareRecipientType; a string literal in SysRecordShare; a string literal in sys_record_share))
  • content/docs/permissions/permissions-matrix.mdx (via sys_record_share (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), unit_and_subordinates (literal, a string literal in RecordShareRecipientType; a string literal in SysRecordShare; a string literal in sys_record_share))
  • content/docs/permissions/positions.mdx (via unit_and_subordinates (literal, a string literal in RecordShareRecipientType; a string literal in SysRecordShare; a string literal in sys_record_share))
  • content/docs/permissions/sharing-rules.mdx (via sys_record_share (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), unit_and_subordinates (literal, a string literal in RecordShareRecipientType; a string literal in SysRecordShare; a string literal in sys_record_share))
  • content/docs/permissions/system-context.mdx (via sys_record_share (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/plugins/packages.mdx (via sys_record_share (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/protocol/objectql/security.mdx (via sys_record_share (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), unit_and_subordinates (literal, a string literal in RecordShareRecipientType; a string literal in SysRecordShare; a string literal in sys_record_share))

⛔ 9 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 in RecordShareRecipientType; a string literal in SysRecordShare; a string literal in sys_record_share))
  • content/docs/releases/v13.mdx (via unit_and_subordinates (literal, a string literal in RecordShareRecipientType; a string literal in SysRecordShare; a string literal in sys_record_share))
  • content/docs/releases/v14.mdx (via sys_record_share (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v15.mdx (via sys_record_share (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v16.mdx (via sys_record_share (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/17-0.mdx (via RecordShareRecipientType (symbol, a top-level type), unit_and_subordinates (literal, a string literal in RecordShareRecipientType; a string literal in SysRecordShare; a string literal in sys_record_share))
  • content/docs/releases/v17/17-1.mdx (via sys_record_share (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/17-3.mdx (via sys_record_share (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/index.mdx (via sys_record_share (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))

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/spec/vitest.repo-tests.json) — pages documenting those are invisible to this run
  • 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 — 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 87e902dcbb8a0bbdcb6f146427afbe1cb0ec3f6f → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 87e902dcbb8a0bbdcb6f146427afbe1cb0ec3f6f

⚠️ 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 87e902dcbb8a0bbdcb6f146427afbe1cb0ec3f6f → 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: 04410745d67245dabfae35d9248876fca4f7f904
Local-runs: none

Reviewed against: card #23228 (body; comments 6144075528, 6147063404, 6153222151; the seat ACCEPT 6153329772 read as a claim under test, not as evidence), ADR-0138 D8 and the E4 execution row on main, ADR-0087 (the 2026-09-13 level amendment and the disposition-vocabulary addenda), PR #23324 (body, the one PR comment, file list, the net diff against main), the check-runs on the head, and the head itself read by its 40-hex sha (git show, git grep). objectui read on disk at the pinned 1fb1ced36 and at its current 7578323a5; nothing cloned, built, run or checked out.

① Derived judgments

(a) Inside ADR-0138 D8 / E4; the 13-file surface held. The diff retires exactly the record-share guest recipient: the union member in packages/spec/src/contracts/sharing-service.ts and the select option in packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts go together, each with a comment that holds the two equal. The four *.objects.generated.ts bundles lose exactly the guest key under recipient_type.options and nothing else moves in them, which is the generator's shape (merge mode keeps translated values, drops a key the source no longer declares); pnpm check:i18n runs inside Lint & Repo Gates, green on this head. No other D8 row is touched: position.zod.ts, sys-audience-binding-suggestion.object.ts, execution-context.zod.ts, explain.zod.ts, assemble-execution-context.ts and rest-server.ts are absent from the file list. The sharing-RULE vocabulary is untouched: sys-sharing-rule.object.ts, security/sharing.zod.ts and SharingRuleRecipientType are not in the diff; the only edit near it is a comment line in the spec contract test. Against the claim's bullets: contract union, select option, translations, pins and tests (four test files plus the vitest.repo-tests.json registration), the ADR-0087 marker and one changeset are all present; no docs page needed a change (content/docs/kernel/runtime-services/sharing-service.mdx:59 lists the grant's refusal reasons without naming guest, and no page in content/docs lists the record-share recipient values). The grant edit in packages/plugins/plugin-sharing/src/sharing-service.ts: the dev reads it as the record's pin "the value refused with its prescription". I hold that reading. The pin cannot exist without a refusal that carries a prescription, and the pre-existing generic refusal carried a misdirection for guest ("delivered via sharing rules", which the rule vocabulary retired in v17), so the edit is the pin's subject, not scope growth. The claim's "change no live behaviour" bullet addressed dead reader branches; the one live change is the sentence for the retired value alone, and the dev declared it. Read in the diff: the else arm's string is byte-identical to the old string, the VALIDATION_FAILED: prefix is kept on both arms, and no other value's path changes.

(b) The row door, and a legacy guest row. Measured on the head, not from the claim: (1) Update. packages/objectql/src/validation/record-validator.ts validateRecordInScope walks every declared field only in insert mode; in update mode it iterates Object.entries(data) and validates only supplied fields ("an OMITTED field never 400s"), and every engine call site (engine.ts:16450, :16642, :16786, :16909) passes the patch (hookContext.input.data) in update mode. An update of a stored guest row that does not carry recipient_type is not refused. (2) Gates. buildReadFilter, buildWriteFilter and checkEdit query sys_record_share with recipient_type: 'user' (sharing-service.ts:518, :594, :805); a guest row is never consulted. (3) Revoke. revoke() deletes by id (system path directly; user path after assertCanManageShares and the source check) and reads no recipient_type; the orphan sweeps key on record ids. (4) List and explain. listShares() returns rows verbatim, so a legacy row surfaces with recipient_type: 'guest', a value the published RecordShare type no longer admits (a typing gap for a TypeScript reader of that array, not a runtime fault); plugin-security's explain engine renders it as kind record_share, via guest: plus the id, effect neutral (explain-engine.ts:1488-1498) and throws nothing; rest-server.ts passes recipientType through untouched and its guest reads are the guest_portal form doors. (5) No in-repo writer re-sends the value: the grant's update half patches level, source, reason and organization only, and the rule reconciler writes user rows. So the changeset's sentence "A guest row that a system write stored before this release is read by no gate, both before and after" is true as written. Nothing is owed for such rows under ADR-0087: a database row is data, not a stored metadata surface, which is the condition the record sets for registered; the row stays inert, visible in listShares and Setup's all_shares view, and revocable through the existing delete path. One cosmetic consequence, non-blocking: with the guest label gone from the bundles, a legacy row's recipient_type renders as the raw value in Setup. The disposition's "nothing to rewrite" assumption holds.

(c) The grant door. The refusal sentence is true: ADR-0090 D11's section says, verbatim at its rejected-alternative paragraph, that guest access flows through the guest position plus share links; share links are the real object packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts; public forms are the real surface (rest-server.ts registerFormEndpoints, publicFormGrant). The VALIDATION_FAILED: prefix is kept, so respondSharingError's startsWith map (rest-server.ts:11119) still answers 400 and strips the prefix; every other value's sentence is the old string unchanged. The RETIRED_RECIPIENT_TYPE_REFUSALS map plus its hasOwnProperty-guarded helper is more machinery than the one value needs (D8 keeps group, position and unit_and_subordinates as members, so no second entry is in sight); a plain equality branch would do the same. Noted only, not wrong: the helper takes unknown, the lookup is own-property guarded, and the generic arm is untouched.

(d) The pins; the census radius. Each fails on revert: (1) union: the @ts-expect-error assignment becomes an unused directive (TS2578) and the Extract equality collapses to a non-never member, so the Assert alias fails; the file is compiled by check:test-typecheck under tsconfig.test.json (include src/**/*) and is absent from packages/spec/test-typecheck-debt.json, so the error is live, not ledgered. (2) select: options equality against the four values, plus a real ObjectQL over SqlDriver insert of guest refused with VALIDATION_FAILED / invalid_option / the four options and nothing stored, beside a user control that lands; re-offering the option fails two of three (driver-sql and better-sqlite3 are plugin-sharing devDependencies, so the harness resolves in CI). (3) grant: the sentence's first clause, the D11 clause, the trailing remedy, no "sharing rules", and an empty table; dropping the arm fails the first-clause assertion. (4) census, in test:repo, which Test Core runs (ci.yml:965, turbo run test test:repo): three matchers each with planted positives and neighbour negatives (a comparison, principalKind, guest_portal, prose, a comment), lit controls on the real tree (visited above 1000, record-share files above 20, user producers above 20), then the sweep. Its radius is the five roots packages, examples, skills, content, scripts with the per-extension shape that scripts/cross-package-test-inputs.mjs declares for @objectstack/spec; the comment-mask import rides scripts/js-comment-mask.mjs, which has a .d.mts beside it and precedent in eight CLI tests. Outside the radius I grepped the whole head myself: no file anywhere spells recipient_type or recipientType as guest as a producer; docs/, .claude/, .github/, apps/, patches/, docker/ and the root files carry only historical ADR and audit prose, and one stale sharing-RULE example (docs/notes/crm-development-standards.mdx:291, the sharedWith vocabulary user | group | role | role_and_subordinates | guest, retired in v17 and not a record-share recipient, not this card's). No enum list pairs unit_and_subordinates with guest outside the changeset and ADR-0111's history. No AI tool schema or service spells it. No seed targets sys_record_share. objectui at the pin and at its current head: zero hits for recipient near guest; RecipientPickerField.TYPE_TO_OBJECT maps user, team, business_unit, unit_and_subordinates plus the position registry, and unknown kinds degrade to a text input. The census is honest about its bound (runtime-assembled values, the out-of-radius roots), and the bound held under my own wider read.

(e) ADR-0087 and the changeset, every sentence read. True throughout: the four-member union; the compile error for recipientType: 'guest' on GrantShareInput and for RecordShare.recipient_type (both typed RecordShareRecipientType at the head); the four options and the dropped labels; the grant's prior refusal (ADR-0111 D7 arm); the user-only gates; the ADR-0090 D11 route; the v17 rule-vocabulary retirement (17.sharing-rule-recipient-reconcile); the 400 before and after; the row door's invalid_option with the four values; the unchanged set. The marker's category reasoning is correct on the gate's own grammar: type-surface-only is closed (the diff touches packages/spec and a contracts/ file; nothing narrowed off any), runtime-interface-only is closed (declared under contracts/), unpublished is false, registered would write a ledger line that objectstack migrate meta has nothing to project, and not-required (no-migration-prescription) is the category ADR-0138 D8 and the E4 row expect when the census finds no stored metadata surface. The omitted FROM/TO block is required by that category, not a shortcut: ADR-0087's addendum says the catch-all "is refused by a body that carries a migration prescription", and the gate's detector reads arrows, labelled FROM/TO and framed tables; the body carries none and states what each door answers in prose. check-adr-0087-registration runs in Check Changeset (pr-automation.yml:980), green on both runs. Level: pre mode (.changeset/pre.json mode pre, tag next); ADR-0087's 2026-09-13 amendment rules that a pre-GA retirement ships minor with the BREAKING banner and the disposition; both are present; check-changeset-no-major refuses major in this window. Correct category, correct level.

(f) The out-of-scope findings. The group sentence: the generic arm says group/position recipients are delivered via sharing rules, and the rule select on the head is user, team, business_unit, position, unit_and_subordinates, field, with no group; the ledger entry sharing-rule-recipient-reconcile records the group to team rename as the ADR-0090 D3 rename (so the dev's citation is substantiated in-repo, whatever the seat could confirm). It predates this PR, this PR leaves that arm byte-identical, D8 keeps group as a union member, and the right wording is a choice; #23325 is the correct carrier and nothing is owed here. The no-per-option-retirement-slot observation holds on the head: keptOptionValues on the validator is the import runner's kept-cell channel, and enumWithRetiredValues attaches to a z.enum, which neither declaration is. Nothing is owed by this PR; see ③ for the consequence.

(g) CI on the head, read at 2026-10-13T19:30Z. Not green: Test Core (1/6) through Test Core (6/6) in_progress, Type Check · workspace in_progress; the required summary contexts Test Core and TypeScript Type Check have not yet been produced, so this record does not vouch for them. No check has failed. Green: Build Core, Lint & Repo Gates, Governed Surface Queue Guard, Check Changeset (both runs), Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, the seven Lint · jobs, Spec property liveness, Dogfood Regression Gate, and the PR-automation checks. Skipped by design on a PR run: Build Docs, Console Pin Gate, the Dogfood matrix shards, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Packed-tarball smoke (opt-in), and the second Auto Label / Check PR Size runs. The docs-drift comment is advisory; none of the nine hand-written pages it lists names guest as a record-share recipient.

② Semver level

minor for @objectstack/spec and @objectstack/plugin-sharing, with the BREAKING banner, the ! in the title, the Clause-②: no (narrowing) line and the marker not-required (no-migration-prescription) in the body. That is the level ADR-0087's amended level rule assigns to a pre-GA accept-set narrowing, and the category the Accepted record names for E4 when the census finds nothing. Published surface narrowed: the RecordShareRecipientType union (recorded in packages/spec/api-surface/contracts.json, which is byte-identical, as a value narrowing must leave it) and the recipient_type option set of sys_record_share.

③ Boundary flags

  • Declared deviation, accepted: no FROM/TO block, though the dispatch listed one. The record's expected category refuses one, and the dev chose the record and reported it. Correct precedence.
  • Declared deviation, accepted: the grant's refusal-with-prescription edit as a live-behaviour change confined to the retired value's sentence. Prefix, status, and every other value's path unchanged, verified in the diff.
  • Non-blocking: the one-entry RETIRED_RECIPIENT_TYPE_REFUSALS map is speculative infrastructure for a value class with one member; a plain branch would carry the same pin.
  • Non-blocking: the row door's refusal lists the four legal values and carries no prescription; the prescription travels on the union's TSDoc, the grant's sentence and the changeset. A per-option retirement slot on Field.select would be a new mechanism outside this card; the dev named the gap rather than hand-rolling one.
  • Non-blocking: a guest row stored before this release remains visible in listShares, Setup's all_shares view and explain output, under a value the published RecordShare type no longer admits, and renders without a label. Inert before and after; revocable.
  • Out of scope, carried elsewhere: the group refusal wording (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); the stale sharing-RULE example in docs/notes/crm-development-standards.mdx:291, which names the rule vocabulary retired in v17 and is not this card's.
  • Not re-judged here: Test Core and TypeScript Type Check were in progress at the read; the owning seat reads them green before the queue.

Implemented-by: claude/issue-23228-record-share-guest-recipient
Reviewed-by: session_012uFegdfR6K6uqvLzt7XsdZ

VERDICT: PASS

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.

ADR-0138 E4: retire sys_record_share's guest recipient (D8)

2 participants