@cueloop/schema 0.1.0-alpha.63 → 0.1.0-alpha.65

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,85 @@
1
+ /**
2
+ * The linear view of a session's active path. Every client reads the artifact
3
+ * text, the working copy, and the comments straight off the record, so the
4
+ * record keeps them materialized; this module recomputes them from the path
5
+ * whenever a tip moves. Comments the path does not reach are shelved, never
6
+ * dropped, so a later move can show them again.
7
+ */
8
+
9
+ import { derivePath, followBranch, MAIN_BRANCH, pathOf, type SessionHistory } from "./history";
10
+ import type { Annotation, ReviewSession } from "./types";
11
+
12
+ export interface PathView {
13
+ /** The last agent revision on the path: what the artifact shows. */
14
+ content: string;
15
+ /** The reviewer's edits over it, when the path's head is a reviewer revision. */
16
+ workingCopy: string | undefined;
17
+ /** The comments open on the path, in path order. */
18
+ annotations: Annotation[];
19
+ /** Every other comment the session knows. */
20
+ shelvedAnnotations: Annotation[];
21
+ }
22
+
23
+ /**
24
+ * Fold the active path over every comment the session holds, on or off the
25
+ * path. Comments on the path whose objects are gone stay gone.
26
+ */
27
+ export function viewOfPath(history: SessionHistory, known: Annotation[]): PathView {
28
+ const derived = derivePath(history);
29
+ const agentHead = pathOf(history)
30
+ .filter((entry) => entry.type === "revision" && entry.by === "agent")
31
+ .at(-1);
32
+ const content = agentHead?.type === "revision" ? agentHead.content : derived.head.content;
33
+ const byId = new Map(known.map((annotation) => [annotation.id, annotation]));
34
+ const annotations: Annotation[] = [];
35
+
36
+ for (const id of derived.annotationIds) {
37
+ const annotation = byId.get(id);
38
+
39
+ if (annotation) {
40
+ annotations.push(annotation);
41
+ byId.delete(id);
42
+ }
43
+ }
44
+
45
+ return {
46
+ content,
47
+ workingCopy: derived.head.content === content ? undefined : derived.head.content,
48
+ annotations,
49
+ shelvedAnnotations: [...byId.values()],
50
+ };
51
+ }
52
+
53
+ /** Write a path view back into a record, in place; absent fields are removed. */
54
+ export function applyPathView(session: ReviewSession, view: PathView): void {
55
+ session.artifact = { ...session.artifact, content: view.content };
56
+ session.annotations = view.annotations;
57
+ if (view.workingCopy === undefined) delete session.workingCopy;
58
+ else session.workingCopy = view.workingCopy;
59
+ if (view.shelvedAnnotations.length === 0) delete session.shelvedAnnotations;
60
+ else session.shelvedAnnotations = view.shelvedAnnotations;
61
+ }
62
+
63
+ /**
64
+ * The record as a share carries it: the followed branch's path and what it
65
+ * shows, with no other branch, no shelved comment, and no working copy of the
66
+ * owner's. A followed branch that no longer exists falls back to main. A record
67
+ * without a history travels as it is.
68
+ */
69
+ export function viewFollowing(session: ReviewSession, branch?: string): ReviewSession {
70
+ const asked = branch ?? session.shareBranch ?? MAIN_BRANCH;
71
+ // a branch that no longer exists falls back to main, matching the daemon
72
+ const followed =
73
+ session.history && session.history.tips[asked] === undefined ? MAIN_BRANCH : asked;
74
+ const shared: ReviewSession = { ...session, shareBranch: followed };
75
+
76
+ if (!session.history) return shared;
77
+ const history = followBranch(session.history, followed);
78
+ const known = [...session.annotations, ...(session.shelvedAnnotations ?? [])];
79
+
80
+ shared.history = history;
81
+ applyPathView(shared, viewOfPath(history, known));
82
+ delete shared.shelvedAnnotations;
83
+
84
+ return shared;
85
+ }
package/src/types.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * The ReviewSession primitive. Everything in cueloop renders, annotates, or
3
3
  * resolves this one noun. This module is pure data shapes - no IO, no
4
- * dependencies.
4
+ * dependencies beyond the history shapes.
5
5
  */
6
6
 
7
+ import type { SessionHistory } from "./history";
8
+
7
9
  export const SCHEMA_VERSION = "1";
8
10
 
9
11
  /** A workspace is a repo/branch context holding review sessions. */
@@ -28,7 +30,7 @@ export type ArtifactType = (typeof ARTIFACT_TYPES)[number];
28
30
 
29
31
  /** Trust-boundary guard: is this string one of the artifact primitives? */
30
32
  export function isArtifactType(value: string): value is ArtifactType {
31
- return (ARTIFACT_TYPES as readonly string[]).includes(value);
33
+ return ARTIFACT_TYPES.some((candidate) => candidate === value);
32
34
  }
33
35
 
34
36
  /**
@@ -61,6 +63,17 @@ export interface ArtifactMeta {
61
63
  /** How a file changed, so curation emits the right create/delete headers. */
62
64
  export type DiffFileStatus = "added" | "modified" | "deleted";
63
65
 
66
+ /**
67
+ * One reject decision of a diff review: a whole hunk, or one change block of
68
+ * it when `changeIndex` is set. The daemon curates the working copy from the
69
+ * full set, so every client sees the same patch.
70
+ */
71
+ export interface HunkRejection {
72
+ path: string;
73
+ hunkIndex: number;
74
+ changeIndex?: number;
75
+ }
76
+
64
77
  export interface DiffFileContents {
65
78
  path: string;
66
79
  oldContents: string;
@@ -91,9 +104,11 @@ export interface Anchor {
91
104
  quote: string;
92
105
  prefix: string;
93
106
  suffix: string;
94
- /** Index of the block the anchor was made in (hint). */
107
+ /** Index of the block the anchor starts in (hint). */
95
108
  blockIndex?: number;
96
- /** Character offsets within that block's text (hint). */
109
+ /** Last block of a quote that spans blocks (hint); absent for one block. */
110
+ endBlockIndex?: number;
111
+ /** Character offsets: `start` within the first block, `end` within the last (hint). */
97
112
  start?: number;
98
113
  end?: number;
99
114
  /** Prototype anchors: the CSS selector of the annotated element (authority). */
@@ -127,6 +142,11 @@ export interface Annotation {
127
142
  * planner can tell whose note is whose and never overwrite a collaborator's.
128
143
  */
129
144
  author?: string;
145
+ /**
146
+ * The root comment this one replies to. Absent on a root. A reply shares its
147
+ * root's anchor, so a discussion stays one conversation when the text moves.
148
+ */
149
+ replyTo?: string;
130
150
  /**
131
151
  * Set when a revision addressed this annotation: the agent reported the id
132
152
  * on resubmit ("agent"), or the quoted text disappeared from the revised
@@ -190,6 +210,15 @@ export interface ReviewSession {
190
210
  /** Revision history; artifact.content always equals the latest revision. */
191
211
  revisions: Revision[];
192
212
  annotations: Annotation[];
213
+ /**
214
+ * The session's history as a tree of entries with named branches; the
215
+ * artifact text and the open comments derive from the active path. Absent
216
+ * only on records written before histories existed; the store migrates
217
+ * those on read.
218
+ */
219
+ history?: SessionHistory;
220
+ /** A diff review's reject decisions; the working copy is the patch they leave. */
221
+ curation?: HunkRejection[];
193
222
  /**
194
223
  * The reviewer's working copy of the artifact source (plan edits).
195
224
  * Serializes as ONE unified diff against the submitted revision.
@@ -205,8 +234,18 @@ export interface ReviewSession {
205
234
  verdict: Verdict | null;
206
235
  status: SessionStatus;
207
236
  createdAt: string;
237
+ /**
238
+ * Comments off the active path - removed, or made on a segment a tip moved
239
+ * away from. Nothing is deleted: a navigate or switch that brings their
240
+ * entries back onto the path shows them again.
241
+ */
242
+ shelvedAnnotations?: Annotation[];
243
+ /** The session this one was forked from. */
244
+ parentSessionId?: string;
208
245
  /** Share id once published; lets the planner pull collaborator notes back. */
209
246
  shareId?: string;
247
+ /** The branch the share follows and shows collaborators; `main` when absent. */
248
+ shareBranch?: string;
210
249
  /** SSH fingerprint that created the share; the gateway stamps it to gate pulls. */
211
250
  owner?: string;
212
251
  /**
@@ -41,6 +41,20 @@ function signature(text: string): string {
41
41
  .join("\0");
42
42
  }
43
43
 
44
+ /**
45
+ * Whether a base block is missing from the working source: the working copy
46
+ * holds fewer blocks with its kind and text than the base does. Only such a
47
+ * block can be restored without duplicating content.
48
+ */
49
+ export function isBlockCut(base: string, working: string, block: Block): boolean {
50
+ const matches = (text: string) =>
51
+ parseBlocks(text).filter(
52
+ (candidate) => candidate.kind === block.kind && candidate.text === block.text,
53
+ ).length;
54
+
55
+ return matches(working) < matches(base);
56
+ }
57
+
44
58
  /**
45
59
  * Re-insert a cut base block into the working source before `beforeLine`.
46
60
  * Returns undefined when the block structure round-trips to the base content