@kubun/mutation 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ import type { DocumentMutation, DocumentNode } from '@kubun/protocol';
2
+ /** An exit notice requires a live held state and cannot carry application data. */
3
+ export declare function isAccessSlotOnlyMutation(mutation: DocumentMutation, previous: DocumentNode | null): boolean;
@@ -0,0 +1,23 @@
1
+ import { canonicalStringify } from '@sozai/codec';
2
+ function withoutAccessSlots(data) {
3
+ const { accessPermissions: _hand, defaultAccessPermissions: _default, ...rest } = data;
4
+ return rest;
5
+ }
6
+ /** An exit notice requires a live held state and cannot carry application data. */ export function isAccessSlotOnlyMutation(mutation, previous) {
7
+ if (previous?.data == null || mutation.typ === 'tombstone') return false;
8
+ if (mutation.typ === 'set') {
9
+ return canonicalStringify(withoutAccessSlots(mutation.data)) === canonicalStringify(withoutAccessSlots(previous.data));
10
+ }
11
+ return mutation.patch.length > 0 && mutation.patch.every((patch)=>{
12
+ return [
13
+ '/accessPermissions',
14
+ '/accessPermissions/read',
15
+ '/accessPermissions/write',
16
+ '/defaultAccessPermissions'
17
+ ].includes(patch.path) && [
18
+ 'add',
19
+ 'replace',
20
+ 'remove'
21
+ ].includes(patch.op) && !('from' in patch);
22
+ });
23
+ }
package/lib/apply.d.ts CHANGED
@@ -1,10 +1,26 @@
1
1
  import { HLC } from '@kubun/hlc';
2
2
  import type { DocumentModelID } from '@kubun/id';
3
3
  import { DocumentID } from '@kubun/id';
4
- import type { ChangeDocumentMutation, DocumentData, DocumentModel, DocumentMutation, DocumentNode, SetDocumentMutation } from '@kubun/protocol';
5
- import { type Validator } from '@sozai/schema';
4
+ import type { ChangeDocumentMutation, DocumentData, DocumentModel, DocumentMutation, DocumentNode, SetDocumentMutation, TombstoneDocumentMutation } from '@kubun/protocol';
5
+ import type { Validator } from '@sozai/schema';
6
6
  export type DocumentValidator = Validator<DocumentData>;
7
7
  export type ValidatorsRecord = Record<string, Promise<DocumentValidator>>;
8
+ /**
9
+ * Reserved key holding a merge-policy model's winning-live fetch-order
10
+ * register for its provider group: `{ hlc, digest }`, the greatest
11
+ * `sourceFetchHLC` observed for a live candidate, paired with that
12
+ * candidate's state digest. The bare `hlc` also lands in `field_hlcs` under
13
+ * this same key (mirroring `SET_KEY`) for the total-order comparison;
14
+ * `field_values` carries the full cell so `winnerStateDigestOf` can read the
15
+ * digest without a second store round trip.
16
+ */
17
+ export declare const SET_FETCH_KEY = "$setFetch";
18
+ /**
19
+ * Reserved key holding a merge-policy model's winning-delete fetch-order
20
+ * register: `{ hlc }`, the greatest deleting `sourceFetchHLC` observed. Same
21
+ * `field_hlcs`/`field_values` split as `SET_FETCH_KEY`.
22
+ */
23
+ export declare const DELETE_FETCH_KEY = "/fetch";
8
24
  /**
9
25
  * Post-state access gate evaluated AFTER the post-apply `DocumentNode` is
10
26
  * computed in memory but BEFORE any persist call. Returning `false` instructs
@@ -12,7 +28,11 @@ export type ValidatorsRecord = Record<string, Promise<DocumentValidator>>;
12
28
  * effects (`saveDocument`, `createDocument`, `updateFieldHLCs`) are not
13
29
  * invoked when the gate denies, so engine state is unchanged.
14
30
  */
15
- export type PostStateGate = (postState: DocumentNode) => Promise<boolean> | boolean;
31
+ export type PostStateGateContext = {
32
+ previous: DocumentNode | null;
33
+ mutation: DocumentMutation;
34
+ };
35
+ export type PostStateGate = (postState: DocumentNode, context: PostStateGateContext) => Promise<boolean> | boolean;
16
36
  /**
17
37
  * Minimal database interface required by mutation operations.
18
38
  * This decouples @kubun/mutation from the full KubunDB/store API,
@@ -45,6 +65,7 @@ export type MutationContext = {
45
65
  * injected checker, only the owner may write (owner-only safe default).
46
66
  */
47
67
  checkWriteAccess?: (doc: DocumentNode, mutation: DocumentMutation) => Promise<boolean>;
68
+ checkShareAccess?: (doc: DocumentNode, mutation: DocumentMutation) => Promise<boolean>;
48
69
  hlc?: HLC;
49
70
  maxDriftMS?: number;
50
71
  /**
@@ -65,7 +86,136 @@ export declare function getDocumentValidator(ctx: MutationContext, id: string):
65
86
  * lands whenever it lands.
66
87
  */
67
88
  export declare const DEFAULT_MAX_DRIFT_MS = 3600000;
89
+ export declare function validateFetchTokenDrift(token: string, maxDriftMS: number): void;
68
90
  export declare function getTopLevelField(path: string): string;
69
- export declare function applyChangeMutation(ctx: MutationContext, mutation: ChangeDocumentMutation): Promise<DocumentNode | null>;
70
- export declare function applySetMutation(ctx: MutationContext, mutation: SetDocumentMutation): Promise<DocumentNode | null>;
71
- export declare function applyMutation(ctx: MutationContext, mutation: DocumentMutation): Promise<DocumentNode | null>;
91
+ /** Assemble visible fields and overlay the retained effective read rule. */
92
+ export declare function assembleDocumentData(values: Record<string, unknown>): DocumentData;
93
+ export declare function validateSetData(validator: DocumentValidator, rawData: Record<string, unknown>, documentID: string): DocumentData;
94
+ /**
95
+ * A model's provider merge policy, or `null` for an ordinary model. Read
96
+ * straight from the store on every call (no cache) since it gates which
97
+ * branch a SET/tombstone apply takes. `requiredGroupFields` is the subset of
98
+ * `group` the model schema's own `required` array names — the fields a
99
+ * complete-group SET may never omit (an omitted OPTIONAL group field is
100
+ * legitimate: it clears that field's stored value on a winning SET instead).
101
+ */
102
+ export declare function resolveModelPolicy(ctx: MutationContext, modelID: DocumentModelID | string): Promise<{
103
+ group: Array<string>;
104
+ requiredGroupFields: ReadonlySet<string>;
105
+ } | null>;
106
+ /**
107
+ * The winning side of a merge-policy document's fetch-order registers: its
108
+ * token, its state digest (the live `$setFetch` digest, or the constant
109
+ * `DELETED_STATE_DIGEST` when the delete side wins), and whether that side is
110
+ * live. Shared by `winnerStateDigestOf` (the digest-only public read) and the
111
+ * SET classifier/register-winner logic below, which also need the frontier's
112
+ * own token and liveness to compare a candidate against it.
113
+ */
114
+ export type ObservedProviderValue = {
115
+ fetchHLC: string;
116
+ valueDigest: string | null;
117
+ present: boolean;
118
+ };
119
+ export type FetchFrontier = {
120
+ hlc: string;
121
+ digest: string;
122
+ alive: boolean;
123
+ observed?: ObservedProviderValue;
124
+ };
125
+ export declare function providerGroupDigest(group: Record<string, unknown>): string;
126
+ /**
127
+ * Pure merge-policy liveness read: the winning side's state digest alone —
128
+ * the live `$setFetch` digest, or `DELETED_STATE_DIGEST` when the delete side
129
+ * wins, or `null` when neither register is present. This is what lets
130
+ * `@kubun/store-graph`'s `readWinnerStateDigest` reuse the comparison after
131
+ * its own single-column read instead of also threading `field_hlcs`.
132
+ */
133
+ export declare function winnerStateDigestOf(fieldValues: Record<string, unknown>): string | null;
134
+ /**
135
+ * The diagnostic outcome of applying a candidate against a merge-policy
136
+ * document's provider group. `applied` and `dominated` are pure functions of
137
+ * arrival order and state (never authorization); `nonconforming` flags an
138
+ * exact-token disagreement the total order still resolves deterministically.
139
+ */
140
+ export type MergeOutcome = 'applied' | 'dominated' | 'nonconforming';
141
+ /**
142
+ * The result of applying a mutation. `document` is the thin compat surface
143
+ * every caller reads. The remaining fields are present together only for a
144
+ * non-null application against a merge-policy provider group's SET or
145
+ * tombstone path: `outcome` is `classifySetOutcome`/`classifyTombstoneOutcome`'s
146
+ * diagnostic label, `candidateDigest` the incoming candidate's own state
147
+ * digest, `winnerDigest` the register winner's state digest after this apply,
148
+ * and `winnerChanged` whether that winner digest moved from before to after.
149
+ *
150
+ * `outcome` and `winnerChanged` are independent signals, not the same
151
+ * question asked twice. `outcome` classifies the candidate's standing
152
+ * against the total order: `applied` means the candidate is a valid member
153
+ * of the current frontier, which includes an idempotent re-projection of
154
+ * content the frontier already holds (an exact-token, equal-digest replay —
155
+ * `winnerChanged` is `false` there, since the register's digest never
156
+ * moved). `nonconforming` flags an exact-token disagreement in content; the
157
+ * total order still resolves it deterministically to one side or the other,
158
+ * so a `nonconforming` candidate can itself either win (`winnerChanged:
159
+ * true`, its digest becomes the new winner) or lose (`winnerChanged:
160
+ * false`) — the label alone does not say which. Whether the register's
161
+ * winning digest actually moved is answered by `winnerChanged` alone, never
162
+ * inferred from the `outcome` label. Outcome and digest equality are a
163
+ * third, separate independent axis: a `dominated` candidate can carry
164
+ * either an equal or unequal digest against the winner. An ordinary-model
165
+ * application, a CHANGE application, and a dropped (gate-denied or
166
+ * null-document) application carry none of the four — just `{ document }`.
167
+ */
168
+ export type MutationApplyResult = {
169
+ document: DocumentNode | null;
170
+ outcome?: MergeOutcome;
171
+ candidateDigest?: string;
172
+ winnerDigest?: string;
173
+ winnerChanged?: boolean;
174
+ };
175
+ /**
176
+ * Fetch-token-first diagnostic classification of an incoming live SET
177
+ * candidate against the stored frontier (`F_hlc` = the frontier's own token,
178
+ * equal to `max(setFetchHLC, deleteFetchHLC)` by construction — whichever
179
+ * register wins carries the greater of the two). Discriminates on the
180
+ * candidate's token alone; state/digest is inspected only at an exact token
181
+ * tie, so this is exclusive and total: a below-frontier candidate is never
182
+ * simultaneously `dominated` (hash irrelevant beneath the frontier) and
183
+ * `nonconforming` (hash only inspected at an exact tie).
184
+ *
185
+ * This is the diagnostic label only — separate from which side the register
186
+ * actually keeps (`setWinsFrontier` below). At an exact tie with unequal
187
+ * digests the register still picks the lexically greater one deterministically,
188
+ * even though the label stays `nonconforming` for both directions: the label
189
+ * flags the disagreement, it does not report which side won it.
190
+ */
191
+ export declare function classifySetOutcome(token: string, candidateDigest: string, frontier: FetchFrontier | null): MergeOutcome;
192
+ /**
193
+ * Fetch-token-first diagnostic classification of an incoming tombstone
194
+ * candidate against the stored frontier, mirroring `classifySetOutcome`. At
195
+ * an exact token tie the register always keeps the delete (fail-safe), so a
196
+ * tie against an already-deleted frontier is `applied` (idempotent
197
+ * re-tombstoning) while a tie against a live frontier is `nonconforming` —
198
+ * the label flags the disagreement even though the register still resolves
199
+ * it deterministically to the delete.
200
+ */
201
+ export declare function classifyTombstoneOutcome(token: string, frontier: FetchFrontier | null): MergeOutcome;
202
+ export declare function applyChangeMutation(ctx: MutationContext, mutation: ChangeDocumentMutation): Promise<MutationApplyResult>;
203
+ export declare function applySetMutation(ctx: MutationContext, mutation: SetDocumentMutation): Promise<MutationApplyResult>;
204
+ /**
205
+ * A fetch-token-carrying deletion on a merge-policy document's `/fetch`
206
+ * liveness register. Unlike `applyChangeMutation`'s delete branch, a
207
+ * tombstone may arrive before the document exists at all — it carries the
208
+ * full SET-equivalent creation-and-authorization surface (`aud`/`unq`) so a
209
+ * first-seen tombstone can establish a `data: null` row exactly as a SET
210
+ * creates state from `owner`+`unq`. Ordering is governed by the same total
211
+ * key as the set side (`deleteWinsFrontier`), so sets and deletes commute
212
+ * regardless of arrival order.
213
+ *
214
+ * A tombstone is a merge-policy-only concept — an ordinary model deletes via
215
+ * the `/`→null CHANGE path, never a tombstone — so a model with no
216
+ * `providerMergePolicy` rejects outright rather than running the fetch-order
217
+ * registers against a document whose liveness is governed by plain
218
+ * `$set`/`/` HLC registers instead.
219
+ */
220
+ export declare function applyTombstoneMutation(ctx: MutationContext, mutation: TombstoneDocumentMutation): Promise<MutationApplyResult>;
221
+ export declare function applyMutation(ctx: MutationContext, mutation: DocumentMutation): Promise<MutationApplyResult>;