@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.
- package/package.json +1 -1
- package/src/anchor.test.ts +115 -2
- package/src/anchor.ts +199 -6
- package/src/feedback.test.ts +67 -0
- package/src/feedback.ts +23 -1
- package/src/history.test.ts +395 -0
- package/src/history.ts +419 -0
- package/src/index.ts +2 -0
- package/src/path-view.test.ts +201 -0
- package/src/path-view.ts +85 -0
- package/src/types.ts +43 -4
- package/src/working-copy.ts +14 -0
package/src/path-view.ts
ADDED
|
@@ -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 (
|
|
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
|
|
107
|
+
/** Index of the block the anchor starts in (hint). */
|
|
95
108
|
blockIndex?: number;
|
|
96
|
-
/**
|
|
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
|
/**
|
package/src/working-copy.ts
CHANGED
|
@@ -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
|