Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/22497-master-chain-docblock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@objectstack/spec': patch
'@objectstack/plugin-sharing': patch
---

docs: four contract docblocks now describe what the code decides. No type, schema or runtime change.

Clause-②: no

- **`ControlledByParentWriteDenialLeg` (`@objectstack/spec`).** The `master_chain` leg has three resolution refusals: no relation to derive from, a row that is not present, and an empty master reference. Each names the master whose own master the walk could not derive. That master is the record's own master, when it is itself `controlled_by_parent`, or any master above it. The docblock said only a master above the record's own master. The next sentence now says "past the record itself" where it said "above the first hop", so the two agree. The `record_not_found` bullet of `ControlledByParentWriteUnresolvedReason` had the same first-hop gap for a missing master row: it now says an absent row of a master that is itself `controlled_by_parent` is the `master_chain` leg, on the first hop or above it, and the legs judge a missing row only where the master governs its own rows.
- **`ISharingService.canEdit` (`@objectstack/spec`) and `SharingService.canEdit` (`@objectstack/plugin-sharing`).** The caller lists no longer name the `sys_attachment` and `sys_comment` parent gates. Those gates read `checkEdit` and the master-detail write check. The `ISharingService.canEdit` sentence on how they reach the service now names `checkEdit`.

`ControlledByParentWriteDenialLeg`, `ControlledByParentWriteOutcome`, `canEdit` and `checkEdit` keep their declared shapes, and no verdict, outcome or refusal changes.
8 changes: 4 additions & 4 deletions packages/plugins/plugin-sharing/src/sharing-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,10 +816,10 @@ export class SharingService implements ISharingService {
* verdict that is not `deny`. The truth table is byte-for-byte the historical
* one — `abstain` is where the old `true` for public / owner-less / bypassed
* objects went — so every existing caller (the sharing middleware, the
* `sys_attachment` parent gate, the ADR-0055 master check) keeps its exact
* semantics. A caller that would let this answer OVERRIDE another authority
* must read {@link checkEdit} instead, because only there is "I permit this"
* distinguishable from "I do not enforce here".
* ADR-0055 master check) keeps its exact semantics. A caller that would let
* this answer OVERRIDE another authority must read {@link checkEdit} instead,
* because only there is "I permit this" distinguishable from "I do not
* enforce here".
*/
async canEdit(
object: string,
Expand Down
15 changes: 9 additions & 6 deletions packages/spec/src/contracts/security-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,10 @@ export interface OwnershipFloorAlternate {
* ({@link ISecurityService.checkAuthoredRowWrite}).
* - `master_chain`: the chain itself cannot be walked to a master that governs
* its own rows. It re-enters an object it has already visited, it exceeds
* the depth bound, or a master ABOVE the record's own master has no relation
* to derive from, is not present, or has an empty master reference. Above the
* first hop the check answers these as refusals, not as defects of the
* the depth bound, or a master that is itself `controlled_by_parent` (the
* record's own master, or any master above it) has no relation to derive its
* own master from, is not present, or has an empty master reference. Past the
* record itself the check answers these as refusals, not as defects of the
* request, so they are a leg here and never `unresolvable`.
*
* The first three legs run on every hop, so `object_permission` can name the
Expand All @@ -339,9 +340,11 @@ export type ControlledByParentWriteDenialLeg =
* defect (`422 INVALID_METADATA`).
* - `record_not_found`: the record `(object, recordId)` does not exist
* (`404 RECORD_NOT_FOUND`). This is the addressed record, not its master. A
* missing master row is not one of these outcomes: on the first hop the legs
* judge it as they judge any master row, and above the first hop it is the
* `master_chain` leg.
* missing master row is not one of these outcomes: where the master governs
* its own rows the legs judge it as they judge any master row, and where it
* is itself `controlled_by_parent` the walk reads that row to derive the next
* master, so an absent row after the legs is the `master_chain` leg, on the
* first hop or above it.
* - `master_reference_missing`: the record's stored master reference is empty
* (`422 MISSING_REQUIRED_FIELD`).
*/
Expand Down
12 changes: 6 additions & 6 deletions packages/spec/src/contracts/sharing-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,18 +291,18 @@ export interface ISharingService {
* [#7144] The write DEPTH in that first sentence is an INPUT this service is
* handed, never one it resolves — so a caller outside the CRUD middleware
* (the `sys_comment` and `sys_attachment` PARENT-record gates, which both
* reach this service through this method) matches ownership at `own`. That is
* the ruled shape, and the fail-open that blocks widening it is written up
* once in "Write DEPTH is an input the CALLER supplies" on this interface.
* reach this service through {@link checkEdit}) matches ownership at `own`.
* That is the ruled shape, and the fail-open that blocks widening it is
* written up once in "Write DEPTH is an input the CALLER supplies" on this
* interface.
*
* [#6428] The two-state PROJECTION of {@link checkEdit}: `true` for every
* verdict that is not `deny`, i.e. `allow` and `abstain` alike. That
* collapse is the historical semantics, kept byte-for-byte so existing
* callers do not drift — it is correct for a caller that only ADDS this gate
* to whatever else already guards the row (the sharing middleware, the
* `sys_attachment` parent gate, the ADR-0055 master check), and WRONG for a
* caller that would let this answer override another authority's floor. The
* latter must read {@link checkEdit}.
* ADR-0055 master check), and WRONG for a caller that would let this answer
* override another authority's floor. The latter must read {@link checkEdit}.
*/
canEdit(
object: string,
Expand Down
Loading