agentic-comments 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pooria Arab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,178 @@
1
- # Temporary Holding Version
1
+ # agentic-comments
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![MIT license](https://img.shields.io/badge/license-MIT-blue)](https://opensource.org/license/mit)
4
+
5
+ [Source](https://github.com/pooriaarab/agentic-comments) · [Support](https://github.com/pooriaarab/agentic-comments/issues)
6
+
7
+ Comment threads and suggestions anchored to any content, from people and agents.
8
+
9
+ > Status: first release, 0.1.0. Threads, suggestions, and anchor survival work. The CLI, the MCP
10
+ > server, and notifications come next.
11
+
12
+ ## Install
13
+
14
+ Requires Node.js 20 or later. The quick start imports `yjs`, so install it with this package:
15
+
16
+ ```sh
17
+ npm install agentic-comments yjs
18
+ ```
19
+
20
+ To build from source, see Develop.
21
+
22
+ ## Quick start
23
+
24
+ ```ts
25
+ import * as Y from "yjs";
26
+ import { Comments, stamp, textAnchor } from "agentic-comments";
27
+
28
+ const doc = new Y.Doc();
29
+ doc.getText("body").insert(0, "Hello brave world");
30
+
31
+ const comments = new Comments(doc);
32
+ const ada = stamp({ kind: "human", name: "ada" });
33
+ const scribe = stamp({ kind: "agent_of_human", name: "scribe" });
34
+
35
+ const thread = comments.add({ anchor: textAnchor(doc.getText("body"), 6, 11), body: "Word choice? @scribe" }, ada);
36
+ comments.reply(thread.id, { body: "I suggest a shorter word." }, scribe);
37
+ comments.resolve(thread.id, ada);
38
+ ```
39
+
40
+ The `Comments` object lives on a Yjs document. Send the document updates with your
41
+ normal sync, and every replica sees the same threads.
42
+
43
+ ## Features
44
+
45
+ - Threads anchored to a text range, a cell key, a slide element, or a canvas element.
46
+ You can add your own anchor type.
47
+ - A text anchor uses Yjs relative positions, so it follows its text through edits.
48
+ - Messages with mentions. Resolve and reopen a thread.
49
+ - Suggestions: a change your app proposes. A person, or an agent with the accept scope, accepts it.
50
+ An agent never accepts or rejects its own. Two replicas that accept at once apply it once.
51
+ - The host stamps every author. An operation never reads an author from its input.
52
+ - Every message keeps the author kind, so agent comments stay marked as machine-made.
53
+ - Replies and status merge across replicas. Two replicas that resolve at once agree.
54
+ - A malformed thread from a bad replica is skipped. A bad message is left out, and `skipped` on the
55
+ thread counts those. `problems()` names them.
56
+
57
+ ## When the content changes
58
+
59
+ Every thread and suggestion has an `anchorState`:
60
+
61
+ | State | Meaning |
62
+ | --- | --- |
63
+ | `attached` | The content is there, as it was. |
64
+ | `edited` | A text range is there, with other text. `currentText` shows it. |
65
+ | `orphaned` | The content is gone. The thread stays readable and takes replies. |
66
+ | `unknown` | Nothing can tell. A cell, slide, canvas, or custom anchor has no host answer. |
67
+
68
+ - A text anchor follows its text through edits by any replica. Deleting the whole range orphans it.
69
+ - For other anchors, pass `exists(anchor)` to `new Comments`. Return `true` or `false`. Any other answer,
70
+ or a throw, means `unknown`.
71
+ - `list({ anchorState: "orphaned" })` finds threads to review.
72
+ - `relocate(threadId, actor)` moves a text anchor to its quote when the quote occurs exactly once in the
73
+ same text. It never guesses. It throws `RELOCATE_FAILED` for no match or many.
74
+ - Accept refuses a suggestion whose anchor is orphaned or edited. The suggestion stays pending, and a
75
+ person can reject it.
76
+ - `currentText` is document text. It can hold any character, so a UI should treat it as untrusted.
77
+
78
+ ## Suggestions
79
+
80
+ Your app owns the change. This package stores it as JSON, tracks the decision, and calls your `apply`.
81
+
82
+ ```ts
83
+ import { ACCEPT_SCOPE, Suggestions } from "agentic-comments";
84
+
85
+ const suggestions = new Suggestions(comments, {
86
+ apply(change, { anchor, by, ydoc }) {
87
+ // Edit ydoc here. Throw before you edit when the change cannot apply.
88
+ return { /* a receipt, JSON that lets revert undo this edit */ };
89
+ },
90
+ revert(receipt, { ydoc }) {
91
+ // Undo a duplicate edit. Every replica runs this at once, so use an edit that merges to one result.
92
+ // Throw before you change anything.
93
+ },
94
+ });
95
+
96
+ const s = suggestions.add({ anchor, change: { replacement: "bold" }, summary: "Word choice" }, scribe);
97
+ suggestions.accept(s.id, ada);
98
+ ```
99
+
100
+ - A signed-in person (`human`) accepts or rejects. Every other kind, including `user_token`, needs the
101
+ `comments:accept` scope, and never decides a suggestion it wrote. Names match exactly, in Unicode NFC form (a name in another form is refused), and two
102
+ principals that are not signed-in people with the same name count as one, whatever their kinds.
103
+ - Accept checks that a text anchor still matches the document, then runs the edit and writes the
104
+ record in one step. If `apply` throws before it edits, the suggestion stays pending. If it edits and
105
+ then fails, the decision is recorded without a receipt, and `problems()` reports it. A host that cannot undo a double apply must say `idempotent: true`.
106
+ - Every replica of a document must use the same host mode: the same `revert`, or the same `idempotent`
107
+ flag. A mix can leave a duplicate edit in the document.
108
+ - `problems()` entries about an edit that failed after it landed, or a duplicate with no receipt, live in
109
+ memory on the replica that saw them. Other replicas and a reload do not show them.
110
+ - A decision by an agent with the accept scope is final for everyone, including people.
111
+ - An accept on a replica that has not yet seen a reject wins, even when a person made the reject long before.
112
+ The window is any time the replicas were apart. An agent with the scope can override a person this way.
113
+ A host that wants the person to win can accept only while its replica is in sync.
114
+ - Two replicas that accept before they meet each apply the edit. After the merge, the application of
115
+ the lowest client id stays and `revert` undoes each other one. An accept and a reject at once end as
116
+ accepted.
117
+ - A receipt is stored data. A peer that can write comment data can choose one, and can rewrite the
118
+ `anchor` to match. Check the receipt in `revert` before you use it. The sync layer's write control is
119
+ the real protection. A record with no receipt (an `apply` that edited and
120
+ then failed) always stays, and the other application is undone. If two records have none, the extra
121
+ edit cannot be undone. `problems()` reports it. With `idempotent: true`, a duplicate is never undone.
122
+ - `apply` and `revert` must be synchronous, and must edit only the Yjs document. `accept` cannot run
123
+ inside another Yjs transaction, so not inside an observer or an `apply`.
124
+ - A peer that can write the document can write a decision record. Records with a bad author, time, or
125
+ receipt, or from a decider who wrote the suggestion as an agent, are not counted. A well-formed forged
126
+ record is, and it can win settle, so every replica undoes the real edit. The sync layer must decide who
127
+ may write.
128
+
129
+ ## Author kinds
130
+
131
+ | Kind | Meaning | Label |
132
+ | --- | --- | --- |
133
+ | `human` | A person signed in. | person |
134
+ | `user_token` | A person, through a token an agent holds. | person via agent |
135
+ | `agent_of_human` | An agent that acts for a person. | agent |
136
+ | `agent_of_agent` | An agent that acts for another agent. | agent |
137
+ | `agent` | An agent with its own identity. | agent |
138
+
139
+ ## Limits
140
+
141
+ - No file store, CLI, MCP server, or HTTP API yet.
142
+ - `add` checks that a text anchor points into this document and matches its quote.
143
+ - The library trusts the host to call `stamp()` only for a principal it authenticated.
144
+ - A reply to a resolved thread is refused on the replica that sees the resolve. A reply and a
145
+ resolve made at once on two replicas both stay. The merged thread is resolved and holds the reply.
146
+ - Author and kind are only as trustworthy as every peer that can write the document. A peer can write
147
+ any author straight into the Yjs data. The sync layer must decide who may write.
148
+ - `relocate` and `exists` never touch a thread whose anchor is attached. `exists` runs for every thread
149
+ on every read, so keep it fast.
150
+ - A local reply stops at 1,000 messages. Replicas that reply at once can pass that. A read shows the first
151
+ 2,000 valid messages. `skipped` and `problems()` report the rest.
152
+ - Every replica of a document must register the same custom anchor kinds.
153
+ - A repeated message id hides every copy. A peer that appends a copy of a thread's only message makes
154
+ the thread unreadable.
155
+ - The quoted text of a text anchor is not checked when a thread is read. A peer can store any text there.
156
+ `textAnchor` refuses a quote whose JSON form passes 4,096 characters. Quotes with many newlines or
157
+ quotation marks hit that before 2,000 characters.
158
+ - The first visible message decides when a thread started, so a peer that inserts an older message at
159
+ the front moves the thread in `list()`.
160
+ - A read scans every stored entry. A peer that writes millions of junk entries slows every read.
161
+ - A body mentions anyone named as `@handle`. Put `@` text in backticks to avoid that. Any two backticks in a body pair up, so a loose backtick
162
+ can hide a real mention that sits between two of them. A scoped
163
+ package name such as `@scope/pkg` is not a mention.
164
+ - Mentions are stored in lowercase, with no leading `@`. A handle holds up to 64 letters, digits, marks,
165
+ and `. _ -`. A longer handle in a body is refused.
166
+
167
+ ## Develop
168
+
169
+ ```sh
170
+ git clone https://github.com/pooriaarab/agentic-comments
171
+ cd agentic-comments
172
+ npm ci
173
+ npm run ci:local
174
+ ```
175
+
176
+ ## License
177
+
178
+ MIT.
package/SKILL.md ADDED
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: agentic-comments
3
+ description: Comment threads and suggestions anchored to any content, from people and agents. Keywords: comments,threads,mentions,suggestions,annotations,collaboration,agents,yjs.
4
+ ---
5
+
6
+ # agentic-comments
7
+
8
+ Comment threads and suggestions anchored to any content, from people and agents.
9
+
10
+ ## Use the SDK
11
+
12
+ The host gives you an `Actor` that names you. Pass it to each call.
13
+
14
+ ```ts
15
+ import { Comments } from "agentic-comments";
16
+
17
+ const comments = new Comments(ydoc);
18
+ const thread = comments.add({ anchor: { type: "cell", key: "Sheet1!B4" }, body: "Check this total." }, actor);
19
+ comments.reply(thread.id, { body: "Fixed." }, actor);
20
+ ```
21
+
22
+ Check `thread.anchorState`. An `orphaned` thread has lost its content. Read it, reply, or call
23
+ `comments.relocate(id, actor)`. Do not treat it as bad data.
24
+
25
+ Suggest a change with `suggestions.add({ anchor, change, summary }, actor)`. Accept needs a person or
26
+ the `comments:accept` scope. You never accept or reject your own suggestion.
27
+
28
+ The CLI and the MCP server are not built yet.
29
+
30
+ ## Rules
31
+
32
+ - The host stamps the author. Never call `stamp()` yourself, and never pass an author in an input.
33
+ - Reopen a resolved thread before you reply.
34
+ - Report refusals and errors as failures.
@@ -0,0 +1,332 @@
1
+ import * as Y from 'yjs';
2
+
3
+ declare const VERSION: string;
4
+
5
+ /** A Yjs relative position as JSON. It names an item, not an index, so it follows its text. */
6
+ interface RelativePositionJson {
7
+ type?: {
8
+ client: number;
9
+ clock: number;
10
+ };
11
+ tname?: string;
12
+ item?: {
13
+ client: number;
14
+ clock: number;
15
+ };
16
+ assoc?: number;
17
+ }
18
+ interface TextAnchor {
19
+ type: "text";
20
+ /** The text under the range when the comment was made. `Comments.add` checks it against the document. */
21
+ text: string;
22
+ start: RelativePositionJson;
23
+ end: RelativePositionJson;
24
+ }
25
+ interface CellAnchor {
26
+ type: "cell";
27
+ /** A cell key the host app defines, such as `Sheet1!B4` or a stable cell id. */
28
+ key: string;
29
+ }
30
+ interface SlideAnchor {
31
+ type: "slide";
32
+ elementId: string;
33
+ }
34
+ interface CanvasAnchor {
35
+ type: "canvas";
36
+ elementId: string;
37
+ }
38
+ /**
39
+ * Where a thread points. A custom kind (see `AnchorKind`) returns its own shape, so narrow on `type`
40
+ * before you read its fields.
41
+ */
42
+ type Anchor = TextAnchor | CellAnchor | SlideAnchor | CanvasAnchor;
43
+ /** Teach `Comments` a new anchor type. `parse` returns a clean copy or throws. */
44
+ interface AnchorKind {
45
+ type: string;
46
+ parse(raw: Record<string, unknown>): {
47
+ type: string;
48
+ };
49
+ }
50
+ /**
51
+ * Whether the content under an anchor is still there.
52
+ * `attached`: yes, as it was. `edited`: the range is there, with other text. `orphaned`: the content is
53
+ * gone. `unknown`: nothing can tell, as for a cell with no host answer.
54
+ */
55
+ type AnchorState = {
56
+ state: "attached";
57
+ } | {
58
+ state: "edited";
59
+ currentText: string;
60
+ } | {
61
+ state: "orphaned";
62
+ } | {
63
+ state: "unknown";
64
+ };
65
+ /** The text of a Yjs text type. An embed counts as one character, so indexes match Yjs positions. */
66
+ declare function textOf(type: Y.Text | Y.XmlText): string;
67
+ /** Pin a range of a Yjs text. Later edits by any replica do not move the anchor off its text. */
68
+ declare function textAnchor(type: Y.Text | Y.XmlText, from: number, to: number): TextAnchor;
69
+
70
+ declare const PRINCIPAL_KINDS: readonly ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
71
+ type PrincipalKind = (typeof PRINCIPAL_KINDS)[number];
72
+ /** What a thread keeps about who wrote a message. Scopes never reach storage. */
73
+ interface Author {
74
+ kind: PrincipalKind;
75
+ name: string;
76
+ }
77
+ interface StampInput {
78
+ kind: PrincipalKind;
79
+ name: string;
80
+ /** Rights the host granted this principal, such as `comments:accept`. Used to decide, never stored. */
81
+ scopes?: string[];
82
+ }
83
+ /**
84
+ * The only author a write accepts. The host makes one from the principal it authenticated, and an
85
+ * operation never reads an author from its input. An object that `stamp()` did not make is refused,
86
+ * and a stamped one cannot change.
87
+ */
88
+ declare class Actor implements Author {
89
+ readonly kind: PrincipalKind;
90
+ readonly name: string;
91
+ readonly scopes: readonly string[];
92
+ private constructor();
93
+ static of(input: StampInput): Actor;
94
+ static is(value: unknown): value is Actor;
95
+ }
96
+ /** Make the author for one authenticated principal. Throws when the kind or name is missing or wrong. */
97
+ declare const stamp: (input: StampInput) => Actor;
98
+ /** The label a UI shows. Agent work stays marked as machine-made. */
99
+ declare function authorLabel(author: Author): "person" | "person via agent" | "agent";
100
+
101
+ interface Message {
102
+ id: string;
103
+ at: string;
104
+ author: Author;
105
+ body: string;
106
+ /** Handles named in the body or listed on the input, without the `@`. */
107
+ mentions: string[];
108
+ }
109
+ interface Thread {
110
+ id: string;
111
+ anchor: Anchor;
112
+ status: "open" | "resolved";
113
+ /**
114
+ * The status change that won, and when. For a new thread, who opened it. Replicas that change the
115
+ * status at once agree on one change. Yjs picks it by client id, not by time.
116
+ */
117
+ statusBy: Author;
118
+ statusAt: string;
119
+ messages: Message[];
120
+ /** Whether the anchored content is still there. A thread whose content is gone stays readable. */
121
+ anchorState: AnchorState;
122
+ /** How many stored entries this read did not show: invalid ones, repeated ids, and valid ones past the read limit. `problems()` names them. */
123
+ skipped: number;
124
+ }
125
+ interface CommentsOptions {
126
+ now?: () => Date;
127
+ /** More anchor types. A type that is already built in is refused. */
128
+ anchors?: AnchorKind[];
129
+ /**
130
+ * Tell whether the content a cell, slide, canvas, or custom anchor points at still exists. Return `true`
131
+ * or `false`. Any other answer, or a throw, means unknown. Text anchors do not need it.
132
+ */
133
+ exists?: (anchor: Anchor) => boolean | undefined;
134
+ }
135
+ interface NewThread {
136
+ anchor: Anchor;
137
+ body: string;
138
+ mentions?: string[];
139
+ }
140
+ interface NewReply {
141
+ body: string;
142
+ mentions?: string[];
143
+ }
144
+ interface ThreadFilter {
145
+ status?: Thread["status"];
146
+ anchorType?: string;
147
+ /** A handle, with or without the `@`. */
148
+ mentioning?: string;
149
+ anchorState?: AnchorState["state"];
150
+ }
151
+ /** Comment threads on a Yjs document. Replies and status merge across replicas. */
152
+ declare class Comments {
153
+ readonly ydoc: Y.Doc;
154
+ private readonly kinds;
155
+ private readonly now;
156
+ private readonly exists?;
157
+ /** Every replica of a document must register the same custom anchor kinds. Others read those threads as malformed. */
158
+ constructor(ydoc: Y.Doc, options?: CommentsOptions);
159
+ private get map();
160
+ /** The clock, as an ISO time. Used by `Suggestions`. */
161
+ time(): string;
162
+ /** Whether the content under an anchor is still there. This never throws. */
163
+ anchorState(anchor: Anchor): AnchorState;
164
+ /** Read an anchor from stored data. This does not check the document. */
165
+ parseAnchor(raw: unknown): Anchor;
166
+ /** Parse an anchor from input and check it against this document. Throws `INVALID_ANCHOR` when it fails. */
167
+ checkAnchor(raw: unknown): Anchor;
168
+ /** A text anchor must name text in this document, in order, and match its quote. */
169
+ private checkInDocument;
170
+ /** Build a message, and refuse it unless a read would show it back. Nothing is written yet. */
171
+ private message;
172
+ private readMessage;
173
+ /** A thread with a bad anchor or status is malformed. A bad message is skipped and named in `issues`. */
174
+ private read;
175
+ private fields;
176
+ /** Start a thread with its first message. The anchor and body are checked before anything is written. */
177
+ add(input: NewThread, actor: Actor): Thread;
178
+ /**
179
+ * Add a message. The resolved check runs on this replica only. A reply and a resolve made at once
180
+ * on two replicas both stay, and the merged thread is resolved with the reply in it.
181
+ */
182
+ reply(threadId: string, input: NewReply, actor: Actor): Message;
183
+ private setStatus;
184
+ /**
185
+ * Move a text anchor whose range is gone or edited to its quote, when the quote occurs exactly once in
186
+ * the same text. There is no fuzzy match. Throws `RELOCATE_FAILED` when it cannot say where. An attached
187
+ * thread is returned as it is.
188
+ */
189
+ relocate(threadId: string, actor: Actor): Thread;
190
+ resolve(threadId: string, actor: Actor): Thread;
191
+ reopen(threadId: string, actor: Actor): Thread;
192
+ get(threadId: string): Thread;
193
+ /** Threads in the order they were started. A malformed thread is skipped; `problems()` names it. */
194
+ list(filter?: ThreadFilter): Thread[];
195
+ /** Threads or messages that could not be read, for example from a replica that wrote bad data. */
196
+ problems(): {
197
+ id: string;
198
+ reason: string;
199
+ }[];
200
+ private readAll;
201
+ }
202
+
203
+ type CommentsErrorCode = "INVALID_AUTHOR" | "INVALID_INPUT" | "INVALID_ANCHOR" | "NO_THREAD" | "THREAD_RESOLVED" | "CORRUPT" | "NO_SUGGESTION" | "FORBIDDEN" | "ALREADY_DECIDED" | "APPLY_FAILED" | "RELOCATE_FAILED";
204
+ declare class CommentsError extends Error {
205
+ readonly code: CommentsErrorCode;
206
+ constructor(code: CommentsErrorCode, message: string);
207
+ }
208
+
209
+ /** The scope that lets an agent accept or reject a suggestion that another principal wrote. */
210
+ declare const ACCEPT_SCOPE = "comments:accept";
211
+ interface Suggestion {
212
+ id: string;
213
+ anchor: Anchor;
214
+ /** Whether the anchored content is still there. Accept refuses an orphaned or edited one. */
215
+ anchorState: AnchorState;
216
+ /** What the host app proposes. This package never reads it. The host's `apply` does. */
217
+ change: unknown;
218
+ summary?: string;
219
+ author: Author;
220
+ createdAt: string;
221
+ status: "pending" | "accepted" | "rejected";
222
+ decidedBy?: Author;
223
+ decidedAt?: string;
224
+ }
225
+ interface ApplyContext {
226
+ suggestionId: string;
227
+ anchor: Anchor;
228
+ /** Who wrote the suggestion. */
229
+ author: Author;
230
+ /** Who accepts it. Label the edit with this author. */
231
+ by: Author;
232
+ ydoc: Y.Doc;
233
+ }
234
+ interface RevertContext {
235
+ suggestionId: string;
236
+ anchor: Anchor;
237
+ /** Who wrote the suggestion. */
238
+ author: Author;
239
+ /** Who made the application being undone. */
240
+ by: Author;
241
+ ydoc: Y.Doc;
242
+ }
243
+ /**
244
+ * The host app's side. Each hook is synchronous, so the check and the edit stay in one step.
245
+ * Make every edit in `ydoc`, so it merges with the rest.
246
+ */
247
+ interface SuggestionHost {
248
+ /** Check a change when it is suggested. Return the clean change, or throw. */
249
+ parse?(change: unknown): unknown;
250
+ /**
251
+ * Make the edit. Throw before you change anything when the edit cannot apply, for example when the
252
+ * anchored content changed. Yjs cannot undo a partial edit. If `apply` edits and then fails, the
253
+ * decision is recorded as accepted with no receipt, and `problems()` reports it. Return a receipt,
254
+ * JSON data that lets `revert` undo this edit.
255
+ */
256
+ apply(change: unknown, context: ApplyContext): unknown;
257
+ /**
258
+ * Two replicas can accept one suggestion before they meet. Each applies the edit. After a merge one
259
+ * application stays and `revert` undoes each other. Every replica runs it at the same time, and the
260
+ * results must merge to one outcome, as a Yjs delete of the same range does. Throw before you change
261
+ * anything. A receipt comes from stored data. A peer that can write comment data can choose it, so
262
+ * check it before you use it. The anchor and the author are stored data too, so a check against them
263
+ * helps against a mistake and not against a peer that can write comment data.
264
+ */
265
+ revert?(receipt: unknown, context: RevertContext): void;
266
+ /** Say that applying the same change twice is harmless. Then `revert` is not needed, and a duplicate is not undone. */
267
+ idempotent?: boolean;
268
+ }
269
+ interface NewSuggestion {
270
+ anchor: Anchor;
271
+ change: unknown;
272
+ summary?: string;
273
+ }
274
+ interface SuggestionFilter {
275
+ status?: Suggestion["status"];
276
+ anchorState?: AnchorState["state"];
277
+ }
278
+ /**
279
+ * Suggested changes to content, from people and agents. Nothing changes until a person, or an agent
280
+ * with the accept scope that did not write it, accepts. Accept and reject are safe across replicas.
281
+ */
282
+ declare class Suggestions {
283
+ private readonly comments;
284
+ private readonly host;
285
+ private readonly onTransaction;
286
+ private failures;
287
+ constructor(comments: Comments, host: SuggestionHost);
288
+ /** Stop listening to the document. */
289
+ close(): void;
290
+ private get ydoc();
291
+ private get map();
292
+ private get applied();
293
+ private get rejected();
294
+ private change;
295
+ /**
296
+ * Records of one kind for one suggestion, in client order. A record that is not valid is not counted:
297
+ * one with no author or time, a receipt that is not small JSON, or a decider who decided their own work.
298
+ */
299
+ private records;
300
+ /** Group the entries of a decision map by suggestion id in one pass, so a read does not scan the map per suggestion. */
301
+ private index;
302
+ private read;
303
+ private fields;
304
+ /** Propose a change. Anyone can suggest. The host stamps the author. */
305
+ add(input: NewSuggestion, actor: Actor): Suggestion;
306
+ /** A person decides. An agent needs the accept scope, and never decides its own suggestion. */
307
+ private mayDecide;
308
+ /**
309
+ * Apply the change as the accepting author and record the decision, in one step. Accepting an
310
+ * accepted suggestion changes nothing. When `apply` throws, the suggestion stays pending.
311
+ */
312
+ accept(suggestionId: string, actor: Actor): Suggestion;
313
+ /** Same right as accept. A person can withdraw their own suggestion. */
314
+ reject(suggestionId: string, actor: Actor): Suggestion;
315
+ get(suggestionId: string): Suggestion;
316
+ /** In the order they were made. A malformed suggestion is skipped, and `problems()` names it. */
317
+ list(filter?: SuggestionFilter): Suggestion[];
318
+ /** Suggestions that could not be read, and reverts that failed. */
319
+ problems(): {
320
+ id: string;
321
+ reason: string;
322
+ }[];
323
+ private readAll;
324
+ /**
325
+ * Two replicas can accept one suggestion before they meet. After a merge, the application of the
326
+ * lowest client id stays, and the host reverts each other one. Every replica reaches the same result.
327
+ * A revert that throws leaves its record, so the next merge tries again.
328
+ */
329
+ settle(): void;
330
+ }
331
+
332
+ export { ACCEPT_SCOPE, Actor, type Anchor, type AnchorKind, type AnchorState, type ApplyContext, type Author, type CanvasAnchor, type CellAnchor, Comments, CommentsError, type CommentsErrorCode, type CommentsOptions, type Message, type NewReply, type NewSuggestion, type NewThread, PRINCIPAL_KINDS, type PrincipalKind, type RelativePositionJson, type RevertContext, type SlideAnchor, type StampInput, type Suggestion, type SuggestionFilter, type SuggestionHost, Suggestions, type TextAnchor, type Thread, type ThreadFilter, VERSION, authorLabel, stamp, textAnchor, textOf };