getgloss 0.5.0 → 0.6.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/cli/index.js CHANGED
@@ -12,7 +12,7 @@ import path from "path";
12
12
  // package.json
13
13
  var package_default = {
14
14
  name: "getgloss",
15
- version: "0.5.0",
15
+ version: "0.6.0",
16
16
  description: "Local browser-based diff review for coding-agent loops.",
17
17
  type: "module",
18
18
  packageManager: "pnpm@10.33.2",
@@ -42,31 +42,31 @@ var package_default = {
42
42
  node: ">=20"
43
43
  },
44
44
  dependencies: {
45
- "@hono/node-server": "^1.14.4",
46
- "@pierre/diffs": "^1.2.1",
47
- "@tailwindcss/vite": "^4.1.7",
48
- commander: "^14.0.0",
49
- execa: "^9.5.3",
50
- "get-port": "^7.1.0",
51
- hono: "^4.7.10",
52
- "lucide-react": "^1.16.0",
53
- open: "^10.1.2",
54
- react: "^19.1.0",
55
- "react-dom": "^19.1.0",
56
- ulid: "^3.0.0",
57
- zustand: "^5.0.5"
45
+ "@hono/node-server": "1.19.14",
46
+ "@tailwindcss/vite": "4.3.0",
47
+ commander: "14.0.3",
48
+ execa: "9.6.1",
49
+ "get-port": "7.2.0",
50
+ hono: "4.12.21",
51
+ "lucide-react": "1.16.0",
52
+ open: "10.2.0",
53
+ react: "19.2.6",
54
+ "react-dom": "19.2.6",
55
+ ulid: "3.0.2",
56
+ zustand: "5.0.13"
58
57
  },
59
58
  devDependencies: {
60
- "@biomejs/biome": "^2.0.6",
61
- "@types/node": "^24.0.1",
62
- "@types/react": "^19.1.6",
63
- "@types/react-dom": "^19.1.5",
64
- "@vitejs/plugin-react": "^4.5.2",
65
- tsup: "^8.5.0",
66
- tsx: "^4.20.3",
67
- typescript: "^5.8.3",
68
- vite: "^6.3.5",
69
- vitest: "^3.2.3"
59
+ "@biomejs/biome": "2.4.15",
60
+ "@types/node": "24.12.4",
61
+ "@types/react": "19.2.15",
62
+ "@types/react-dom": "19.2.3",
63
+ "@vitejs/plugin-react": "4.7.0",
64
+ tailwindcss: "4.3.0",
65
+ tsup: "8.5.1",
66
+ tsx: "4.22.3",
67
+ typescript: "5.9.3",
68
+ vite: "6.4.2",
69
+ vitest: "3.2.4"
70
70
  },
71
71
  keywords: [
72
72
  "diff",
@@ -139,32 +139,40 @@ async function ensureDir(dir) {
139
139
  // src/cli/git.ts
140
140
  import { execa } from "execa";
141
141
 
142
- // src/cli/diff-parser.ts
142
+ // src/shared/language.ts
143
143
  import path2 from "path";
144
- var hunkHeaderPattern = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/;
145
- function stripGitPath(input) {
146
- return input.replace(/^[ab]\//, "");
147
- }
144
+ var languageByExtension = {
145
+ cjs: "js",
146
+ css: "css",
147
+ go: "go",
148
+ html: "html",
149
+ js: "js",
150
+ json: "json",
151
+ jsx: "jsx",
152
+ md: "markdown",
153
+ mjs: "js",
154
+ py: "python",
155
+ rb: "ruby",
156
+ rs: "rust",
157
+ sh: "bash",
158
+ swift: "swift",
159
+ ts: "ts",
160
+ tsx: "tsx",
161
+ yaml: "yaml",
162
+ yml: "yaml"
163
+ };
148
164
  function languageForPath(filePath) {
149
165
  const ext = path2.extname(filePath).slice(1).toLowerCase();
150
166
  if (!ext) {
151
167
  return null;
152
168
  }
153
- const map = {
154
- cjs: "js",
155
- mjs: "js",
156
- js: "js",
157
- jsx: "jsx",
158
- ts: "ts",
159
- tsx: "tsx",
160
- py: "python",
161
- rb: "ruby",
162
- sh: "bash",
163
- md: "markdown",
164
- yml: "yaml",
165
- yaml: "yaml"
166
- };
167
- return map[ext] ?? ext;
169
+ return languageByExtension[ext] ?? ext;
170
+ }
171
+
172
+ // src/cli/diff-parser.ts
173
+ var hunkHeaderPattern = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/;
174
+ function stripGitPath(input) {
175
+ return input.replace(/^[ab]\//, "");
168
176
  }
169
177
  function emptyFile() {
170
178
  return {
@@ -466,10 +474,198 @@ async function assertGitAvailable() {
466
474
  // src/cli/lifecycle.ts
467
475
  import { spawn } from "child_process";
468
476
  import { existsSync, openSync } from "fs";
469
- import { readFile, rm, writeFile } from "fs/promises";
477
+ import { rm } from "fs/promises";
470
478
  import { fileURLToPath } from "url";
471
479
  import getPort from "get-port";
472
480
 
481
+ // src/shared/server-info.ts
482
+ import { readFile } from "fs/promises";
483
+
484
+ // src/shared/json.ts
485
+ import { writeFile } from "fs/promises";
486
+ function serializeJson(value) {
487
+ return `${JSON.stringify(value, null, 2)}
488
+ `;
489
+ }
490
+ async function writeJsonFile(filePath, value) {
491
+ await writeFile(filePath, serializeJson(value));
492
+ }
493
+
494
+ // src/shared/validation.ts
495
+ var reviewStatuses = ["pending", "submitted", "cancelled", "resolved"];
496
+ var resolutionStatuses = ["partial", "resolved"];
497
+ var reviewUpdateReasons = ["review-resolved", "comment-resolved", "comment-reopened"];
498
+ var sides = ["L", "R"];
499
+ var diffLineTypes = ["context", "add", "delete"];
500
+ var diffScopeModes = ["working", "branch", "explicit"];
501
+ var diffFallbackReasons = ["working-tree-clean", "missing-branch-base"];
502
+ function parseJson(raw, guard, label) {
503
+ const parsed = JSON.parse(raw);
504
+ return parseJsonValue(parsed, guard, label);
505
+ }
506
+ function parseJsonValue(value, guard, label) {
507
+ if (!guard(value)) {
508
+ throw new Error(`Invalid ${label}`);
509
+ }
510
+ return value;
511
+ }
512
+ function isServerInfo(value) {
513
+ return isRecord(value) && isNumber(value.pid) && isNumber(value.port) && isString(value.version) && isString(value.startedAt) && isString(value.stateDir);
514
+ }
515
+ function isHealthResponse(value) {
516
+ return isRecord(value) && isBoolean(value.ok) && isString(value.version) && isNumber(value.activeReviews);
517
+ }
518
+ function isCreateReviewResponse(value) {
519
+ return isRecord(value) && isReviewMeta(value.meta) && isString(value.url);
520
+ }
521
+ function isListReviewsResponse(value) {
522
+ return isRecord(value) && isArrayOf(value.reviews, isReviewMeta);
523
+ }
524
+ function isOpenResult(value) {
525
+ return isRecord(value) && isString(value.reviewId) && isString(value.url) && isNumber(value.files) && isOptionalNumber(value.comments) && isOptionalString(value.feedbackPath) && isOptionalString(value.markdownPath) && isOptionalString(value.artifactDir);
526
+ }
527
+ function isResolveResult(value) {
528
+ return isRecord(value) && value.ok === true && isString(value.reviewId) && isReviewStatus(value.status) && isResolutionStatus(value.resolutionStatus) && isResolutionCounts(value.comments) && isString(value.path) && isResolutionBundle(value.resolution);
529
+ }
530
+ function isReviewRecord(value) {
531
+ return isRecord(value) && isReviewMeta(value.meta) && isDiffPayload(value.diff) && isOptional(value.feedback, isFeedbackBundle) && isOptional(value.resolution, isResolutionBundle);
532
+ }
533
+ function isStoredReviewMeta(value) {
534
+ return isRecord(value) && isString(value.id) && isString(value.cwd) && isBaseRef(value.base) && isNullableString(value.branch) && isReviewStatus(value.status) && isString(value.createdAt) && isOptionalString(value.submittedAt) && isOptionalString(value.resolvedAt) && isOptionalString(value.artifactDir) && isOptionalString(value.feedbackPath) && isOptionalString(value.markdownPath);
535
+ }
536
+ function isReviewMeta(value) {
537
+ return isStoredReviewMeta(value) && isString(value.artifactDir);
538
+ }
539
+ function isDiffPayload(value) {
540
+ return isRecord(value) && isBaseRef(value.base) && isNullableString(value.branch) && isString(value.cwd) && isDiffScope(value.scope) && isDiffStats(value.stats) && isString(value.rawDiff) && isArrayOf(value.files, isDiffFile) && isString(value.capturedAt);
541
+ }
542
+ function isFeedbackBundle(value) {
543
+ return isRecord(value) && value.version === 1 && isString(value.reviewId) && isString(value.timestamp) && isBaseRef(value.base) && isNullableString(value.branch) && isArrayOf(value.comments, isComment);
544
+ }
545
+ function isResolutionBundle(value) {
546
+ return isRecord(value) && isString(value.reviewId) && isResolutionStatus(value.status) && isNullableString(value.summary) && isNullableString(value.resolvedAt) && isArrayOf(value.comments, isResolvedComment);
547
+ }
548
+ function isReviewEvent(value) {
549
+ if (!isRecord(value) || !isString(value.reviewId) || !isString(value.type)) {
550
+ return false;
551
+ }
552
+ switch (value.type) {
553
+ case "review.opened":
554
+ case "review.cancelled":
555
+ return true;
556
+ case "review.submitted":
557
+ return isRecord(value.counts) && isNumber(value.counts.files) && isNumber(value.counts.comments);
558
+ case "review.updated":
559
+ return isReviewUpdateReason(value.reason) && isReviewStatus(value.status) && isResolutionStatus(value.resolutionStatus) && isResolutionCounts(value.counts);
560
+ default:
561
+ return false;
562
+ }
563
+ }
564
+ function isDiffScope(value) {
565
+ return isRecord(value) && isOneOf(value.mode, diffScopeModes) && isNullableString(value.requestedBase) && isBaseRef(value.base) && isDiffRef(value.comparison) && (value.fallbackReason === null || isOneOf(value.fallbackReason, diffFallbackReasons));
566
+ }
567
+ function isDiffRef(value) {
568
+ return isRecord(value) && isString(value.ref) && isNullableString(value.sha);
569
+ }
570
+ function isBaseRef(value) {
571
+ return isRecord(value) && isString(value.ref) && isString(value.sha);
572
+ }
573
+ function isDiffStats(value) {
574
+ return isRecord(value) && isNumber(value.files) && isNumber(value.additions) && isNumber(value.deletions);
575
+ }
576
+ function isDiffFile(value) {
577
+ return isRecord(value) && isString(value.path) && isNullableString(value.oldPath) && isNumber(value.additions) && isNumber(value.deletions) && isBoolean(value.isBinary) && isBoolean(value.isDeleted) && isBoolean(value.isNew) && isBoolean(value.isRenamed) && isNullableString(value.language) && isArrayOf(value.hunks, isDiffHunk);
578
+ }
579
+ function isDiffHunk(value) {
580
+ return isRecord(value) && isNumber(value.oldStart) && isNumber(value.oldLines) && isNumber(value.newStart) && isNumber(value.newLines) && isString(value.header) && isArrayOf(value.lines, isDiffLine);
581
+ }
582
+ function isDiffLine(value) {
583
+ return isRecord(value) && isOneOf(value.type, diffLineTypes) && isNullableNumber(value.oldLine) && isNullableNumber(value.newLine) && isString(value.content);
584
+ }
585
+ function isComment(value) {
586
+ return isRecord(value) && isString(value.id) && isString(value.filePath) && isNumber(value.startLine) && isNumber(value.endLine) && isOneOf(value.side, sides) && isString(value.body) && isString(value.originalSnippet) && isString(value.createdAt);
587
+ }
588
+ function isResolvedComment(value) {
589
+ return isRecord(value) && isString(value.commentId) && value.status === "resolved" && isOptionalString(value.summary) && isString(value.resolvedAt);
590
+ }
591
+ function isResolutionCounts(value) {
592
+ return isRecord(value) && isNumber(value.total) && isNumber(value.resolved) && isNumber(value.open);
593
+ }
594
+ function isReviewStatus(value) {
595
+ return isOneOf(value, reviewStatuses);
596
+ }
597
+ function isResolutionStatus(value) {
598
+ return isOneOf(value, resolutionStatuses);
599
+ }
600
+ function isReviewUpdateReason(value) {
601
+ return isOneOf(value, reviewUpdateReasons);
602
+ }
603
+ function isRecord(value) {
604
+ return typeof value === "object" && value !== null && !Array.isArray(value);
605
+ }
606
+ function isArrayOf(value, guard) {
607
+ return Array.isArray(value) && value.every(guard);
608
+ }
609
+ function isOptional(value, guard) {
610
+ return value === void 0 || guard(value);
611
+ }
612
+ function isString(value) {
613
+ return typeof value === "string";
614
+ }
615
+ function isOptionalString(value) {
616
+ return value === void 0 || isString(value);
617
+ }
618
+ function isNullableString(value) {
619
+ return value === null || isString(value);
620
+ }
621
+ function isNumber(value) {
622
+ return typeof value === "number" && Number.isFinite(value);
623
+ }
624
+ function isOptionalNumber(value) {
625
+ return value === void 0 || isNumber(value);
626
+ }
627
+ function isNullableNumber(value) {
628
+ return value === null || isNumber(value);
629
+ }
630
+ function isBoolean(value) {
631
+ return typeof value === "boolean";
632
+ }
633
+ function isOneOf(value, options) {
634
+ return typeof value === "string" && options.includes(value);
635
+ }
636
+
637
+ // src/shared/server-info.ts
638
+ async function readServerInfo() {
639
+ let raw;
640
+ try {
641
+ raw = await readFile(globalServerFile(), "utf8");
642
+ } catch (error) {
643
+ if (isFileNotFound(error)) {
644
+ return null;
645
+ }
646
+ throw new Error(`Could not read server info at ${globalServerFile()}: ${formatError(error)}`, {
647
+ cause: error
648
+ });
649
+ }
650
+ try {
651
+ return parseJson(raw, isServerInfo, "server info");
652
+ } catch (error) {
653
+ throw new Error(`Invalid server info at ${globalServerFile()}: ${formatError(error)}`, {
654
+ cause: error
655
+ });
656
+ }
657
+ }
658
+ async function writeServerInfo(info) {
659
+ await ensureDir(globalStateDir());
660
+ await writeJsonFile(globalServerFile(), info);
661
+ }
662
+ function isFileNotFound(error) {
663
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
664
+ }
665
+ function formatError(error) {
666
+ return error instanceof Error ? error.message : String(error);
667
+ }
668
+
473
669
  // src/cli/server-client.ts
474
670
  var ServerClient = class {
475
671
  constructor(baseUrl) {
@@ -477,31 +673,53 @@ var ServerClient = class {
477
673
  }
478
674
  baseUrl;
479
675
  async health() {
480
- return this.get("/api/health");
676
+ return this.get("/api/health", isHealthResponse, "health response");
481
677
  }
482
678
  async createReview(diff) {
483
- return this.post("/api/reviews", diff);
679
+ return this.post("/api/reviews", diff, isCreateReviewResponse, "create review response");
484
680
  }
485
681
  async getReview(reviewId) {
486
- return this.get(`/api/reviews/${reviewId}`);
682
+ return this.get(`/api/reviews/${reviewId}`, isReviewRecord, "review response");
487
683
  }
488
684
  async listReviews() {
489
- return this.get("/api/reviews");
685
+ return this.get("/api/reviews", isListReviewsResponse, "review list response");
490
686
  }
491
687
  async getFeedback(reviewId) {
492
- return this.get(`/api/reviews/${reviewId}/feedback`);
688
+ return this.get(`/api/reviews/${reviewId}/feedback`, isFeedbackBundle, "feedback response");
493
689
  }
494
690
  async markResolved(reviewId, summary) {
495
- return this.post(`/api/reviews/${reviewId}/resolved`, { summary });
691
+ const request = { summary };
692
+ return this.post(
693
+ `/api/reviews/${reviewId}/resolved`,
694
+ request,
695
+ isResolveResult,
696
+ "resolve response"
697
+ );
496
698
  }
497
699
  async resolveComment(reviewId, commentId, summary) {
498
- return this.post(`/api/reviews/${reviewId}/comments/${commentId}/resolved`, { summary });
700
+ const request = { summary };
701
+ return this.post(
702
+ `/api/reviews/${reviewId}/comments/${commentId}/resolved`,
703
+ request,
704
+ isResolveResult,
705
+ "resolve comment response"
706
+ );
499
707
  }
500
708
  async reopenComment(reviewId, commentId) {
501
- return this.delete(`/api/reviews/${reviewId}/comments/${commentId}/resolved`);
709
+ return this.delete(
710
+ `/api/reviews/${reviewId}/comments/${commentId}/resolved`,
711
+ isResolveResult,
712
+ "reopen comment response"
713
+ );
502
714
  }
503
715
  async submitReview(reviewId, comments) {
504
- return this.post(`/api/reviews/${reviewId}/submit`, { comments });
716
+ const request = { comments };
717
+ return this.post(
718
+ `/api/reviews/${reviewId}/submit`,
719
+ request,
720
+ isOpenResult,
721
+ "submit review response"
722
+ );
505
723
  }
506
724
  async watchReview(reviewId, timeoutSeconds) {
507
725
  const deadline = timeoutSeconds && timeoutSeconds > 0 ? Date.now() + timeoutSeconds * 1e3 : null;
@@ -552,7 +770,7 @@ var ServerClient = class {
552
770
  if (!dataLine) {
553
771
  continue;
554
772
  }
555
- const event = JSON.parse(dataLine.slice(5).trim());
773
+ const event = parseJson(dataLine.slice(5).trim(), isReviewEvent, "review event");
556
774
  if (event.type === "review.submitted" || event.type === "review.cancelled") {
557
775
  await reader.cancel().catch(() => void 0);
558
776
  return event;
@@ -560,28 +778,29 @@ var ServerClient = class {
560
778
  }
561
779
  }
562
780
  }
563
- async get(path3) {
781
+ async get(path3, guard, label) {
564
782
  const response = await fetch(`${this.baseUrl}${path3}`);
565
- return parseResponse(response);
783
+ return parseResponse(response, guard, label);
566
784
  }
567
- async post(path3, body) {
785
+ async post(path3, body, guard, label) {
568
786
  const response = await fetch(`${this.baseUrl}${path3}`, {
569
787
  method: "POST",
570
788
  headers: { "content-type": "application/json" },
571
789
  body: JSON.stringify(body)
572
790
  });
573
- return parseResponse(response);
791
+ return parseResponse(response, guard, label);
574
792
  }
575
- async delete(path3) {
793
+ async delete(path3, guard, label) {
576
794
  const response = await fetch(`${this.baseUrl}${path3}`, { method: "DELETE" });
577
- return parseResponse(response);
795
+ return parseResponse(response, guard, label);
578
796
  }
579
797
  };
580
- async function parseResponse(response) {
798
+ async function parseResponse(response, guard, label) {
581
799
  if (!response.ok) {
582
800
  throw new Error(`${response.status} ${response.statusText}: ${await response.text()}`);
583
801
  }
584
- return await response.json();
802
+ const value = await response.json();
803
+ return parseJsonValue(value, guard, label);
585
804
  }
586
805
  function isPrematureWatchEnd(error) {
587
806
  return error instanceof Error && error.message === "watch stream ended before completion";
@@ -594,13 +813,6 @@ async function sleep(milliseconds) {
594
813
  }
595
814
 
596
815
  // src/cli/lifecycle.ts
597
- async function readServerInfo() {
598
- try {
599
- return JSON.parse(await readFile(globalServerFile(), "utf8"));
600
- } catch {
601
- return null;
602
- }
603
- }
604
816
  function serverUrl(info) {
605
817
  return `http://localhost:${info.port}`;
606
818
  }
@@ -652,8 +864,7 @@ async function startServer(options = {}) {
652
864
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
653
865
  stateDir: globalStateDir()
654
866
  };
655
- await writeFile(globalServerFile(), `${JSON.stringify(info, null, 2)}
656
- `);
867
+ await writeServerInfo(info);
657
868
  const deadline = Date.now() + 8e3;
658
869
  while (Date.now() < deadline) {
659
870
  if (await isServerResponsive(info)) {
@@ -687,17 +898,37 @@ function isPidAlive(pid) {
687
898
  }
688
899
 
689
900
  // src/server/store.ts
690
- import { readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
901
+ import { readdir, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
691
902
  import { ulid } from "ulid";
692
903
 
693
- // src/shared/markdown.ts
694
- function formatLineRange(comment) {
695
- const prefix = comment.side;
696
- if (comment.startLine === comment.endLine) {
697
- return `${prefix}${comment.startLine}`;
698
- }
699
- return `${prefix}${comment.startLine}-${prefix}${comment.endLine}`;
904
+ // src/shared/comments.ts
905
+ function compareCommentsByLocation(a, b) {
906
+ return a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine || a.endLine - b.endLine || a.side.localeCompare(b.side);
700
907
  }
908
+ function countCommentFiles(comments) {
909
+ return new Set(comments.map((comment) => comment.filePath)).size;
910
+ }
911
+ function formatLineRange(range, options = {}) {
912
+ const startLine = Math.min(range.startLine, range.endLine);
913
+ const endLine = Math.max(range.startLine, range.endLine);
914
+ if (startLine === endLine) {
915
+ return `${range.side}${startLine}`;
916
+ }
917
+ const endPrefix = options.repeatSideOnEnd === false ? "" : range.side;
918
+ return `${range.side}${startLine}-${endPrefix}${endLine}`;
919
+ }
920
+ function resolutionCounts(feedback, resolvedComments = []) {
921
+ const comments = feedback?.comments ?? [];
922
+ const resolvedIds = new Set(resolvedComments.map((comment) => comment.commentId));
923
+ const resolved = comments.filter((comment) => resolvedIds.has(comment.id)).length;
924
+ return {
925
+ total: comments.length,
926
+ resolved,
927
+ open: comments.length - resolved
928
+ };
929
+ }
930
+
931
+ // src/shared/markdown.ts
701
932
  function fenceFor(snippet) {
702
933
  let fence = "```";
703
934
  while (snippet.includes(fence)) {
@@ -705,40 +936,13 @@ function fenceFor(snippet) {
705
936
  }
706
937
  return fence;
707
938
  }
708
- function languageForPath2(filePath) {
709
- const ext = filePath.split(".").pop()?.toLowerCase();
710
- const map = {
711
- cjs: "js",
712
- css: "css",
713
- go: "go",
714
- html: "html",
715
- js: "js",
716
- json: "json",
717
- jsx: "jsx",
718
- md: "markdown",
719
- mjs: "js",
720
- py: "python",
721
- rb: "ruby",
722
- rs: "rust",
723
- sh: "bash",
724
- swift: "swift",
725
- ts: "ts",
726
- tsx: "tsx",
727
- yaml: "yaml",
728
- yml: "yaml"
729
- };
730
- return ext ? map[ext] ?? ext : "";
731
- }
732
939
  function languageForSnippet(filePath, snippet) {
733
940
  const lines = snippet.split("\n").filter((line) => line.length > 0);
734
941
  const looksLikeUnifiedDiff = lines.length > 0 && lines.some((line) => line.startsWith("+") || line.startsWith("-")) && lines.every((line) => line.startsWith("+") || line.startsWith("-") || line.startsWith(" "));
735
- return looksLikeUnifiedDiff ? "diff" : languageForPath2(filePath);
736
- }
737
- function byFileThenLine(a, b) {
738
- return a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine || a.endLine - b.endLine || a.side.localeCompare(b.side);
942
+ return looksLikeUnifiedDiff ? "diff" : languageForPath(filePath) ?? "";
739
943
  }
740
944
  function serializeFeedbackMarkdown(bundle) {
741
- const comments = [...bundle.comments].sort(byFileThenLine);
945
+ const comments = [...bundle.comments].sort(compareCommentsByLocation);
742
946
  const files = [...new Set(comments.map((comment) => comment.filePath))];
743
947
  const lines = [
744
948
  `# Gloss feedback - ${bundle.timestamp}`,
@@ -764,6 +968,11 @@ function serializeFeedbackMarkdown(bundle) {
764
968
  `;
765
969
  }
766
970
 
971
+ // src/shared/reviews.ts
972
+ function isResolvableReviewStatus(status) {
973
+ return status === "submitted" || status === "resolved";
974
+ }
975
+
767
976
  // src/server/store.ts
768
977
  var ReviewStore = class {
769
978
  reviews = /* @__PURE__ */ new Map();
@@ -808,9 +1017,7 @@ var ReviewStore = class {
808
1017
  timestamp,
809
1018
  base: record.diff.base,
810
1019
  branch: record.diff.branch,
811
- comments: [...comments].sort(
812
- (a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine || a.endLine - b.endLine || a.side.localeCompare(b.side)
813
- )
1020
+ comments: [...comments].sort(compareCommentsByLocation)
814
1021
  };
815
1022
  record.feedback = feedback;
816
1023
  record.meta = { ...record.meta, status: "submitted", submittedAt: timestamp };
@@ -826,17 +1033,15 @@ var ReviewStore = class {
826
1033
  };
827
1034
  await ensureDir(artifactDir);
828
1035
  await Promise.all([
829
- writeFile2(globalReviewMetaFile(id), `${JSON.stringify(record.meta, null, 2)}
830
- `),
831
- writeFile2(feedbackPath, `${JSON.stringify(feedback, null, 2)}
832
- `),
1036
+ writeJsonFile(globalReviewMetaFile(id), record.meta),
1037
+ writeJsonFile(feedbackPath, feedback),
833
1038
  writeFile2(markdownPath, serializeFeedbackMarkdown(feedback))
834
1039
  ]);
835
1040
  this.emit({
836
1041
  type: "review.submitted",
837
1042
  reviewId: id,
838
1043
  counts: {
839
- files: new Set(feedback.comments.map((comment) => comment.filePath)).size,
1044
+ files: countCommentFiles(feedback.comments),
840
1045
  comments: feedback.comments.length
841
1046
  }
842
1047
  });
@@ -898,7 +1103,7 @@ var ReviewStore = class {
898
1103
  ],
899
1104
  record
900
1105
  );
901
- const counts = this.resolutionCounts(record, comments);
1106
+ const counts = resolutionCounts(record.feedback, comments);
902
1107
  const fullyResolved = counts.total === counts.resolved;
903
1108
  const resolution = {
904
1109
  reviewId: id,
@@ -921,7 +1126,7 @@ var ReviewStore = class {
921
1126
  (record.resolution?.comments ?? []).filter((comment) => comment.commentId !== commentId),
922
1127
  record
923
1128
  );
924
- const counts = this.resolutionCounts(record, comments);
1129
+ const counts = resolutionCounts(record.feedback, comments);
925
1130
  const fullyResolved = counts.total > 0 && counts.total === counts.resolved;
926
1131
  const resolvedAt = fullyResolved ? (/* @__PURE__ */ new Date()).toISOString() : null;
927
1132
  const resolution = {
@@ -954,10 +1159,8 @@ var ReviewStore = class {
954
1159
  const dir = globalReviewDir(record.meta.id);
955
1160
  await ensureDir(dir);
956
1161
  await Promise.all([
957
- writeFile2(globalReviewMetaFile(record.meta.id), `${JSON.stringify(record.meta, null, 2)}
958
- `),
959
- writeFile2(globalReviewDiffFile(record.meta.id), `${JSON.stringify(record.diff, null, 2)}
960
- `)
1162
+ writeJsonFile(globalReviewMetaFile(record.meta.id), record.meta),
1163
+ writeJsonFile(globalReviewDiffFile(record.meta.id), record.diff)
961
1164
  ]);
962
1165
  }
963
1166
  async loadKnownReview(id) {
@@ -971,56 +1174,65 @@ var ReviewStore = class {
971
1174
  let entries;
972
1175
  try {
973
1176
  entries = await readdir(globalReviewsDir(), { withFileTypes: true });
974
- } catch {
975
- return;
1177
+ } catch (error) {
1178
+ if (isFileNotFound2(error)) {
1179
+ return;
1180
+ }
1181
+ throw new Error(
1182
+ `Could not read reviews directory at ${globalReviewsDir()}: ${formatError2(error)}`,
1183
+ {
1184
+ cause: error
1185
+ }
1186
+ );
976
1187
  }
977
1188
  await Promise.all(
978
1189
  entries.filter((entry) => entry.isDirectory()).map((entry) => this.loadReview(entry.name))
979
1190
  );
980
1191
  }
981
1192
  async loadReview(id) {
1193
+ const metaPath = globalReviewMetaFile(id);
1194
+ const diffPath = globalReviewDiffFile(id);
1195
+ let metaRaw;
1196
+ let diffRaw;
982
1197
  try {
983
- const [metaRaw, diffRaw] = await Promise.all([
984
- readFile2(globalReviewMetaFile(id), "utf8"),
985
- readFile2(globalReviewDiffFile(id), "utf8")
1198
+ [metaRaw, diffRaw] = await Promise.all([
1199
+ readFile2(metaPath, "utf8"),
1200
+ readFile2(diffPath, "utf8")
986
1201
  ]);
987
- const meta = JSON.parse(metaRaw);
988
- const diff = JSON.parse(diffRaw);
989
- let feedback;
990
- let resolution;
991
- try {
992
- feedback = JSON.parse(
993
- await readFile2(globalReviewFeedbackFile(id), "utf8")
994
- );
995
- } catch {
996
- feedback = void 0;
997
- }
998
- try {
999
- resolution = JSON.parse(
1000
- await readFile2(globalReviewResolvedFile(id), "utf8")
1001
- );
1002
- } catch {
1003
- resolution = void 0;
1202
+ } catch (error) {
1203
+ if (isFileNotFound2(error)) {
1204
+ return null;
1004
1205
  }
1005
- const record = {
1006
- meta: {
1007
- ...meta,
1008
- artifactDir: meta.artifactDir ?? globalReviewDir(id),
1009
- feedbackPath: meta.feedbackPath ?? (feedback ? globalReviewFeedbackFile(id) : void 0),
1010
- markdownPath: meta.markdownPath ?? (feedback ? globalReviewMarkdownFile(id) : void 0)
1011
- },
1012
- diff,
1013
- feedback,
1014
- resolution
1015
- };
1016
- this.reviews.set(id, record);
1017
- return record;
1018
- } catch {
1019
- return null;
1206
+ throw new Error(`Could not load review ${id}: ${formatError2(error)}`, { cause: error });
1020
1207
  }
1208
+ const meta = parseJsonFile(metaRaw, isStoredReviewMeta, "review metadata", metaPath);
1209
+ const diff = parseJsonFile(diffRaw, isDiffPayload, "review diff", diffPath);
1210
+ const feedback = await readOptionalJsonFile(
1211
+ globalReviewFeedbackFile(id),
1212
+ isFeedbackBundle,
1213
+ "review feedback"
1214
+ );
1215
+ const resolution = await readOptionalJsonFile(
1216
+ globalReviewResolvedFile(id),
1217
+ isResolutionBundle,
1218
+ "review resolution"
1219
+ );
1220
+ const record = {
1221
+ meta: {
1222
+ ...meta,
1223
+ artifactDir: meta.artifactDir ?? globalReviewDir(id),
1224
+ feedbackPath: meta.feedbackPath ?? (feedback ? globalReviewFeedbackFile(id) : void 0),
1225
+ markdownPath: meta.markdownPath ?? (feedback ? globalReviewMarkdownFile(id) : void 0)
1226
+ },
1227
+ diff,
1228
+ feedback,
1229
+ resolution
1230
+ };
1231
+ this.reviews.set(id, record);
1232
+ return record;
1021
1233
  }
1022
1234
  assertResolvable(record, id) {
1023
- if (record.meta.status !== "submitted" && record.meta.status !== "resolved") {
1235
+ if (!isResolvableReviewStatus(record.meta.status)) {
1024
1236
  throw new Error(`Review ${id} is ${record.meta.status} and cannot be resolved`);
1025
1237
  }
1026
1238
  if (!record.feedback) {
@@ -1038,17 +1250,15 @@ var ReviewStore = class {
1038
1250
  const resolvedPath = globalReviewResolvedFile(record.meta.id);
1039
1251
  await ensureDir(globalReviewDir(record.meta.id));
1040
1252
  await Promise.all([
1041
- writeFile2(resolvedPath, `${JSON.stringify(resolution, null, 2)}
1042
- `),
1043
- writeFile2(globalReviewMetaFile(record.meta.id), `${JSON.stringify(record.meta, null, 2)}
1044
- `)
1253
+ writeJsonFile(resolvedPath, resolution),
1254
+ writeJsonFile(globalReviewMetaFile(record.meta.id), record.meta)
1045
1255
  ]);
1046
1256
  const result = {
1047
1257
  ok: true,
1048
1258
  reviewId: record.meta.id,
1049
1259
  status: record.meta.status,
1050
1260
  resolutionStatus: resolution.status,
1051
- comments: this.resolutionCounts(record, resolution.comments),
1261
+ comments: resolutionCounts(record.feedback, resolution.comments),
1052
1262
  path: resolvedPath,
1053
1263
  resolution
1054
1264
  };
@@ -1070,19 +1280,34 @@ var ReviewStore = class {
1070
1280
  (a, b) => (feedbackIndex.get(a.commentId) ?? Number.MAX_SAFE_INTEGER) - (feedbackIndex.get(b.commentId) ?? Number.MAX_SAFE_INTEGER)
1071
1281
  );
1072
1282
  }
1073
- resolutionCounts(record, comments) {
1074
- const total = record.feedback.comments.length;
1075
- const resolvedIds = new Set(comments.map((comment) => comment.commentId));
1076
- const resolved = record.feedback.comments.filter(
1077
- (comment) => resolvedIds.has(comment.id)
1078
- ).length;
1079
- return {
1080
- total,
1081
- resolved,
1082
- open: total - resolved
1083
- };
1084
- }
1085
1283
  };
1284
+ async function readOptionalJsonFile(filePath, guard, label) {
1285
+ let raw;
1286
+ try {
1287
+ raw = await readFile2(filePath, "utf8");
1288
+ } catch (error) {
1289
+ if (isFileNotFound2(error)) {
1290
+ return void 0;
1291
+ }
1292
+ throw new Error(`Could not read ${label} at ${filePath}: ${formatError2(error)}`, {
1293
+ cause: error
1294
+ });
1295
+ }
1296
+ return parseJsonFile(raw, guard, label, filePath);
1297
+ }
1298
+ function parseJsonFile(raw, guard, label, filePath) {
1299
+ try {
1300
+ return parseJson(raw, guard, label);
1301
+ } catch (error) {
1302
+ throw new Error(`Invalid ${label} at ${filePath}: ${formatError2(error)}`, { cause: error });
1303
+ }
1304
+ }
1305
+ function isFileNotFound2(error) {
1306
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
1307
+ }
1308
+ function formatError2(error) {
1309
+ return error instanceof Error ? error.message : String(error);
1310
+ }
1086
1311
  var reviewStore = new ReviewStore();
1087
1312
 
1088
1313
  // src/cli/status.ts
@@ -1179,7 +1404,7 @@ program.command("status").description("Show server and active reviews").action(a
1179
1404
  responsive && info ? `Gloss server running at ${serverUrl(info)} with ${reviews.length} active review(s)` : "Gloss server is not running"
1180
1405
  );
1181
1406
  });
1182
- program.command("stop").description("Stop the managed background server").option("--all", "reserved for future multi-server cleanup").action(async () => {
1407
+ program.command("stop").description("Stop the managed background server").action(async () => {
1183
1408
  const globals = program.opts();
1184
1409
  const result = await stopServer();
1185
1410
  globals.json ? printJson(result) : printPlain(result.stopped ? "Gloss server stopped" : "Gloss server was not running");
@@ -1230,11 +1455,6 @@ program.command("doctor").description("Diagnose setup and validate git/state").a
1230
1455
  ok: info ? await isServerResponsive(info) : false,
1231
1456
  detail: info ? serverUrl(info) : "not started"
1232
1457
  });
1233
- checks.push({
1234
- name: "@pierre/diffs license",
1235
- ok: true,
1236
- detail: "apache-2.0 dependency present"
1237
- });
1238
1458
  if (globals.json) {
1239
1459
  printJson({ checks });
1240
1460
  } else {