@fullstackhouse/cerber 0.36.1 → 0.37.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/core/artifact.js +6 -0
- package/dist/core/diff.js +24 -0
- package/dist/core/refresh.js +3 -0
- package/dist/core/revise.js +2 -0
- package/dist/runner/review.js +6 -1
- package/dist/server/index.js +24 -1
- package/package.json +1 -1
- package/web/dist/assets/index-DWlymBuF.js +248 -0
- package/web/dist/assets/{index-CQ7EKSng.css → index-xd7yDwD1.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DWA4BuJj.js +0 -244
package/dist/core/artifact.js
CHANGED
|
@@ -445,6 +445,12 @@ export const ArtifactSchema = z.object({
|
|
|
445
445
|
})
|
|
446
446
|
.nullable()
|
|
447
447
|
.optional(),
|
|
448
|
+
/**
|
|
449
|
+
* Files the user marked as viewed: path → `fileFingerprint` of the file's
|
|
450
|
+
* patch at the time. A mark whose fingerprint no longer matches the diff is
|
|
451
|
+
* a file that changed since, and reads as not viewed. Absent: none yet.
|
|
452
|
+
*/
|
|
453
|
+
viewed: z.record(z.string(), z.string()).optional(),
|
|
448
454
|
/** Last time this review was pulled forward onto a newer head commit. */
|
|
449
455
|
refresh: RefreshInfoSchema.nullable().default(null),
|
|
450
456
|
/** The last check for findings somebody else already raised. Null: never checked. */
|
package/dist/core/diff.js
CHANGED
|
@@ -114,3 +114,27 @@ export function unclaimedFiles(diff, chapters) {
|
|
|
114
114
|
export function diffLineCounts(diff) {
|
|
115
115
|
return new Map(splitDiffByFile(diff).map((p) => [p.path, p.patch.split("\n").length]));
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* What a file's change says, reduced to a short hash — the thing a "viewed"
|
|
119
|
+
* mark remembers, so a new push that changes the file unticks it.
|
|
120
|
+
*
|
|
121
|
+
* Only the changed and context lines count: a rebase that moves the hunk
|
|
122
|
+
* headers or the blob ids changes nothing you read. A patch with no such lines
|
|
123
|
+
* (a binary, a pure rename) falls back to the whole patch, whose `index` line
|
|
124
|
+
* is then the only thing that tells two versions apart.
|
|
125
|
+
*/
|
|
126
|
+
export function fileFingerprint(patch) {
|
|
127
|
+
const lines = patch.split("\n");
|
|
128
|
+
// `---`/`+++` are file headers only before the first hunk; inside one they
|
|
129
|
+
// are a removed `-- comment` or an added `++ line`, and those are changes.
|
|
130
|
+
const firstHunk = lines.findIndex((l) => l.startsWith("@@"));
|
|
131
|
+
const content = firstHunk < 0 ? [] : lines.slice(firstHunk).filter((l) => /^[+\- ]/.test(l));
|
|
132
|
+
const text = (content.length > 0 ? content : lines).join("\n");
|
|
133
|
+
// FNV-1a, 32-bit.
|
|
134
|
+
let hash = 0x811c9dc5;
|
|
135
|
+
for (let i = 0; i < text.length; i++) {
|
|
136
|
+
hash ^= text.charCodeAt(i);
|
|
137
|
+
hash = Math.imul(hash, 0x01000193);
|
|
138
|
+
}
|
|
139
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
140
|
+
}
|
package/dist/core/refresh.js
CHANGED
|
@@ -80,6 +80,9 @@ export function mergeRunResult(fresh, current) {
|
|
|
80
80
|
notified: current.notified,
|
|
81
81
|
calibration: current.calibration,
|
|
82
82
|
filed: current.filed,
|
|
83
|
+
// Reading progress is the user's; a mark on a file the run saw change
|
|
84
|
+
// unticks itself through its fingerprint.
|
|
85
|
+
viewed: current.viewed,
|
|
83
86
|
// A decision of the user's, like the status above it: a handoff that landed
|
|
84
87
|
// while the run worked has already moved the request on GitHub, and a run
|
|
85
88
|
// that wrote `null` over it would leave the row reading "skipped" with
|
package/dist/core/revise.js
CHANGED
|
@@ -72,6 +72,8 @@ export function mergeConcurrentEdits(before, after, current) {
|
|
|
72
72
|
// Nor does a turn announce anything: the ledger is the poll's, and a turn
|
|
73
73
|
// takes minutes, which is long enough for a poll to have stamped it.
|
|
74
74
|
notified: current.notified,
|
|
75
|
+
// Ticked while the turn ran, and never the turn's to touch.
|
|
76
|
+
viewed: current.viewed,
|
|
75
77
|
// Nor does it compare the draft with what others said; that check folds
|
|
76
78
|
// onto the disk on its own.
|
|
77
79
|
raisedCheck: current.raisedCheck,
|
package/dist/runner/review.js
CHANGED
|
@@ -167,6 +167,9 @@ async function performReview(ref, pr, existing, opts, log) {
|
|
|
167
167
|
// `existing` was loaded minutes ago, and a poll may have announced the row
|
|
168
168
|
// since.
|
|
169
169
|
notified: existing?.notified,
|
|
170
|
+
// Reading progress is the user's, like the chat. A file this run finds
|
|
171
|
+
// changed unticks itself through its fingerprint.
|
|
172
|
+
viewed: existing?.viewed,
|
|
170
173
|
pr,
|
|
171
174
|
diff,
|
|
172
175
|
summary: "",
|
|
@@ -220,7 +223,9 @@ async function performReview(ref, pr, existing, opts, log) {
|
|
|
220
223
|
// how the reopen happens at all. A settle that appeared since is the one this
|
|
221
224
|
// must not touch.
|
|
222
225
|
const decided = existing ? userOwnsStatus(existing) : false;
|
|
223
|
-
const claimed = await updateArtifactByKey(artifactKey(artifact.id), (current) => !decided && userOwnsStatus(current)
|
|
226
|
+
const claimed = await updateArtifactByKey(artifactKey(artifact.id), (current) => !decided && userOwnsStatus(current)
|
|
227
|
+
? current
|
|
228
|
+
: { ...artifact, notified: current.notified, viewed: current.viewed });
|
|
224
229
|
const held = claimed !== null && !decided && userOwnsStatus(claimed);
|
|
225
230
|
if (!held) {
|
|
226
231
|
if (claimed)
|
package/dist/server/index.js
CHANGED
|
@@ -355,7 +355,9 @@ export async function buildApp(opts) {
|
|
|
355
355
|
// decision, and claiming the row for a run would erase it — the detached
|
|
356
356
|
// run below folds its draft under whatever the row says instead
|
|
357
357
|
// (`mergeRunResult`), which is the same line every other writer holds.
|
|
358
|
-
const claimed = await updateArtifactByKey(artifactKey(artifact.id), (current) => userOwnsStatus(current)
|
|
358
|
+
const claimed = await updateArtifactByKey(artifactKey(artifact.id), (current) => userOwnsStatus(current)
|
|
359
|
+
? current
|
|
360
|
+
: { ...artifact, notified: current.notified, viewed: current.viewed });
|
|
359
361
|
// Declining the claim is only half the job: the run below forces, and a
|
|
360
362
|
// forced run reopens a settled row on purpose. So the decision ends the
|
|
361
363
|
// request — hand it back and start nothing.
|
|
@@ -520,6 +522,27 @@ export async function buildApp(opts) {
|
|
|
520
522
|
return c.json({ error: "not found" }, 404);
|
|
521
523
|
return c.json(updated);
|
|
522
524
|
});
|
|
525
|
+
// Reading progress, not a review edit — so a sent review takes it too.
|
|
526
|
+
app.put("/api/reviews/:key/viewed", async (c) => {
|
|
527
|
+
const body = await c.req.json().catch(() => null);
|
|
528
|
+
if (typeof body?.path !== "string" || !body.path) {
|
|
529
|
+
return c.json({ error: "path must be a non-empty string" }, 400);
|
|
530
|
+
}
|
|
531
|
+
if (body.fingerprint !== null && typeof body.fingerprint !== "string") {
|
|
532
|
+
return c.json({ error: "fingerprint must be a string, or null to unmark" }, 400);
|
|
533
|
+
}
|
|
534
|
+
const updated = await updateArtifactByKey(c.req.param("key"), (a) => {
|
|
535
|
+
const viewed = { ...a.viewed };
|
|
536
|
+
if (body.fingerprint === null)
|
|
537
|
+
delete viewed[body.path];
|
|
538
|
+
else
|
|
539
|
+
viewed[body.path] = body.fingerprint;
|
|
540
|
+
return { ...a, viewed };
|
|
541
|
+
});
|
|
542
|
+
if (!updated)
|
|
543
|
+
return c.json({ error: "not found" }, 404);
|
|
544
|
+
return c.json(updated);
|
|
545
|
+
});
|
|
523
546
|
// ---- Freshness: pull a review forward onto the PR's current head ----
|
|
524
547
|
// Read-only against GitHub (pr view + pr diff); writes only the local artifact.
|
|
525
548
|
app.post("/api/reviews/:key/refresh", async (c) => {
|
package/package.json
CHANGED