review-workspace 1.1.0 → 1.2.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/export/report.js
CHANGED
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
function formatTarget(target) {
|
|
2
|
-
switch (target.type) {
|
|
3
|
-
case 'file':
|
|
4
|
-
return target.path;
|
|
5
|
-
case 'hunk':
|
|
6
|
-
return `${target.path}#hunk${target.hunkIndex}`;
|
|
7
|
-
case 'binary':
|
|
8
|
-
return `${target.path} (binary)`;
|
|
9
|
-
case 'line':
|
|
10
|
-
return `${target.path}:${target.line} (${target.side})`;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
export function buildConcernComment(concern) {
|
|
14
|
-
const prefix = concern.target ? `[${formatTarget(concern.target)}] ` : '';
|
|
15
|
-
return `${prefix}${concern.note}`;
|
|
16
|
-
}
|
|
17
|
-
/** One comment per Concern, plain text — pasteable individually into an external tool. */
|
|
18
|
-
export function buildConcernComments(state) {
|
|
19
|
-
return state.concerns.map(buildConcernComment);
|
|
20
|
-
}
|
|
21
1
|
/**
|
|
22
2
|
* A self-contained Markdown report — no server needed to view it afterward.
|
|
23
3
|
* Read-only with respect to the bundle: never mutates state.json or questions.jsonl.
|
|
@@ -41,16 +21,6 @@ export function buildReport(document, state) {
|
|
|
41
21
|
}
|
|
42
22
|
}
|
|
43
23
|
lines.push('');
|
|
44
|
-
lines.push('## Concerns');
|
|
45
|
-
if (state.concerns.length === 0) {
|
|
46
|
-
lines.push('_None raised._');
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
for (const comment of buildConcernComments(state)) {
|
|
50
|
-
lines.push(`- ${comment}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
lines.push('');
|
|
54
24
|
lines.push('## Notes');
|
|
55
25
|
if (state.notes.length === 0) {
|
|
56
26
|
lines.push('_None._');
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
// File name predates the Comment/kind split (it only ever held Questions) — kept
|
|
5
|
+
// as-is so bundles created before this change keep reading correctly; only the
|
|
6
|
+
// in-memory shape gained `kind`, not the on-disk path.
|
|
4
7
|
function logPath(bundlePath) {
|
|
5
8
|
return join(bundlePath, 'questions.jsonl');
|
|
6
9
|
}
|
|
@@ -17,15 +20,22 @@ function readLog(bundlePath) {
|
|
|
17
20
|
function appendEntry(bundlePath, entry) {
|
|
18
21
|
appendFileSync(logPath(bundlePath), JSON.stringify(entry) + '\n');
|
|
19
22
|
}
|
|
20
|
-
/** Raises a new, immutable
|
|
23
|
+
/** Raises a new, immutable question Comment. Never mutates or removes any existing log entry. */
|
|
21
24
|
export function raiseQuestion(bundlePath, body, target) {
|
|
22
|
-
const entry = {
|
|
25
|
+
const entry = {
|
|
26
|
+
type: 'raised',
|
|
27
|
+
id: randomUUID(),
|
|
28
|
+
createdAt: new Date().toISOString(),
|
|
29
|
+
body,
|
|
30
|
+
target,
|
|
31
|
+
kind: 'question',
|
|
32
|
+
};
|
|
23
33
|
appendEntry(bundlePath, entry);
|
|
24
|
-
return { id: entry.id, createdAt: entry.createdAt, body: entry.body, target: entry.target, status: 'open' };
|
|
34
|
+
return { id: entry.id, createdAt: entry.createdAt, body: entry.body, target: entry.target, kind: 'question', status: 'open' };
|
|
25
35
|
}
|
|
26
36
|
/**
|
|
27
|
-
* Withdraws an existing
|
|
28
|
-
* correction is recorded as new log entries, never as an edit to the original.
|
|
37
|
+
* Withdraws an existing question Comment and raises its replacement in one call —
|
|
38
|
+
* a correction is recorded as new log entries, never as an edit to the original.
|
|
29
39
|
*/
|
|
30
40
|
export function withdrawAndReplaceQuestion(bundlePath, questionId, replacementBody, replacementTarget) {
|
|
31
41
|
const replacement = {
|
|
@@ -34,6 +44,7 @@ export function withdrawAndReplaceQuestion(bundlePath, questionId, replacementBo
|
|
|
34
44
|
createdAt: new Date().toISOString(),
|
|
35
45
|
body: replacementBody,
|
|
36
46
|
target: replacementTarget,
|
|
47
|
+
kind: 'question',
|
|
37
48
|
};
|
|
38
49
|
const withdrawal = {
|
|
39
50
|
type: 'withdrawn',
|
|
@@ -51,30 +62,32 @@ export function withdrawAndReplaceQuestion(bundlePath, questionId, replacementBo
|
|
|
51
62
|
createdAt: replacement.createdAt,
|
|
52
63
|
body: replacement.body,
|
|
53
64
|
target: replacement.target,
|
|
65
|
+
kind: 'question',
|
|
54
66
|
status: 'open',
|
|
55
67
|
},
|
|
56
68
|
};
|
|
57
69
|
}
|
|
58
|
-
/** Reduces the append-only log into the current set of
|
|
70
|
+
/** Reduces the append-only log into the current set of Comments. */
|
|
59
71
|
export function readQuestions(bundlePath) {
|
|
60
|
-
const
|
|
72
|
+
const comments = new Map();
|
|
61
73
|
for (const entry of readLog(bundlePath)) {
|
|
62
74
|
if (entry.type === 'raised') {
|
|
63
|
-
|
|
75
|
+
comments.set(entry.id, {
|
|
64
76
|
id: entry.id,
|
|
65
77
|
createdAt: entry.createdAt,
|
|
66
78
|
body: entry.body,
|
|
67
79
|
target: entry.target,
|
|
80
|
+
kind: entry.kind ?? 'question',
|
|
68
81
|
status: 'open',
|
|
69
82
|
});
|
|
70
83
|
}
|
|
71
84
|
else {
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
const comment = comments.get(entry.questionId);
|
|
86
|
+
if (comment) {
|
|
87
|
+
comment.status = 'withdrawn';
|
|
88
|
+
comment.supersededBy = entry.replacementId;
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
}
|
|
79
|
-
return [...
|
|
92
|
+
return [...comments.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
80
93
|
}
|
|
@@ -8,7 +8,7 @@ import { watchBundle } from "./watch-bundle.js";
|
|
|
8
8
|
import { raiseQuestion, readQuestions, 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
|
-
import {
|
|
11
|
+
import { buildReport } from "../export/report.js";
|
|
12
12
|
import { resolveAssetPath } from "../security/asset-path.js";
|
|
13
13
|
export function generateWriteToken() {
|
|
14
14
|
return randomBytes(24).toString('hex');
|
|
@@ -118,10 +118,6 @@ export function startReviewServer(bundlePath, opts = {}) {
|
|
|
118
118
|
res.end(buildReport(latest.document, readReviewState(bundlePath)));
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
|
-
if (req.method === 'GET' && url.pathname === '/concerns') {
|
|
122
|
-
respond(200, buildConcernComments(readReviewState(bundlePath)));
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
121
|
if (req.method === 'GET' && url.pathname.startsWith('/assets/')) {
|
|
126
122
|
const assetPath = decodeURIComponent(url.pathname.slice('/assets/'.length));
|
|
127
123
|
const resolution = resolveAssetPath(bundlePath, assetPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "review-workspace",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|