@cueloop/schema 0.1.0-alpha.6 → 0.1.0-alpha.60
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 +17 -17
- package/src/anchor.test.ts +154 -39
- package/src/anchor.ts +161 -42
- package/src/diff.test.ts +42 -25
- package/src/diff.ts +112 -77
- package/src/feedback.test.ts +170 -40
- package/src/feedback.ts +124 -47
- package/src/fuzzy.test.ts +92 -0
- package/src/fuzzy.ts +101 -0
- package/src/herdr.test.ts +50 -0
- package/src/herdr.ts +54 -0
- package/src/index.ts +5 -0
- package/src/inline.test.ts +195 -0
- package/src/inline.ts +240 -0
- package/src/markdown.test.ts +76 -21
- package/src/markdown.ts +123 -72
- package/src/participants.test.ts +58 -0
- package/src/participants.ts +38 -0
- package/src/types.test.ts +30 -0
- package/src/types.ts +124 -9
- package/src/working-copy.test.ts +156 -0
- package/src/working-copy.ts +66 -0
package/src/markdown.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Markdown block model
|
|
3
|
-
*
|
|
2
|
+
* Markdown block model: parse source into addressable blocks that record
|
|
3
|
+
* their source line ranges. Rendered-to-source mapping is block+line
|
|
4
4
|
* granular; the quote carries sub-block precision. Round-trip safe for the
|
|
5
5
|
* subset cueloop plans use; unknown constructs fall back to paragraph blocks
|
|
6
6
|
* so no content is ever lost.
|
|
@@ -19,113 +19,164 @@ export interface Block {
|
|
|
19
19
|
lineEnd: number;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The leading block markers the parser strips from a line: headings (h1-h3),
|
|
24
|
+
* bullet list, ordered list, and blockquote. Single source of truth so the
|
|
25
|
+
* anchor resolver strips exactly the same set and the two never drift.
|
|
26
|
+
*/
|
|
27
|
+
export const LEADING_BLOCK_MARKER = /^(?:#{1,3} |- |\d+\. |> )/;
|
|
28
|
+
|
|
29
|
+
/** Remove one leading markdown block marker, or return the line unchanged. */
|
|
30
|
+
export function stripLeadingBlockMarker(line: string): string {
|
|
31
|
+
return line.replace(LEADING_BLOCK_MARKER, "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isMarkerLine(line: string): boolean {
|
|
23
35
|
return (
|
|
24
|
-
|
|
25
|
-
l.startsWith("# ") ||
|
|
26
|
-
l.startsWith("## ") ||
|
|
27
|
-
l.startsWith("### ") ||
|
|
28
|
-
l.startsWith("> ") ||
|
|
29
|
-
/^- /.test(l) ||
|
|
30
|
-
/^\d+\. /.test(l) ||
|
|
31
|
-
/^(---|\*\*\*|___)\s*$/.test(l)
|
|
36
|
+
line.startsWith("```") || LEADING_BLOCK_MARKER.test(line) || /^(---|\*\*\*|___)\s*$/.test(line)
|
|
32
37
|
);
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
export function parseBlocks(
|
|
36
|
-
const lines =
|
|
40
|
+
export function parseBlocks(markdown: string): Block[] {
|
|
41
|
+
const lines = markdown.split("\n");
|
|
37
42
|
const blocks: Block[] = [];
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
let lineIndex = 0;
|
|
44
|
+
|
|
45
|
+
while (lineIndex < lines.length) {
|
|
46
|
+
const line = lines[lineIndex]!;
|
|
47
|
+
|
|
48
|
+
if (line.trim() === "") {
|
|
49
|
+
lineIndex++;
|
|
43
50
|
continue;
|
|
44
51
|
}
|
|
45
|
-
if (
|
|
46
|
-
const start =
|
|
47
|
-
const lang =
|
|
48
|
-
|
|
52
|
+
if (line.startsWith("```")) {
|
|
53
|
+
const start = lineIndex;
|
|
54
|
+
const lang = line.slice(3).trim() || undefined;
|
|
55
|
+
|
|
56
|
+
lineIndex++;
|
|
49
57
|
const body: string[] = [];
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
|
|
59
|
+
while (lineIndex < lines.length && !lines[lineIndex]!.startsWith("```")) {
|
|
60
|
+
body.push(lines[lineIndex]!);
|
|
61
|
+
lineIndex++;
|
|
53
62
|
}
|
|
54
|
-
const end = Math.min(
|
|
55
|
-
|
|
63
|
+
const end = Math.min(lineIndex, lines.length - 1);
|
|
64
|
+
|
|
65
|
+
lineIndex++;
|
|
56
66
|
blocks.push({ kind: "code", text: body.join("\n"), lang, lineStart: start, lineEnd: end });
|
|
57
|
-
} else if (
|
|
58
|
-
blocks.push({
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
} else if (line.startsWith("### ")) {
|
|
68
|
+
blocks.push({
|
|
69
|
+
kind: "h3",
|
|
70
|
+
text: stripLeadingBlockMarker(line),
|
|
71
|
+
lineStart: lineIndex,
|
|
72
|
+
lineEnd: lineIndex,
|
|
73
|
+
});
|
|
74
|
+
lineIndex++;
|
|
75
|
+
} else if (line.startsWith("## ")) {
|
|
76
|
+
blocks.push({
|
|
77
|
+
kind: "h2",
|
|
78
|
+
text: stripLeadingBlockMarker(line),
|
|
79
|
+
lineStart: lineIndex,
|
|
80
|
+
lineEnd: lineIndex,
|
|
81
|
+
});
|
|
82
|
+
lineIndex++;
|
|
83
|
+
} else if (line.startsWith("# ")) {
|
|
84
|
+
blocks.push({
|
|
85
|
+
kind: "h1",
|
|
86
|
+
text: stripLeadingBlockMarker(line),
|
|
87
|
+
lineStart: lineIndex,
|
|
88
|
+
lineEnd: lineIndex,
|
|
89
|
+
});
|
|
90
|
+
lineIndex++;
|
|
91
|
+
} else if (/^(---|\*\*\*|___)\s*$/.test(line)) {
|
|
92
|
+
blocks.push({ kind: "hr", text: "", lineStart: lineIndex, lineEnd: lineIndex });
|
|
93
|
+
lineIndex++;
|
|
94
|
+
} else if (line.startsWith("> ")) {
|
|
95
|
+
const start = lineIndex;
|
|
71
96
|
const body: string[] = [];
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
|
|
98
|
+
while (lineIndex < lines.length && lines[lineIndex]!.startsWith("> ")) {
|
|
99
|
+
body.push(stripLeadingBlockMarker(lines[lineIndex]!));
|
|
100
|
+
lineIndex++;
|
|
75
101
|
}
|
|
76
|
-
blocks.push({
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
blocks.push({
|
|
103
|
+
kind: "quote",
|
|
104
|
+
text: body.join("\n"),
|
|
105
|
+
lineStart: start,
|
|
106
|
+
lineEnd: lineIndex - 1,
|
|
107
|
+
});
|
|
108
|
+
} else if (line.startsWith("- ")) {
|
|
109
|
+
blocks.push({
|
|
110
|
+
kind: "li",
|
|
111
|
+
text: stripLeadingBlockMarker(line),
|
|
112
|
+
lineStart: lineIndex,
|
|
113
|
+
lineEnd: lineIndex,
|
|
114
|
+
});
|
|
115
|
+
lineIndex++;
|
|
116
|
+
} else if (/^\d+\. /.test(line)) {
|
|
117
|
+
blocks.push({
|
|
118
|
+
kind: "oli",
|
|
119
|
+
text: stripLeadingBlockMarker(line),
|
|
120
|
+
lineStart: lineIndex,
|
|
121
|
+
lineEnd: lineIndex,
|
|
122
|
+
});
|
|
123
|
+
lineIndex++;
|
|
83
124
|
} else {
|
|
84
|
-
const start =
|
|
125
|
+
const start = lineIndex;
|
|
85
126
|
const body: string[] = [];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
127
|
+
|
|
128
|
+
while (
|
|
129
|
+
lineIndex < lines.length &&
|
|
130
|
+
lines[lineIndex]!.trim() !== "" &&
|
|
131
|
+
!isMarkerLine(lines[lineIndex]!)
|
|
132
|
+
) {
|
|
133
|
+
body.push(lines[lineIndex]!);
|
|
134
|
+
lineIndex++;
|
|
89
135
|
}
|
|
90
|
-
blocks.push({ kind: "p", text: body.join("\n"), lineStart: start, lineEnd:
|
|
136
|
+
blocks.push({ kind: "p", text: body.join("\n"), lineStart: start, lineEnd: lineIndex - 1 });
|
|
91
137
|
}
|
|
92
138
|
}
|
|
139
|
+
|
|
93
140
|
return blocks;
|
|
94
141
|
}
|
|
95
142
|
|
|
96
143
|
/** Serialize one block back to its markdown chunk. */
|
|
97
|
-
export function blockToMd(
|
|
98
|
-
switch (
|
|
144
|
+
export function blockToMd(block: Block, ordinal = 1): string {
|
|
145
|
+
switch (block.kind) {
|
|
99
146
|
case "h1":
|
|
100
|
-
return "# " +
|
|
147
|
+
return "# " + block.text;
|
|
101
148
|
case "h2":
|
|
102
|
-
return "## " +
|
|
149
|
+
return "## " + block.text;
|
|
103
150
|
case "h3":
|
|
104
|
-
return "### " +
|
|
151
|
+
return "### " + block.text;
|
|
105
152
|
case "li":
|
|
106
|
-
return "- " +
|
|
153
|
+
return "- " + block.text;
|
|
107
154
|
case "oli":
|
|
108
|
-
return `${ordinal}. ` +
|
|
155
|
+
return `${ordinal}. ` + block.text;
|
|
109
156
|
case "quote":
|
|
110
|
-
return
|
|
157
|
+
return block.text
|
|
111
158
|
.split("\n")
|
|
112
|
-
.map((
|
|
159
|
+
.map((line) => "> " + line)
|
|
113
160
|
.join("\n");
|
|
114
161
|
case "code":
|
|
115
|
-
return "```" + (
|
|
162
|
+
return "```" + (block.lang ?? "") + "\n" + block.text + "\n```";
|
|
116
163
|
case "hr":
|
|
117
164
|
return "---";
|
|
118
165
|
default:
|
|
119
|
-
return
|
|
166
|
+
return block.text;
|
|
120
167
|
}
|
|
121
168
|
}
|
|
122
169
|
|
|
123
170
|
/** The section (nearest preceding heading) a block belongs to. */
|
|
124
171
|
export function sectionOf(blocks: Block[], index: number): string {
|
|
125
|
-
let
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
172
|
+
let sectionTitle = "";
|
|
173
|
+
|
|
174
|
+
for (let blockIndex = 0; blockIndex <= index && blockIndex < blocks.length; blockIndex++) {
|
|
175
|
+
const block = blocks[blockIndex]!;
|
|
176
|
+
|
|
177
|
+
if (block.kind === "h1" || block.kind === "h2" || block.kind === "h3")
|
|
178
|
+
sectionTitle = block.text;
|
|
129
179
|
}
|
|
130
|
-
|
|
180
|
+
|
|
181
|
+
return sectionTitle;
|
|
131
182
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { registerParticipant } from "./participants";
|
|
3
|
+
import { SCHEMA_VERSION, type ReviewSession } from "./types";
|
|
4
|
+
|
|
5
|
+
function emptySession(): ReviewSession {
|
|
6
|
+
return {
|
|
7
|
+
schemaVersion: SCHEMA_VERSION,
|
|
8
|
+
id: "ses_1",
|
|
9
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
10
|
+
artifact: { type: "plan", content: "# Plan\n", meta: {} },
|
|
11
|
+
revisions: [],
|
|
12
|
+
annotations: [],
|
|
13
|
+
verdict: null,
|
|
14
|
+
status: "pending",
|
|
15
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("registerParticipant", () => {
|
|
20
|
+
test("adds a new author with a display name", () => {
|
|
21
|
+
// Act
|
|
22
|
+
const next = registerParticipant(emptySession(), "SHA256:ana", "Ana");
|
|
23
|
+
|
|
24
|
+
// Assert
|
|
25
|
+
expect(next.participants).toEqual([{ id: "SHA256:ana", provider: "ssh", name: "Ana" }]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("records presence without a name (renders anonymous)", () => {
|
|
29
|
+
// Act
|
|
30
|
+
const next = registerParticipant(emptySession(), "SHA256:ana");
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect(next.participants).toEqual([{ id: "SHA256:ana", provider: "ssh" }]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a nameless revisit never erases a name a past visit set", () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
const named = registerParticipant(emptySession(), "SHA256:ana", "Ana");
|
|
39
|
+
|
|
40
|
+
// Act
|
|
41
|
+
const next = registerParticipant(named, "SHA256:ana");
|
|
42
|
+
|
|
43
|
+
// Assert
|
|
44
|
+
expect(next).toBe(named);
|
|
45
|
+
expect(next.participants).toEqual([{ id: "SHA256:ana", provider: "ssh", name: "Ana" }]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a later name updates the existing identity in place", () => {
|
|
49
|
+
// Arrange
|
|
50
|
+
const present = registerParticipant(emptySession(), "SHA256:ana");
|
|
51
|
+
|
|
52
|
+
// Act
|
|
53
|
+
const next = registerParticipant(present, "SHA256:ana", "Ana");
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(next.participants).toEqual([{ id: "SHA256:ana", provider: "ssh", name: "Ana" }]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The participant registry: every annotation carries an `author` id, and the
|
|
3
|
+
* rail resolves that id to a display name through `session.participants`. This
|
|
4
|
+
* registers an author in that registry so a name search on "participant" or
|
|
5
|
+
* "author name" lands here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Identity, ReviewSession } from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Return the session with `author` present in the participant registry, setting
|
|
12
|
+
* the display name when one is given. A nameless call only records presence (the
|
|
13
|
+
* rail renders anonymous) and never erases a name a past visit set. Immutable:
|
|
14
|
+
* the input session is not mutated.
|
|
15
|
+
*/
|
|
16
|
+
export function registerParticipant(
|
|
17
|
+
session: ReviewSession,
|
|
18
|
+
author: string,
|
|
19
|
+
name?: string,
|
|
20
|
+
): ReviewSession {
|
|
21
|
+
const participants = session.participants ?? [];
|
|
22
|
+
const existing = participants.find((participant) => participant.id === author);
|
|
23
|
+
const trimmed = name?.trim();
|
|
24
|
+
|
|
25
|
+
if (existing && !trimmed) return session;
|
|
26
|
+
const next: Identity = {
|
|
27
|
+
id: author,
|
|
28
|
+
provider: "ssh",
|
|
29
|
+
...(trimmed ? { name: trimmed } : existing?.name ? { name: existing.name } : {}),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
...session,
|
|
34
|
+
participants: existing
|
|
35
|
+
? participants.map((participant) => (participant.id === author ? next : participant))
|
|
36
|
+
: [...participants, next],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { isMarkdownArtifact, newAnnotationId, verdictAllows } from "./types";
|
|
3
|
+
|
|
4
|
+
describe("verdictAllows", () => {
|
|
5
|
+
test("only approve maps to allow", () => {
|
|
6
|
+
expect(verdictAllows("approve")).toBe(true);
|
|
7
|
+
expect(verdictAllows("comment")).toBe(false);
|
|
8
|
+
expect(verdictAllows("request_changes")).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("isMarkdownArtifact", () => {
|
|
13
|
+
test("plan and reply are markdown; diff and prototype are not", () => {
|
|
14
|
+
expect(isMarkdownArtifact("plan")).toBe(true);
|
|
15
|
+
expect(isMarkdownArtifact("reply")).toBe(true);
|
|
16
|
+
expect(isMarkdownArtifact("diff")).toBe(false);
|
|
17
|
+
expect(isMarkdownArtifact("prototype")).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("newAnnotationId", () => {
|
|
22
|
+
test("unique by construction within a process, even on one millisecond", () => {
|
|
23
|
+
// Act
|
|
24
|
+
const ids = new Set(Array.from({ length: 10_000 }, () => newAnnotationId()));
|
|
25
|
+
|
|
26
|
+
// Assert
|
|
27
|
+
expect(ids.size).toBe(10_000);
|
|
28
|
+
for (const id of ids) expect(id).toMatch(/^a_[0-9a-z]+_[0-9a-z]+$/);
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The ReviewSession primitive
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The ReviewSession primitive. Everything in cueloop renders, annotates, or
|
|
3
|
+
* resolves this one noun. This module is pure data shapes - no IO, no
|
|
4
|
+
* dependencies.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export const SCHEMA_VERSION = "1";
|
|
@@ -12,15 +12,33 @@ export interface WorkspaceKey {
|
|
|
12
12
|
branch: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* What kind of artifact a review session holds. `plan` and `reply` are both
|
|
17
|
+
* markdown documents (see isMarkdownArtifact) - a plan is a proposal written
|
|
18
|
+
* forward, a reply is the agent's previous message pulled back for review.
|
|
19
|
+
* `diff` is a unified-diff patch; `prototype` is a rendered HTML page.
|
|
20
|
+
*/
|
|
21
|
+
export type ArtifactType = "plan" | "diff" | "prototype" | "reply";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Markdown artifacts (plan, reply) are block-parsed and quote-anchored, so they
|
|
25
|
+
* share the plan render path, first-heading title derivation, and revision
|
|
26
|
+
* drift-assist. A diff (a patch) and a prototype (HTML/DOM) do not - keep this
|
|
27
|
+
* the one place that names the set, so a new markdown primitive joins here once.
|
|
28
|
+
*/
|
|
29
|
+
export function isMarkdownArtifact(type: ArtifactType): boolean {
|
|
30
|
+
return type === "plan" || type === "reply";
|
|
31
|
+
}
|
|
16
32
|
|
|
17
33
|
export interface ArtifactMeta {
|
|
18
34
|
cwd?: string;
|
|
19
35
|
agent?: string;
|
|
20
36
|
/** Agent-native session id, for resume/fork context. */
|
|
21
37
|
agentSessionId?: string;
|
|
22
|
-
/** Path to the plan file on disk, so feedback can reference it. */
|
|
38
|
+
/** Path to the plan or reply markdown file on disk, so feedback can reference it. */
|
|
23
39
|
planPath?: string;
|
|
40
|
+
/** Path to the prototype's entry HTML file on disk. */
|
|
41
|
+
prototypePath?: string;
|
|
24
42
|
/** Pull request reference the diff came from, so the verdict can be posted back. */
|
|
25
43
|
pr?: string;
|
|
26
44
|
/** herdr pane the submitting agent runs in - the review returns focus there. */
|
|
@@ -28,11 +46,29 @@ export interface ArtifactMeta {
|
|
|
28
46
|
title?: string;
|
|
29
47
|
}
|
|
30
48
|
|
|
49
|
+
/** Full old/new contents of one changed file, keyed by its repo-relative path. */
|
|
50
|
+
/** How a file changed, so curation emits the right create/delete headers. */
|
|
51
|
+
export type DiffFileStatus = "added" | "modified" | "deleted";
|
|
52
|
+
|
|
53
|
+
export interface DiffFileContents {
|
|
54
|
+
path: string;
|
|
55
|
+
oldContents: string;
|
|
56
|
+
newContents: string;
|
|
57
|
+
/** git's own classification - not inferred from empty contents, so an
|
|
58
|
+
* existing-empty-file edit is a modify, not a create/delete. */
|
|
59
|
+
status: DiffFileStatus;
|
|
60
|
+
}
|
|
61
|
+
|
|
31
62
|
export interface Artifact {
|
|
32
63
|
type: ArtifactType;
|
|
33
|
-
/** Markdown source for plans; unified-diff text for diffs. */
|
|
64
|
+
/** Markdown source for plans and replies; unified-diff text for diffs. */
|
|
34
65
|
content: string;
|
|
35
66
|
meta: ArtifactMeta;
|
|
67
|
+
/**
|
|
68
|
+
* Full file contents for a diff artifact, so hunk curation produces an
|
|
69
|
+
* exactly applyable patch; absent for legacy or partial (PR) diffs.
|
|
70
|
+
*/
|
|
71
|
+
files?: DiffFileContents[];
|
|
36
72
|
}
|
|
37
73
|
|
|
38
74
|
/**
|
|
@@ -49,22 +85,58 @@ export interface Anchor {
|
|
|
49
85
|
/** Character offsets within that block's text (hint). */
|
|
50
86
|
start?: number;
|
|
51
87
|
end?: number;
|
|
88
|
+
/** Prototype anchors: the CSS selector of the annotated element (authority). */
|
|
89
|
+
selector?: string;
|
|
52
90
|
}
|
|
53
91
|
|
|
54
|
-
/** The annotation kind set is open
|
|
55
|
-
export type AnnotationKind = "comment" |
|
|
92
|
+
/** The annotation kind set is open; these are the built-ins. */
|
|
93
|
+
export type AnnotationKind = "comment" | (string & {});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Agent-authored context, not reviewer feedback: the guided walk's per-file
|
|
97
|
+
* notes (kind "note", anchored by the file path). Excluded from the feedback
|
|
98
|
+
* document and the reviewer's pending counts - an agent must never receive
|
|
99
|
+
* its own notes back as items to address.
|
|
100
|
+
*/
|
|
101
|
+
export function isAgentNote(annotation: Pick<Annotation, "kind">): boolean {
|
|
102
|
+
return annotation.kind === "note";
|
|
103
|
+
}
|
|
56
104
|
|
|
57
105
|
export interface Annotation {
|
|
58
106
|
id: string;
|
|
59
107
|
kind: AnnotationKind;
|
|
60
108
|
anchor: Anchor;
|
|
61
|
-
/** Comment body
|
|
109
|
+
/** Comment body. */
|
|
62
110
|
body: string;
|
|
63
111
|
/** Set by resolution when the quote can no longer be found. */
|
|
64
112
|
orphan?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* SSH key fingerprint of a share collaborator who authored this note. Absent
|
|
115
|
+
* on the planner's own annotations; the sharing gateway stamps it so the
|
|
116
|
+
* planner can tell whose note is whose and never overwrite a collaborator's.
|
|
117
|
+
*/
|
|
118
|
+
author?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Set when a revision addressed this annotation: the agent reported the id
|
|
121
|
+
* on resubmit ("agent"), or the quoted text disappeared from the revised
|
|
122
|
+
* plan ("drift"). Addressed annotations leave the default rail view and the
|
|
123
|
+
* next feedback document, but are never deleted.
|
|
124
|
+
*/
|
|
125
|
+
resolution?: AnnotationResolution;
|
|
65
126
|
createdAt: string;
|
|
66
127
|
}
|
|
67
128
|
|
|
129
|
+
export interface AnnotationResolution {
|
|
130
|
+
/** The revision number whose submission addressed this annotation. */
|
|
131
|
+
revision: number;
|
|
132
|
+
source: "agent" | "drift";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** An annotation a revision has addressed; it leaves the default views. */
|
|
136
|
+
export function isAddressed(annotation: Annotation): boolean {
|
|
137
|
+
return annotation.resolution !== undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
68
140
|
export type VerdictKind = "comment" | "approve" | "request_changes";
|
|
69
141
|
|
|
70
142
|
export interface Verdict {
|
|
@@ -83,6 +155,22 @@ export interface Revision {
|
|
|
83
155
|
|
|
84
156
|
export type SessionStatus = "pending" | "resolved";
|
|
85
157
|
|
|
158
|
+
/**
|
|
159
|
+
* A person who authored annotations on a session. `id` is the stable key an
|
|
160
|
+
* annotation's `author` points at - the SSH fingerprint today, an OAuth id
|
|
161
|
+
* ("github:…") later. Provider-agnostic so those identities slot in unchanged.
|
|
162
|
+
*/
|
|
163
|
+
export interface Identity {
|
|
164
|
+
/** Stable identity key; equals an annotation's `author`. */
|
|
165
|
+
id: string;
|
|
166
|
+
/** Identity source. One value today; widen the union when OAuth lands. */
|
|
167
|
+
provider: "ssh";
|
|
168
|
+
/** Display name; absent = the collaborator stayed anonymous. */
|
|
169
|
+
name?: string;
|
|
170
|
+
/** Provider handle: a github login, an email, or a short fingerprint. */
|
|
171
|
+
handle?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
86
174
|
export interface ReviewSession {
|
|
87
175
|
schemaVersion: string;
|
|
88
176
|
id: string;
|
|
@@ -97,12 +185,39 @@ export interface ReviewSession {
|
|
|
97
185
|
* Undefined = no direct edits.
|
|
98
186
|
*/
|
|
99
187
|
workingCopy?: string;
|
|
188
|
+
/**
|
|
189
|
+
* File paths the reviewer marked viewed during the guided walk (diff
|
|
190
|
+
* sessions). Persisting with the session means a resumed review keeps its
|
|
191
|
+
* progress. Undefined = the walk never started.
|
|
192
|
+
*/
|
|
193
|
+
viewedPaths?: string[];
|
|
100
194
|
verdict: Verdict | null;
|
|
101
195
|
status: SessionStatus;
|
|
102
196
|
createdAt: string;
|
|
197
|
+
/** Share id once published; lets the planner pull collaborator notes back. */
|
|
198
|
+
shareId?: string;
|
|
199
|
+
/** SSH fingerprint that created the share; the gateway stamps it to gate pulls. */
|
|
200
|
+
owner?: string;
|
|
201
|
+
/**
|
|
202
|
+
* Identities that authored annotations here, keyed by id (union-by-id, like
|
|
203
|
+
* annotations). The gateway records a collaborator's identity and chosen name;
|
|
204
|
+
* the rail resolves an annotation's `author` against this registry.
|
|
205
|
+
*/
|
|
206
|
+
participants?: Identity[];
|
|
103
207
|
}
|
|
104
208
|
|
|
105
209
|
/** comment and request_changes both map to deny in agent-native contracts. */
|
|
106
210
|
export function verdictAllows(kind: VerdictKind): boolean {
|
|
107
211
|
return kind === "approve";
|
|
108
212
|
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Annotation ids are minted client-side. The counter makes ids unique within
|
|
216
|
+
* a process by construction; the random suffix separates concurrent
|
|
217
|
+
* annotators in different processes on the same millisecond.
|
|
218
|
+
*/
|
|
219
|
+
let annotationSeq = 0;
|
|
220
|
+
|
|
221
|
+
export function newAnnotationId(): string {
|
|
222
|
+
return `a_${Date.now().toString(36)}${(annotationSeq++).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
223
|
+
}
|