review-workspace 1.3.0 → 1.4.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/dist/questions/questions-log.js +51 -5
- package/dist/server/create-server.js +26 -3
- package/package.json +1 -1
- package/ui-dist/_app/index-CIfDjuhH.js +118 -0
- package/ui-dist/_app/index-DlZHsLuA.css +1 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/_app/index-BxFQbyAz.js +0 -118
- package/ui-dist/_app/index-CegXaDjN.css +0 -1
|
@@ -20,18 +20,26 @@ function readLog(bundlePath) {
|
|
|
20
20
|
function appendEntry(bundlePath, entry) {
|
|
21
21
|
appendFileSync(logPath(bundlePath), JSON.stringify(entry) + '\n');
|
|
22
22
|
}
|
|
23
|
-
/** Raises a new, immutable
|
|
24
|
-
export function raiseQuestion(bundlePath, body, target) {
|
|
23
|
+
/** Raises a new, immutable Comment. Never mutates or removes any existing log entry. */
|
|
24
|
+
export function raiseQuestion(bundlePath, body, target, kind = 'question') {
|
|
25
25
|
const entry = {
|
|
26
26
|
type: 'raised',
|
|
27
27
|
id: randomUUID(),
|
|
28
28
|
createdAt: new Date().toISOString(),
|
|
29
29
|
body,
|
|
30
30
|
target,
|
|
31
|
-
kind
|
|
31
|
+
kind,
|
|
32
32
|
};
|
|
33
33
|
appendEntry(bundlePath, entry);
|
|
34
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
id: entry.id,
|
|
36
|
+
createdAt: entry.createdAt,
|
|
37
|
+
body: entry.body,
|
|
38
|
+
target: entry.target,
|
|
39
|
+
kind,
|
|
40
|
+
status: 'open',
|
|
41
|
+
resolved: false,
|
|
42
|
+
};
|
|
35
43
|
}
|
|
36
44
|
/**
|
|
37
45
|
* Withdraws an existing question Comment and raises its replacement in one call —
|
|
@@ -64,9 +72,39 @@ export function withdrawAndReplaceQuestion(bundlePath, questionId, replacementBo
|
|
|
64
72
|
target: replacement.target,
|
|
65
73
|
kind: 'question',
|
|
66
74
|
status: 'open',
|
|
75
|
+
resolved: false,
|
|
67
76
|
},
|
|
68
77
|
};
|
|
69
78
|
}
|
|
79
|
+
function isOpenChangeRequest(comment) {
|
|
80
|
+
return comment.kind === 'change-request' && comment.status === 'open' && !comment.resolved;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Marks an open change-request Comment resolved. This is a human-only, reviewer-triggered
|
|
84
|
+
* action recorded in the same append-only log — nothing a Generator writes (the
|
|
85
|
+
* Review Document, `review.next.json`) can ever produce this entry.
|
|
86
|
+
*
|
|
87
|
+
* Validates before writing: a Comment that doesn't exist, or isn't currently an open
|
|
88
|
+
* change-request, is rejected without appending anything to the log.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveComment(bundlePath, commentId) {
|
|
91
|
+
const comment = readQuestions(bundlePath).find((c) => c.id === commentId);
|
|
92
|
+
if (!comment)
|
|
93
|
+
return { outcome: 'not-found' };
|
|
94
|
+
if (!isOpenChangeRequest(comment))
|
|
95
|
+
return { outcome: 'not-resolvable', comment };
|
|
96
|
+
const entry = {
|
|
97
|
+
type: 'resolved',
|
|
98
|
+
id: randomUUID(),
|
|
99
|
+
createdAt: new Date().toISOString(),
|
|
100
|
+
commentId,
|
|
101
|
+
};
|
|
102
|
+
appendEntry(bundlePath, entry);
|
|
103
|
+
return {
|
|
104
|
+
outcome: 'resolved',
|
|
105
|
+
comment: { ...comment, resolved: true, resolvedAt: entry.createdAt },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
70
108
|
/** Reduces the append-only log into the current set of Comments. */
|
|
71
109
|
export function readQuestions(bundlePath) {
|
|
72
110
|
const comments = new Map();
|
|
@@ -79,15 +117,23 @@ export function readQuestions(bundlePath) {
|
|
|
79
117
|
target: entry.target,
|
|
80
118
|
kind: entry.kind ?? 'question',
|
|
81
119
|
status: 'open',
|
|
120
|
+
resolved: false,
|
|
82
121
|
});
|
|
83
122
|
}
|
|
84
|
-
else {
|
|
123
|
+
else if (entry.type === 'withdrawn') {
|
|
85
124
|
const comment = comments.get(entry.questionId);
|
|
86
125
|
if (comment) {
|
|
87
126
|
comment.status = 'withdrawn';
|
|
88
127
|
comment.supersededBy = entry.replacementId;
|
|
89
128
|
}
|
|
90
129
|
}
|
|
130
|
+
else {
|
|
131
|
+
const comment = comments.get(entry.commentId);
|
|
132
|
+
if (comment && comment.kind === 'change-request') {
|
|
133
|
+
comment.resolved = true;
|
|
134
|
+
comment.resolvedAt = entry.createdAt;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
91
137
|
}
|
|
92
138
|
return [...comments.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
93
139
|
}
|
|
@@ -5,7 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { validateBundle } from "../bundle/validate-bundle.js";
|
|
6
6
|
import { publishBundle } from "../bundle/publish.js";
|
|
7
7
|
import { watchBundle } from "./watch-bundle.js";
|
|
8
|
-
import { raiseQuestion, readQuestions, withdrawAndReplaceQuestion } from "../questions/questions-log.js";
|
|
8
|
+
import { raiseQuestion, readQuestions, resolveComment, withdrawAndReplaceQuestion } from "../questions/questions-log.js";
|
|
9
9
|
import { readReviewState, writeReviewState } from "../review-state/review-state.js";
|
|
10
10
|
import { render } from "../renderer/render.js";
|
|
11
11
|
import { buildReport } from "../export/report.js";
|
|
@@ -15,6 +15,8 @@ export function generateWriteToken() {
|
|
|
15
15
|
}
|
|
16
16
|
const WRITE_TOKEN_HEADER = 'x-write-token';
|
|
17
17
|
const WITHDRAW_PATH = /^\/questions\/([^/]+)\/withdraw$/;
|
|
18
|
+
const RESOLVE_PATH = /^\/questions\/([^/]+)\/resolve$/;
|
|
19
|
+
const COMMENT_KINDS = ['question', 'change-request'];
|
|
18
20
|
const ASSET_CONTENT_TYPES = {
|
|
19
21
|
'.png': 'image/png',
|
|
20
22
|
'.jpg': 'image/jpeg',
|
|
@@ -144,12 +146,16 @@ export function startReviewServer(bundlePath, opts = {}) {
|
|
|
144
146
|
if (req.method === 'POST' && url.pathname === '/questions') {
|
|
145
147
|
readJsonBody(req)
|
|
146
148
|
.then((body) => {
|
|
147
|
-
const { body: text, target } = body;
|
|
149
|
+
const { body: text, target, kind } = body;
|
|
148
150
|
if (typeof text !== 'string' || text.trim() === '') {
|
|
149
151
|
respond(400, { error: 'body is required' });
|
|
150
152
|
return;
|
|
151
153
|
}
|
|
152
|
-
|
|
154
|
+
if (kind !== undefined && !COMMENT_KINDS.includes(kind)) {
|
|
155
|
+
respond(400, { error: `kind must be one of ${COMMENT_KINDS.join(', ')}` });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
respond(201, raiseQuestion(bundlePath, text, target, kind ?? 'question'));
|
|
153
159
|
})
|
|
154
160
|
.catch(() => respond(400, { error: 'invalid JSON body' }));
|
|
155
161
|
return;
|
|
@@ -168,6 +174,23 @@ export function startReviewServer(bundlePath, opts = {}) {
|
|
|
168
174
|
.catch(() => respond(400, { error: 'invalid JSON body' }));
|
|
169
175
|
return;
|
|
170
176
|
}
|
|
177
|
+
// Human-only action: a Reviewer marking a change-request Comment resolved. This
|
|
178
|
+
// never reads from or writes to `review.next.json`/the Generator-owned Review
|
|
179
|
+
// Document — the Generator has no path to setting `resolved` (see ADR 0002).
|
|
180
|
+
const resolveMatch = RESOLVE_PATH.exec(url.pathname);
|
|
181
|
+
if (req.method === 'POST' && resolveMatch) {
|
|
182
|
+
const result = resolveComment(bundlePath, resolveMatch[1]);
|
|
183
|
+
if (result.outcome === 'not-found') {
|
|
184
|
+
respond(404, { error: 'no such Comment' });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (result.outcome === 'not-resolvable') {
|
|
188
|
+
respond(400, { error: 'Comment is not an open change-request' });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
respond(200, result.comment);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
171
194
|
if (req.method === 'PUT' && url.pathname === '/state') {
|
|
172
195
|
readJsonBody(req)
|
|
173
196
|
.then((body) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "review-workspace",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A local, offline decision surface for reviewing a code Comparison — evidence-linked map of intent, behavior, risk, and observable effects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Salih Sayer",
|