@startsimpli/ui 0.4.142 → 0.4.144

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,451 @@
1
+ /**
2
+ * @startsimpli/ui/track-changes — the shapes the track-changes editor surface
3
+ * reads and the one call it writes (bd startsim-sjuai; design startsim-xyn25).
4
+ *
5
+ * THE FIELD STAYS A PLAIN STRING. Suggestions and comments are separate server
6
+ * rows anchored to character offsets of that string at a known version, and
7
+ * blame is derived server-side from the revision chain. The editor renders all
8
+ * three as DECORATIONS over the stored string and never writes a mark into it.
9
+ *
10
+ * STRUCTURAL ON PURPOSE, like `history/types.ts`: packages/ui imports no API
11
+ * client. The consuming app injects its own authed client
12
+ * ({@link TrackChangesClient}). Both spellings of every key are declared
13
+ * because the shared API client camelCases responses recursively; the
14
+ * normalizers in `normalize.ts` read either.
15
+ *
16
+ * ── OFFSET UNITS ────────────────────────────────────────────────────────────
17
+ * The server is Python, so its string offsets count CODE POINTS. The editor is
18
+ * JavaScript, so its offsets count UTF-16 CODE UNITS. They differ by one for
19
+ * every character outside the Basic Multilingual Plane before the offset, and
20
+ * LinkedIn posts are full of emoji. `offset_unit` on a response says which unit
21
+ * it uses; absent means `code_point`. The normalizers convert at the boundary,
22
+ * so everything past them is UTF-16.
23
+ *
24
+ * ── STATUS OF THESE SHAPES ──────────────────────────────────────────────────
25
+ * All three follow the FINAL contracts: blame start-simpli-api #507
26
+ * (startsim-gulrr), suggestions and comments #508 (startsim-txfrl,
27
+ * startsim-a76zp). Every response declares `offset_unit: "utf16"` and the
28
+ * `version` its positions were mapped to; suggestions and comments carry a
29
+ * nested `author` and a `position {state, from, to, version}` (null when not
30
+ * placed); blame spans carry flat actor columns, and an unknown span carries
31
+ * `via: "unknown"`, a `reason` code and a `detail` sentence. Older spellings
32
+ * the normalizer still reads are kept only so a camelising client works.
33
+ */
34
+ import type { ActorKind, ActorKindSource } from '../ActorIdentity';
35
+
36
+ export type { ActorKind, ActorKindSource };
37
+
38
+ /** Which unit a response's offsets count. */
39
+ export type OffsetUnit = 'code_point' | 'utf16';
40
+
41
+ /** The actor columns every annotation and blame span carries, both spellings. */
42
+ export interface WireActor {
43
+ actor_sub?: string | null;
44
+ actorSub?: string | null;
45
+ actor_label?: string | null;
46
+ actorLabel?: string | null;
47
+ actor_kind?: ActorKind | string | null;
48
+ actorKind?: ActorKind | string | null;
49
+ actor_kind_source?: ActorKindSource | string | null;
50
+ actorKindSource?: ActorKindSource | string | null;
51
+ }
52
+
53
+ /** The nested actor object the suggestion and comment routes answer with
54
+ * (start-simpli-api RED branch feat/tc-suggestions-comments): `author`,
55
+ * `resolution.by`. */
56
+ export interface WireAuthor {
57
+ sub?: string | null;
58
+ label?: string | null;
59
+ kind?: ActorKind | string | null;
60
+ kind_source?: ActorKindSource | string | null;
61
+ kindSource?: ActorKindSource | string | null;
62
+ }
63
+
64
+ /** Where the server mapped an anchor to on read: current-version offsets and
65
+ * the mapping's outcome. `from`/`to` are `null` when orphaned. */
66
+ export interface PositionWire {
67
+ from?: number | null;
68
+ to?: number | null;
69
+ state?: AnchorState | string | null;
70
+ /** The record version the position was mapped to. */
71
+ version?: number | null;
72
+ }
73
+
74
+ /** An actor after normalization. `label` may be `''` (a service token can carry
75
+ * no email); `kind` is three-state and `unknown` is calm, not a defect. */
76
+ export interface TrackActor {
77
+ sub: string;
78
+ label: string;
79
+ kind: ActorKind;
80
+ kindSource?: ActorKindSource;
81
+ }
82
+
83
+ /* -------------------------------------------------------------------------- */
84
+ /* BLAME GET /api/v1/entities/<id>/blame/?field=<f> (startsim-gulrr) */
85
+ /* -------------------------------------------------------------------------- */
86
+
87
+ /** How a span's text got there. `restore` credits the restorer; an accepted
88
+ * suggestion credits its AUTHOR, with `accepted_by` the acceptor; `unknown`
89
+ * spans carry a `reason` and no actor at all. */
90
+ export type BlameVia = 'edit' | 'accepted_suggestion' | 'restore' | 'unknown';
91
+
92
+ /** Why nobody can be credited with a span. */
93
+ export type BlameReason = 'before_history' | 'truncated' | 'values_omitted' | 'chain_break' | 'unrecorded_change';
94
+
95
+ /**
96
+ * One span of GET /api/v1/entities/<id>/blame/?field=<f> (start-simpli-api
97
+ * #507, final contract in startsim-gulrr). Spans are contiguous, non-empty,
98
+ * in order and cover the whole text. Author columns are FLAT on a known span;
99
+ * an unknown span has none of them.
100
+ */
101
+ export interface BlameSpanWire extends WireActor {
102
+ from: number;
103
+ to: number;
104
+ via?: BlameVia | string | null;
105
+ /** `null` on an unknown span that no revision touched. */
106
+ version?: number | null;
107
+ at?: string | null;
108
+ /** accepted_suggestion only: the ACCEPTOR, with actor_* keys. */
109
+ accepted_by?: WireActor | null;
110
+ acceptedBy?: WireActor | null;
111
+ suggestion_id?: string | null;
112
+ set_id?: string | null;
113
+ /** restore only. */
114
+ restored_from?: number | null;
115
+ restoredFrom?: number | null;
116
+ /** unknown only. */
117
+ reason?: BlameReason | string | null;
118
+ /** unknown only: a readable sentence. */
119
+ detail?: string | null;
120
+ }
121
+
122
+ export interface BlameResponseWire {
123
+ entity_id?: string;
124
+ field?: string;
125
+ /** The record's current version; the spans describe its text. */
126
+ version?: number | null;
127
+ /** Always "utf16" on this route. */
128
+ offset_unit?: OffsetUnit | null;
129
+ offsetUnit?: OffsetUnit | null;
130
+ /** UTF-16 length of the current text. */
131
+ length?: number | null;
132
+ /** False when the type's history is off: then `spans` is null and
133
+ * `detail` says why. */
134
+ recorded?: boolean | null;
135
+ history?: { enabled?: boolean; declared_by?: string } | null;
136
+ trail_starts?: { version?: number; at?: string; from_creation?: boolean } | null;
137
+ spans?: BlameSpanWire[] | null;
138
+ detail?: string | null;
139
+ }
140
+
141
+ /* -------------------------------------------------------------------------- */
142
+ /* SUGGESTIONS GET/POST /api/v1/entities/<id>/suggestions/ (startsim-txfrl) */
143
+ /* -------------------------------------------------------------------------- */
144
+
145
+ export type SuggestionStatus = 'open' | 'accepted' | 'rejected' | 'withdrawn' | 'superseded';
146
+
147
+ /** Where an anchor landed after the server mapped it to the current version
148
+ * (design section c). An orphaned anchor is NEVER painted at a guessed place. */
149
+ export type AnchorState = 'mapped' | 'reanchored' | 'orphaned';
150
+
151
+ export interface AnchorWire {
152
+ from: number;
153
+ to: number;
154
+ quote?: string | null;
155
+ prefix?: string | null;
156
+ suffix?: string | null;
157
+ }
158
+
159
+ export interface SuggestionWire extends WireActor {
160
+ id: string;
161
+ /** The author, as the RED contract nests it. Flat actor columns are read
162
+ * as a fallback. */
163
+ author?: WireAuthor | null;
164
+ /** The anchor mapped to the CURRENT version, with its state. Preferred
165
+ * over every other spelling below. */
166
+ position?: PositionWire | null;
167
+ set_id?: string | null;
168
+ setId?: string | null;
169
+ field?: string | null;
170
+ base_version?: number | null;
171
+ baseVersion?: number | null;
172
+ /** The anchor as PROPOSED, against `base_version`. */
173
+ anchor?: AnchorWire | null;
174
+ /** ASSUMED: the anchor mapped to the CURRENT version on read. Also read:
175
+ * `current`, `mappedAnchor`. When absent, `anchor` is taken to already be
176
+ * in current-version offsets. */
177
+ mapped_anchor?: AnchorWire | null;
178
+ mappedAnchor?: AnchorWire | null;
179
+ current?: AnchorWire | null;
180
+ replacement?: string | null;
181
+ /** The fold of the suggestion's events. Absent means `open`. */
182
+ status?: SuggestionStatus | string | null;
183
+ /** ASSUMED name. Also read: `anchorStatus`, `anchor_state`, `mapping`. */
184
+ anchor_status?: AnchorState | string | null;
185
+ anchorStatus?: AnchorState | string | null;
186
+ created_at?: string | null;
187
+ createdAt?: string | null;
188
+ /** How a closed suggestion was closed: who, when, and (accepted) the
189
+ * revision it produced. Null while open. */
190
+ resolution?: ResolutionWire | null;
191
+ }
192
+
193
+ export interface ResolutionWire {
194
+ action?: string | null;
195
+ by?: WireAuthor | null;
196
+ at?: string | null;
197
+ reason?: string | null;
198
+ revision?: number | null;
199
+ }
200
+
201
+ export interface SuggestionPageWire {
202
+ version?: number | null;
203
+ next?: string | null;
204
+ next_cursor?: string | null;
205
+ nextCursor?: string | null;
206
+ results?: SuggestionWire[] | null;
207
+ offset_unit?: OffsetUnit | null;
208
+ offsetUnit?: OffsetUnit | null;
209
+ }
210
+
211
+ /** The suggesting-mode post (design section d): the WHOLE proposed text; the
212
+ * server hunks it with the shared word diff and supersedes this author's own
213
+ * still-open set. */
214
+ export interface ProposeRequest {
215
+ field: string;
216
+ base_version: number;
217
+ proposed: string;
218
+ }
219
+
220
+ /* -------------------------------------------------------------------------- */
221
+ /* COMMENTS GET /api/v1/entities/<id>/comments/?field=<f> (startsim-a76zp) */
222
+ /* -------------------------------------------------------------------------- */
223
+
224
+ export interface CommentWire extends WireActor {
225
+ id: string;
226
+ thread_id?: string | null;
227
+ author?: WireAuthor | null;
228
+ edited_at?: string | null;
229
+ editedAt?: string | null;
230
+ /** Mentioned subs. Shown in-app only; nothing is emailed (Quinn). */
231
+ mentions?: string[] | null;
232
+ body?: string | null;
233
+ created_at?: string | null;
234
+ createdAt?: string | null;
235
+ /** Tombstoned: the fact of the comment stays, the body is gone. */
236
+ deleted?: boolean | null;
237
+ }
238
+
239
+ export interface CommentThreadWire extends WireActor {
240
+ /** Also read: `id`. */
241
+ thread_id?: string | null;
242
+ threadId?: string | null;
243
+ id?: string | null;
244
+ field?: string | null;
245
+ anchor?: AnchorWire | null;
246
+ mapped_anchor?: AnchorWire | null;
247
+ mappedAnchor?: AnchorWire | null;
248
+ current?: AnchorWire | null;
249
+ anchor_status?: AnchorState | string | null;
250
+ anchorStatus?: AnchorState | string | null;
251
+ position?: PositionWire | null;
252
+ author?: WireAuthor | null;
253
+ /** `open` | `resolved`. `resolved: true` also read. */
254
+ status?: string | null;
255
+ resolution?: ResolutionWire | null;
256
+ base_version?: number | null;
257
+ created_at?: string | null;
258
+ resolved?: boolean | null;
259
+ comments?: CommentWire[] | null;
260
+ /** The body withheld by the per-field read gate. */
261
+ redacted?: boolean | null;
262
+ }
263
+
264
+ export interface CommentPageWire {
265
+ version?: number | null;
266
+ next?: string | null;
267
+ next_cursor?: string | null;
268
+ nextCursor?: string | null;
269
+ results?: CommentThreadWire[] | null;
270
+ offset_unit?: OffsetUnit | null;
271
+ offsetUnit?: OffsetUnit | null;
272
+ }
273
+
274
+ /* -------------------------------------------------------------------------- */
275
+ /* THE INJECTED CLIENT */
276
+ /* -------------------------------------------------------------------------- */
277
+
278
+ /**
279
+ * What the editor surface needs from the server, and nothing it does not.
280
+ * The consuming app binds the record id:
281
+ *
282
+ * ```ts
283
+ * const client: TrackChangesClient = {
284
+ * blame: (q) => api.entities.blame(id, q),
285
+ * listSuggestions: (q) => api.entities.suggestions(id, q),
286
+ * listComments: (q) => api.entities.comments(id, q),
287
+ * propose: (body) => api.entities.propose(id, body),
288
+ * };
289
+ * ```
290
+ *
291
+ * Every read is optional so a host can mount the editor before a route ships.
292
+ * Accept, reject, reply and resolve are the rail's (startsim-whwxd.3), which
293
+ * extends this interface rather than declaring a second one.
294
+ *
295
+ * A 404 from any read means the caller may not read the record or the field.
296
+ * The editor says so; it never treats it as "no annotations".
297
+ */
298
+ export interface TrackChangesClient {
299
+ blame?: (query: { field: string }) => Promise<BlameResponseWire>;
300
+ /** Keyset-paginated: the hook passes `cursor` (the previous page's
301
+ * `next_cursor`) until there is none, because a proposal supersedes ALL of
302
+ * the author's open suggestions and the editor must know every one. */
303
+ listSuggestions?: (query: { field: string; cursor?: string }) => Promise<SuggestionPageWire | SuggestionWire[]>;
304
+ /** `status: 'all'` is what the rail asks for, so resolved threads stay
305
+ * readable (and reopenable) in its resolved list. */
306
+ listComments?: (query: { field: string; cursor?: string; status?: 'open' | 'resolved' | 'all' }) => Promise<CommentPageWire | CommentThreadWire[]>;
307
+ propose?: (body: ProposeRequest) => Promise<unknown>;
308
+
309
+ /* ---- the rail's writes (startsim-whwxd.3). Each is optional, like the reads:
310
+ * a host that has not bound one simply does not get that control. ---- */
311
+
312
+ /** POST suggestions/accept/ {ids, expected_version}. One revision per
313
+ * suggestion, document order, all or nothing. Resolves the RECORD (the
314
+ * record serializer's body) plus `accepted`; the host must adopt its
315
+ * `version`, or its own next save 412s against this accept. */
316
+ accept?: (req: AcceptRequest) => Promise<AcceptResponseWire>;
317
+ /** POST suggestions/reject/ {ids, reason?}. The record does not move. */
318
+ reject?: (req: { ids: string[]; reason?: string }) => Promise<unknown>;
319
+ /** POST suggestions/<id>/withdraw/. Author only. */
320
+ withdraw?: (id: string) => Promise<unknown>;
321
+ /** POST comments/ — a span, field or record comment. */
322
+ comment?: (req: CommentRequest) => Promise<CommentThreadWire>;
323
+ /** POST comments/<thread_id>/replies/. */
324
+ reply?: (threadId: string, req: { body: string; mentions?: string[] }) => Promise<CommentWire>;
325
+ /** POST comments/<thread_id>/resolve/ */
326
+ resolveThread?: (threadId: string) => Promise<unknown>;
327
+ /** POST comments/<thread_id>/reopen/ */
328
+ reopenThread?: (threadId: string) => Promise<unknown>;
329
+ /** GET /api/v1/org/roster/ — the colleagues a comment may @mention. In-app
330
+ * only: the server sends nothing to anyone mentioned (Quinn, 2026-10-07). */
331
+ roster?: (query: { cursor?: string }) => Promise<RosterPageWire>;
332
+ }
333
+
334
+ export interface AcceptRequest {
335
+ ids: string[];
336
+ /** The version the reviewer is looking at. REQUIRED by the route (428
337
+ * without it, 412 when stale). */
338
+ expectedVersion: number;
339
+ }
340
+
341
+ /** The accept answer: the record body, plus what was accepted. */
342
+ export interface AcceptResponseWire {
343
+ id?: string | number;
344
+ version?: number | null;
345
+ data?: Record<string, unknown>;
346
+ accepted?: { suggestions?: string[]; revisions?: number[] } | null;
347
+ [key: string]: unknown;
348
+ }
349
+
350
+ export interface CommentRequest {
351
+ body: string;
352
+ /** Omitted: a whole-record comment. */
353
+ field?: string;
354
+ /** A span comment: both offsets, UTF-16 at `base_version`. */
355
+ from?: number;
356
+ to?: number;
357
+ base_version?: number;
358
+ mentions?: string[];
359
+ }
360
+
361
+ export interface RosterEntryWire {
362
+ sub?: string | null;
363
+ label?: string | null;
364
+ }
365
+
366
+ export interface RosterPageWire {
367
+ next?: string | null;
368
+ next_cursor?: string | null;
369
+ nextCursor?: string | null;
370
+ results?: RosterEntryWire[] | null;
371
+ }
372
+
373
+ /** A colleague who can be @mentioned. */
374
+ export interface RosterMember {
375
+ sub: string;
376
+ label: string;
377
+ }
378
+
379
+ /* -------------------------------------------------------------------------- */
380
+ /* NORMALIZED SHAPES (UTF-16 offsets into the stored string) */
381
+ /* -------------------------------------------------------------------------- */
382
+
383
+ export interface BlameSpan {
384
+ /** Stable within one response: `blame:<from>`. */
385
+ id: string;
386
+ from: number;
387
+ to: number;
388
+ /** `null` for an unknown span. */
389
+ author: TrackActor | null;
390
+ /** Set when nobody can be credited: the sentence a reader is shown. */
391
+ unknownReason: string | null;
392
+ /** The machine-readable reason code of an unknown span. */
393
+ reason: BlameReason | null;
394
+ version: number | null;
395
+ at: string | null;
396
+ via: BlameVia | null;
397
+ acceptedBy: TrackActor | null;
398
+ restoredFrom: number | null;
399
+ }
400
+
401
+ export interface Suggestion {
402
+ id: string;
403
+ setId: string | null;
404
+ from: number;
405
+ to: number;
406
+ /** The text the suggestion replaces (empty for a pure insert). */
407
+ quote: string;
408
+ /** The text it proposes (empty for a pure delete). */
409
+ replacement: string;
410
+ status: SuggestionStatus;
411
+ anchorState: AnchorState;
412
+ author: TrackActor;
413
+ createdAt: string | null;
414
+ }
415
+
416
+ export interface TrackComment {
417
+ id: string;
418
+ body: string | null;
419
+ deleted: boolean;
420
+ author: TrackActor;
421
+ createdAt: string | null;
422
+ /** Mentioned subs; shown in-app only. */
423
+ mentions: string[];
424
+ editedAt: string | null;
425
+ }
426
+
427
+ export interface CommentThread {
428
+ id: string;
429
+ /** The field the thread is on; `null` for a whole-record comment. */
430
+ field: string | null;
431
+ /** Who resolved or reopened it last, and when. */
432
+ resolution: { action: string; by: TrackActor; at: string | null } | null;
433
+ /** `null` for a whole-record or whole-field comment. */
434
+ from: number | null;
435
+ to: number | null;
436
+ quote: string;
437
+ anchorState: AnchorState;
438
+ resolved: boolean;
439
+ redacted: boolean;
440
+ comments: TrackComment[];
441
+ }
442
+
443
+ /** What a selection becomes when a host anchors a comment to it. UTF-16
444
+ * offsets; convert with `toServerOffsets` before posting. */
445
+ export interface SelectionAnchor {
446
+ from: number;
447
+ to: number;
448
+ quote: string;
449
+ prefix: string;
450
+ suffix: string;
451
+ }