@jpkme/diff-review 0.1.15 → 0.1.17

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.
Files changed (164) hide show
  1. package/README.md +22 -14
  2. package/bin/diff-review.mjs +192 -21
  3. package/lib/acp-policy.mjs +10 -2
  4. package/lib/acp-policy.test.mjs +27 -0
  5. package/lib/acp.mjs +86 -10
  6. package/lib/agent-login.mjs +848 -36
  7. package/lib/agent-login.test.mjs +538 -0
  8. package/lib/agents.mjs +0 -39
  9. package/lib/agents.test.mjs +17 -24
  10. package/lib/extract-cache.test.mjs +162 -162
  11. package/lib/git.mjs +25 -9
  12. package/lib/model-label.mjs +20 -4
  13. package/lib/model-label.test.mjs +15 -2
  14. package/lib/output-name.mjs +7 -0
  15. package/lib/prompt.mjs +14 -11
  16. package/lib/prompt.test.mjs +49 -0
  17. package/lib/review-dir.mjs +14 -1
  18. package/lib/review-meta.mjs +11 -3
  19. package/lib/review-meta.test.mjs +19 -2
  20. package/lib/review-prep.mjs +63 -63
  21. package/lib/review.mjs +36 -4
  22. package/lib/review.test.mjs +35 -0
  23. package/lib/validate.mjs +11 -1
  24. package/lib/validate.test.mjs +47 -0
  25. package/lib/wait-output.test.mjs +1 -1
  26. package/package.json +2 -2
  27. package/schema.json +16 -1
  28. package/web/build/client/_app/immutable/assets/0.B04dzd8P.css +1 -0
  29. package/web/build/client/_app/immutable/assets/0.B04dzd8P.css.br +0 -0
  30. package/web/build/client/_app/immutable/assets/0.B04dzd8P.css.gz +0 -0
  31. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css +1 -0
  32. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.br +0 -0
  33. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.gz +0 -0
  34. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css +1 -0
  35. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.br +0 -0
  36. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.gz +0 -0
  37. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css +1 -0
  38. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.br +0 -0
  39. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.gz +0 -0
  40. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js +1 -0
  41. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.br +0 -0
  42. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.gz +0 -0
  43. package/web/build/client/_app/immutable/chunks/B4SiKXUO.js +2 -0
  44. package/web/build/client/_app/immutable/chunks/B4SiKXUO.js.br +0 -0
  45. package/web/build/client/_app/immutable/chunks/B4SiKXUO.js.gz +0 -0
  46. package/web/build/client/_app/immutable/chunks/{D8WOD_eS.js → DA0YnpGR.js} +3 -3
  47. package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.br +0 -0
  48. package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.gz +0 -0
  49. package/web/build/client/_app/immutable/chunks/{BpVtXA-T.js → Dv85Hz5p.js} +1 -1
  50. package/web/build/client/_app/immutable/chunks/Dv85Hz5p.js.br +2 -0
  51. package/web/build/client/_app/immutable/chunks/Dv85Hz5p.js.gz +0 -0
  52. package/web/build/client/_app/immutable/chunks/b-BaDYIc.js +215 -0
  53. package/web/build/client/_app/immutable/chunks/b-BaDYIc.js.br +0 -0
  54. package/web/build/client/_app/immutable/chunks/b-BaDYIc.js.gz +0 -0
  55. package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js +2 -0
  56. package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js.br +0 -0
  57. package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js.gz +0 -0
  58. package/web/build/client/_app/immutable/entry/start.hH87UZsD.js +1 -0
  59. package/web/build/client/_app/immutable/entry/start.hH87UZsD.js.br +0 -0
  60. package/web/build/client/_app/immutable/entry/start.hH87UZsD.js.gz +0 -0
  61. package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js +1 -0
  62. package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js.br +0 -0
  63. package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js.gz +0 -0
  64. package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js +1 -0
  65. package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js.br +2 -0
  66. package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js.gz +0 -0
  67. package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js +12 -0
  68. package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js.br +0 -0
  69. package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js.gz +0 -0
  70. package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js +340 -0
  71. package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js.br +0 -0
  72. package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js.gz +0 -0
  73. package/web/build/client/_app/version.json +1 -1
  74. package/web/build/client/_app/version.json.br +0 -0
  75. package/web/build/client/_app/version.json.gz +0 -0
  76. package/web/build/handler.js +3 -3
  77. package/web/build/index.js +3 -3
  78. package/web/build/server/chunks/0-8NUeB9cB.js +11 -0
  79. package/web/build/server/chunks/{0-DUStDOsr.js.map → 0-8NUeB9cB.js.map} +1 -1
  80. package/web/build/server/chunks/1-BApN-ILJ.js +7 -0
  81. package/web/build/server/chunks/{1-DUj63QTT.js.map → 1-BApN-ILJ.js.map} +1 -1
  82. package/web/build/server/chunks/2-Bq7iMeQx.js +7 -0
  83. package/web/build/server/chunks/2-Bq7iMeQx.js.map +1 -0
  84. package/web/build/server/chunks/3-W0cqofyE.js +7 -0
  85. package/web/build/server/chunks/{3-C7UD6rei.js.map → 3-W0cqofyE.js.map} +1 -1
  86. package/web/build/server/chunks/_server.ts-Bu1fdGSB.js +123 -0
  87. package/web/build/server/chunks/_server.ts-Bu1fdGSB.js.map +1 -0
  88. package/web/build/server/chunks/{_server.ts-4RcoUd0l.js → _server.ts-BuUNechu.js} +5 -4
  89. package/web/build/server/chunks/_server.ts-BuUNechu.js.map +1 -0
  90. package/web/build/server/chunks/{agent-sessions-C_hlZJKN.js → agent-sessions-BArYLTW6.js} +1103 -146
  91. package/web/build/server/chunks/agent-sessions-BArYLTW6.js.map +1 -0
  92. package/web/build/server/chunks/{git-review-Bh1w0gak.js → git-review-BJ_JrFNP.js} +87 -26
  93. package/web/build/server/chunks/git-review-BJ_JrFNP.js.map +1 -0
  94. package/web/build/server/chunks/{handler-e2lEvjVT.js → handler-Df_bJiCq.js} +3 -3
  95. package/web/build/server/chunks/{handler-e2lEvjVT.js.map → handler-Df_bJiCq.js.map} +1 -1
  96. package/web/build/server/chunks/{hooks.server-Um1Sknmz.js → hooks.server-BEl7rVad.js} +4 -3
  97. package/web/build/server/chunks/{hooks.server-Um1Sknmz.js.map → hooks.server-BEl7rVad.js.map} +1 -1
  98. package/web/build/server/chunks/{index.js-C3-YX8SA.js → index.js-Bb0CZLZ_.js} +884 -881
  99. package/web/build/server/chunks/index.js-Bb0CZLZ_.js.map +1 -0
  100. package/web/build/server/chunks/manifest.js-j442duM7.js +65 -0
  101. package/web/build/server/chunks/manifest.js-j442duM7.js.map +1 -0
  102. package/web/build/server/chunks/{remote-1g4ayag-DMiPy1mT.js → remote-1g4ayag-BxKnZQPn.js} +6 -6
  103. package/web/build/server/chunks/remote-1g4ayag-BxKnZQPn.js.map +1 -0
  104. package/web/build/server/chunks/{remote-wh6t6j-RlIVOUe-.js → remote-wh6t6j-B-AHrGaN.js} +58 -12
  105. package/web/build/server/chunks/remote-wh6t6j-B-AHrGaN.js.map +1 -0
  106. package/web/build/server/chunks/{validate-CAQ31C7k.js → validate-CXjTRGnR.js} +67 -21
  107. package/web/build/server/chunks/validate-CXjTRGnR.js.map +1 -0
  108. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css +0 -1
  109. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
  110. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.gz +0 -0
  111. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css +0 -1
  112. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.br +0 -0
  113. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.gz +0 -0
  114. package/web/build/client/_app/immutable/assets/3.Cw1J6YTM.css +0 -1
  115. package/web/build/client/_app/immutable/assets/3.Cw1J6YTM.css.br +0 -0
  116. package/web/build/client/_app/immutable/assets/3.Cw1J6YTM.css.gz +0 -0
  117. package/web/build/client/_app/immutable/assets/storage.TlsiS-AP.css +0 -1
  118. package/web/build/client/_app/immutable/assets/storage.TlsiS-AP.css.br +0 -0
  119. package/web/build/client/_app/immutable/assets/storage.TlsiS-AP.css.gz +0 -0
  120. package/web/build/client/_app/immutable/chunks/BpVtXA-T.js.br +0 -2
  121. package/web/build/client/_app/immutable/chunks/BpVtXA-T.js.gz +0 -0
  122. package/web/build/client/_app/immutable/chunks/CUhrLa7d.js +0 -210
  123. package/web/build/client/_app/immutable/chunks/CUhrLa7d.js.br +0 -0
  124. package/web/build/client/_app/immutable/chunks/CUhrLa7d.js.gz +0 -0
  125. package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.br +0 -0
  126. package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.gz +0 -0
  127. package/web/build/client/_app/immutable/chunks/QvO6K-HK.js +0 -2
  128. package/web/build/client/_app/immutable/chunks/QvO6K-HK.js.br +0 -0
  129. package/web/build/client/_app/immutable/chunks/QvO6K-HK.js.gz +0 -0
  130. package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js +0 -1
  131. package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.br +0 -0
  132. package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.gz +0 -0
  133. package/web/build/client/_app/immutable/entry/app.BjJsHui4.js +0 -2
  134. package/web/build/client/_app/immutable/entry/app.BjJsHui4.js.br +0 -0
  135. package/web/build/client/_app/immutable/entry/app.BjJsHui4.js.gz +0 -0
  136. package/web/build/client/_app/immutable/entry/start.D4unRHC4.js +0 -1
  137. package/web/build/client/_app/immutable/entry/start.D4unRHC4.js.br +0 -0
  138. package/web/build/client/_app/immutable/entry/start.D4unRHC4.js.gz +0 -0
  139. package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js +0 -1
  140. package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.br +0 -0
  141. package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.gz +0 -0
  142. package/web/build/client/_app/immutable/nodes/1.C8JJYlWG.js +0 -1
  143. package/web/build/client/_app/immutable/nodes/1.C8JJYlWG.js.br +0 -1
  144. package/web/build/client/_app/immutable/nodes/1.C8JJYlWG.js.gz +0 -0
  145. package/web/build/client/_app/immutable/nodes/2.CDI0Ipi4.js +0 -10
  146. package/web/build/client/_app/immutable/nodes/2.CDI0Ipi4.js.br +0 -0
  147. package/web/build/client/_app/immutable/nodes/2.CDI0Ipi4.js.gz +0 -0
  148. package/web/build/client/_app/immutable/nodes/3.DVC22CDg.js +0 -171
  149. package/web/build/client/_app/immutable/nodes/3.DVC22CDg.js.br +0 -0
  150. package/web/build/client/_app/immutable/nodes/3.DVC22CDg.js.gz +0 -0
  151. package/web/build/server/chunks/0-DUStDOsr.js +0 -11
  152. package/web/build/server/chunks/1-DUj63QTT.js +0 -7
  153. package/web/build/server/chunks/2-B4PSKe5P.js +0 -7
  154. package/web/build/server/chunks/2-B4PSKe5P.js.map +0 -1
  155. package/web/build/server/chunks/3-C7UD6rei.js +0 -7
  156. package/web/build/server/chunks/_server.ts-4RcoUd0l.js.map +0 -1
  157. package/web/build/server/chunks/agent-sessions-C_hlZJKN.js.map +0 -1
  158. package/web/build/server/chunks/git-review-Bh1w0gak.js.map +0 -1
  159. package/web/build/server/chunks/index.js-C3-YX8SA.js.map +0 -1
  160. package/web/build/server/chunks/manifest.js-CmbRPv0j.js +0 -58
  161. package/web/build/server/chunks/manifest.js-CmbRPv0j.js.map +0 -1
  162. package/web/build/server/chunks/remote-1g4ayag-DMiPy1mT.js.map +0 -1
  163. package/web/build/server/chunks/remote-wh6t6j-RlIVOUe-.js.map +0 -1
  164. package/web/build/server/chunks/validate-CAQ31C7k.js.map +0 -1
@@ -7,12 +7,15 @@ import { decodePayload } from "./json-payload.mjs";
7
7
  /**
8
8
  * `agentSession` = la sesión ACP que generó la review, para seguir conversando con el agente desde la web.
9
9
  * @param {string} filePath
10
- * @param {{ agent?: string, model?: string, agentSession?: { agentId: string, sessionId: string, outputFilename: string } }} provenance
10
+ * `modelLabel` = nombre legible que el agente da al modelo ("Haiku 5.5"): `model` puede ser un alias
11
+ * (`haiku`) que más adelante apunte a otra versión.
12
+ * @param {{ agent?: string, model?: string, modelLabel?: string, agentSession?: { agentId: string, sessionId: string, outputFilename: string } }} provenance
11
13
  * @returns {boolean}
12
14
  */
13
- export function stampReviewProvenance(filePath, { agent, model, agentSession } = {}) {
15
+ export function stampReviewProvenance(filePath, { agent, model, modelLabel, agentSession } = {}) {
14
16
  const agentName = String(agent || "").trim();
15
17
  const modelId = String(model || "").trim();
18
+ const label = String(modelLabel || "").trim();
16
19
  if (!agentName && !modelId && !agentSession) return false;
17
20
 
18
21
  let payload;
@@ -27,7 +30,12 @@ export function stampReviewProvenance(filePath, { agent, model, agentSession } =
27
30
  const meta =
28
31
  current && typeof current === "object" && !Array.isArray(current) ? { ...current } : {};
29
32
  if (agentName) meta.agent = agentName;
30
- if (modelId) meta.model = modelId;
33
+ if (modelId) {
34
+ meta.model = modelId;
35
+ // Un label viejo describiría otro modelo: se reemplaza o se borra junto con `model`.
36
+ if (label && label !== modelId) meta.modelLabel = label;
37
+ else delete meta.modelLabel;
38
+ }
31
39
  if (agentSession) meta.agentSession = { ...agentSession };
32
40
  payload.meta = meta;
33
41
  writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
@@ -22,10 +22,10 @@ test("stampReviewProvenance no pisa el modelo si no hay uno", () => {
22
22
  const file = join(dir, "diff-rev_a_b.json");
23
23
  writeFileSync(file, JSON.stringify({ meta: { model: "sonnet" }, intent: "x" }));
24
24
 
25
- stampReviewProvenance(file, { agent: "Codex" });
25
+ stampReviewProvenance(file, { agent: "Cursor" });
26
26
 
27
27
  const saved = JSON.parse(readFileSync(file, "utf8"));
28
- assert.equal(saved.meta.agent, "Codex");
28
+ assert.equal(saved.meta.agent, "Cursor");
29
29
  assert.equal(saved.meta.model, "sonnet");
30
30
  });
31
31
 
@@ -39,3 +39,20 @@ test("stampReviewProvenance guarda la sesión del agente", () => {
39
39
 
40
40
  assert.deepEqual(JSON.parse(readFileSync(file, "utf8")).meta.agentSession, agentSession);
41
41
  });
42
+
43
+ test("stampReviewProvenance guarda el nombre con versión junto al alias", () => {
44
+ const dir = mkdtempSync(join(tmpdir(), "diff-review-meta-"));
45
+ const file = join(dir, "diff-rev_a_b.json");
46
+ writeFileSync(file, JSON.stringify({ meta: { model: "claude-haiku-5-5" }, intent: "x" }));
47
+
48
+ stampReviewProvenance(file, { agent: "Claude Code", model: "haiku", modelLabel: "Haiku 5.5" });
49
+ let meta = JSON.parse(readFileSync(file, "utf8")).meta;
50
+ assert.equal(meta.model, "haiku");
51
+ assert.equal(meta.modelLabel, "Haiku 5.5");
52
+
53
+ // Otro modelo sin nombre conocido: el label anterior ya no corresponde.
54
+ stampReviewProvenance(file, { model: "claude-opus-5" });
55
+ meta = JSON.parse(readFileSync(file, "utf8")).meta;
56
+ assert.equal(meta.model, "claude-opus-5");
57
+ assert.equal(meta.modelLabel, undefined);
58
+ });
@@ -1,63 +1,63 @@
1
- /**
2
- * Precomputa contexto git para el prompt de review (diff cache + lista de archivos).
3
- */
4
- import { loadReviewConfig } from "./config.mjs";
5
- import { extractBranchDiff } from "./extract.mjs";
6
- import { outputRelPath } from "./output-name.mjs";
7
- import { ensureReviewDir, readDiffCache, writeDiffCache } from "./review-dir.mjs";
8
-
9
- /**
10
- * Cachea el three-dot diff y arma las opciones de `buildPrompt` (tips, files, ignores).
11
- * @param {{
12
- * repo: string,
13
- * branch: string,
14
- * base?: string,
15
- * outputFilename: string,
16
- * acp?: boolean,
17
- * }} input
18
- */
19
- export function prepareReviewPromptOpts(input) {
20
- const repo = String(input.repo || "").trim();
21
- const branch = String(input.branch || "").trim();
22
- const base = String(input.base || "develop").trim() || "develop";
23
- const outputFilename = String(input.outputFilename || "").trim();
24
- if (!repo || !branch || !outputFilename) {
25
- throw new Error("prepareReviewPromptOpts requiere repo, branch y outputFilename.");
26
- }
27
-
28
- ensureReviewDir(repo);
29
- writeDiffCache(repo, outputFilename, { branch, base });
30
- const cached = readDiffCache(repo, outputFilename);
31
-
32
- let extracted;
33
- try {
34
- extracted = extractBranchDiff({
35
- repo,
36
- branch,
37
- base,
38
- ...(cached ? { cachedDiff: cached } : {}),
39
- });
40
- } catch {
41
- extracted = extractBranchDiff({ repo, branch, base });
42
- }
43
-
44
- const config = loadReviewConfig(repo);
45
- const diffRelPath = cached
46
- ? outputRelPath(outputFilename).replace(/\.json$/i, ".diff")
47
- : null;
48
-
49
- return {
50
- outputFilename,
51
- acp: Boolean(input.acp),
52
- diffRelPath,
53
- mergeBase: extracted.mergeBase || undefined,
54
- branchSha: extracted.branchSha || undefined,
55
- baseSha: extracted.baseSha || undefined,
56
- files: (extracted.files || []).map((f) => ({
57
- path: f.path,
58
- changeType: f.changeType,
59
- })),
60
- ignored: extracted.context?.ignored || [],
61
- ignoreGlobs: config.ignore,
62
- };
63
- }
1
+ /**
2
+ * Precomputa contexto git para el prompt de review (diff cache + lista de archivos).
3
+ */
4
+ import { loadReviewConfig } from "./config.mjs";
5
+ import { extractBranchDiff } from "./extract.mjs";
6
+ import { outputRelPath } from "./output-name.mjs";
7
+ import { ensureReviewDir, readDiffCache, writeDiffCache } from "./review-dir.mjs";
8
+
9
+ /**
10
+ * Cachea el three-dot diff y arma las opciones de `buildPrompt` (tips, files, ignores).
11
+ * @param {{
12
+ * repo: string,
13
+ * branch: string,
14
+ * base?: string,
15
+ * outputFilename: string,
16
+ * acp?: boolean,
17
+ * }} input
18
+ */
19
+ export function prepareReviewPromptOpts(input) {
20
+ const repo = String(input.repo || "").trim();
21
+ const branch = String(input.branch || "").trim();
22
+ const base = String(input.base || "develop").trim() || "develop";
23
+ const outputFilename = String(input.outputFilename || "").trim();
24
+ if (!repo || !branch || !outputFilename) {
25
+ throw new Error("prepareReviewPromptOpts requiere repo, branch y outputFilename.");
26
+ }
27
+
28
+ ensureReviewDir(repo);
29
+ writeDiffCache(repo, outputFilename, { branch, base });
30
+ const cached = readDiffCache(repo, outputFilename);
31
+
32
+ let extracted;
33
+ try {
34
+ extracted = extractBranchDiff({
35
+ repo,
36
+ branch,
37
+ base,
38
+ ...(cached ? { cachedDiff: cached } : {}),
39
+ });
40
+ } catch {
41
+ extracted = extractBranchDiff({ repo, branch, base });
42
+ }
43
+
44
+ const config = loadReviewConfig(repo);
45
+ const diffRelPath = cached
46
+ ? outputRelPath(outputFilename).replace(/\.json$/i, ".diff")
47
+ : null;
48
+
49
+ return {
50
+ outputFilename,
51
+ acp: Boolean(input.acp),
52
+ diffRelPath,
53
+ mergeBase: extracted.mergeBase || undefined,
54
+ branchSha: extracted.branchSha || undefined,
55
+ baseSha: extracted.baseSha || undefined,
56
+ files: (extracted.files || []).map((f) => ({
57
+ path: f.path,
58
+ changeType: f.changeType,
59
+ })),
60
+ ignored: extracted.context?.ignored || [],
61
+ ignoreGlobs: config.ignore,
62
+ };
63
+ }
package/lib/review.mjs CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  BLOCK_OPS,
3
3
  FINDING_CLASSES,
4
4
  FINDING_KINDS,
5
+ FINDING_RESOLUTION_KINDS,
5
6
  FINDING_SEVERITIES,
6
7
  GROUP_KINDS,
7
8
  SKIP_REASONS,
@@ -111,6 +112,37 @@ function findingKey(finding) {
111
112
  return [finding.file, finding.line ?? "", finding.kind, finding.what].join("|");
112
113
  }
113
114
 
115
+ /** Triage UI (`ignored` | `fix` | `corrected`); se preserva al rehidratar desde disco. */
116
+ function normalizeResolution(raw) {
117
+ const kind = String(raw?.kind ?? "")
118
+ .trim()
119
+ .toLowerCase();
120
+ if (!FINDING_RESOLUTION_KINDS.includes(kind)) return undefined;
121
+ const detail = String(raw?.detail ?? "").trim();
122
+ return detail ? { kind, detail } : { kind };
123
+ }
124
+
125
+ function assignFindingIds(findings) {
126
+ const used = new Set();
127
+ for (const finding of findings) {
128
+ const id = String(finding.id || "").trim();
129
+ if (id && !used.has(id)) {
130
+ finding.id = id;
131
+ used.add(id);
132
+ } else {
133
+ finding.id = "";
134
+ }
135
+ }
136
+ let n = 1;
137
+ for (const finding of findings) {
138
+ if (finding.id) continue;
139
+ while (used.has(`f${n}`)) n += 1;
140
+ finding.id = `f${n}`;
141
+ used.add(finding.id);
142
+ n += 1;
143
+ }
144
+ }
145
+
114
146
  function normalizeFindings(raw, { blockIds, knownPaths }) {
115
147
  const seen = new Set();
116
148
  const findings = [];
@@ -123,8 +155,9 @@ function normalizeFindings(raw, { blockIds, knownPaths }) {
123
155
  const severity = oneOf(item.severity, FINDING_SEVERITIES, "med");
124
156
  const block = String(item?.block || "").trim();
125
157
  const line = Number.isFinite(Number(item?.line)) ? Number(item.line) : null;
158
+ const resolution = normalizeResolution(item?.resolution);
126
159
  const finding = {
127
- id: "",
160
+ id: String(item?.id || "").trim(),
128
161
  class: cls,
129
162
  severity,
130
163
  // Una mejora de calidad nunca frena un merge, diga lo que diga el modelo;
@@ -136,6 +169,7 @@ function normalizeFindings(raw, { blockIds, knownPaths }) {
136
169
  block: blockIds.has(block) ? block : "",
137
170
  what,
138
171
  fix: text(item.fix || item.detail, MAX.finding),
172
+ ...(resolution ? { resolution } : {}),
139
173
  };
140
174
  const key = findingKey(finding);
141
175
  if (seen.has(key)) continue;
@@ -150,9 +184,7 @@ function normalizeFindings(raw, { blockIds, knownPaths }) {
150
184
  rank[a.severity] - rank[b.severity] ||
151
185
  a.file.localeCompare(b.file),
152
186
  );
153
- findings.forEach((finding, i) => {
154
- finding.id = `f${i + 1}`;
155
- });
187
+ assignFindingIds(findings);
156
188
  return findings;
157
189
  }
158
190
 
@@ -130,6 +130,41 @@ test("buildReview deduplica hallazgos repetidos y limpia referencias colgadas",
130
130
  assert.equal(doc.findings[0].block, "");
131
131
  });
132
132
 
133
+ test("buildReview preserva resolution e ids al rehidratar triage desde disco", () => {
134
+ const withFindings = payload();
135
+ withFindings.findings = [
136
+ {
137
+ id: "f2",
138
+ class: "quality",
139
+ severity: "low",
140
+ kind: "docs",
141
+ file: "src/a.ts",
142
+ line: 3,
143
+ what: "comentario viejo",
144
+ fix: "actualizar",
145
+ resolution: { kind: "ignored" },
146
+ },
147
+ {
148
+ id: "f1",
149
+ class: "risk",
150
+ severity: "med",
151
+ kind: "bug",
152
+ file: "src/a.ts",
153
+ line: 22,
154
+ block: "b1",
155
+ what: "null sin chequear",
156
+ fix: "guardar",
157
+ resolution: { kind: "corrected", detail: "null-check en parse" },
158
+ },
159
+ ];
160
+ const doc = buildReview({ skeleton: skeleton(), payload: withFindings });
161
+ const byId = Object.fromEntries(doc.findings.map((f) => [f.id, f]));
162
+ assert.equal(doc.findings.length, 2);
163
+ assert.deepEqual(byId.f1.resolution, { kind: "corrected", detail: "null-check en parse" });
164
+ assert.deepEqual(byId.f2.resolution, { kind: "ignored" });
165
+ assert.equal(byId.f1.what, "null sin chequear");
166
+ });
167
+
133
168
  test("una respuesta JSON del modelo atraviesa decode, validación y normalización", () => {
134
169
  const answer = JSON.stringify({
135
170
  meta: { agent: "Claude Code", model: "claude-opus" },
package/lib/validate.mjs CHANGED
@@ -8,6 +8,7 @@ export const FINDING_KINDS = [
8
8
  "missing-test", "perf", "maintainability", "docs", "other",
9
9
  ];
10
10
  export const SKIP_REASONS = ["generated", "lockfile", "format", "vendored", "binary", "trivial", "ignored"];
11
+ export const FINDING_RESOLUTION_KINDS = ["ignored", "fix", "corrected"];
11
12
 
12
13
  /** Lo que tiene que devolver el modelo en un turno. El resto lo agrega el pipeline. */
13
14
  export const REVIEW_SCHEMA = {
@@ -77,6 +78,15 @@ export const REVIEW_SCHEMA = {
77
78
  block: { type: "string" },
78
79
  what: { type: "string", minLength: 1 },
79
80
  fix: { type: "string" },
81
+ resolution: {
82
+ type: "object",
83
+ required: ["kind"],
84
+ additionalProperties: true,
85
+ properties: {
86
+ kind: { type: "string", enum: FINDING_RESOLUTION_KINDS },
87
+ detail: { type: "string" },
88
+ },
89
+ },
80
90
  },
81
91
  },
82
92
  },
@@ -155,7 +165,7 @@ export function reviewIssues(payload, knownPaths = []) {
155
165
  const agentName = typeof meta.agent === "string" ? meta.agent.trim() : "";
156
166
  const modelId = typeof meta.model === "string" ? meta.model.trim() : "";
157
167
  if (!agentName || agentName.startsWith("<")) {
158
- errors.push("meta.agent es obligatorio: nombre del producto (Claude Code, Cursor, Codex, …), sin placeholder");
168
+ errors.push("meta.agent es obligatorio: nombre del producto (Claude Code, Cursor, Copilot, …), sin placeholder");
159
169
  }
160
170
  if (!modelId || modelId.startsWith("<")) {
161
171
  errors.push("meta.model es obligatorio: id exacto del modelo, sin placeholder");
@@ -68,6 +68,53 @@ test("validateAgainst acepta los dos ejes de hallazgo", () => {
68
68
  assert.deepEqual(validateAgainst(REVIEW_SCHEMA, doc), []);
69
69
  });
70
70
 
71
+ test("validateAgainst acepta resolution de triage en hallazgos", () => {
72
+ const doc = valid();
73
+ doc.findings = [
74
+ {
75
+ id: "f1",
76
+ class: "risk",
77
+ severity: "high",
78
+ blocking: true,
79
+ kind: "bug",
80
+ file: "src/a.ts",
81
+ line: 4,
82
+ what: "rompe",
83
+ fix: "arreglar",
84
+ resolution: { kind: "corrected", detail: "null-check en parse" },
85
+ },
86
+ {
87
+ id: "f2",
88
+ class: "quality",
89
+ severity: "low",
90
+ blocking: false,
91
+ kind: "maintainability",
92
+ file: "src/a.ts",
93
+ line: null,
94
+ what: "duplicado",
95
+ resolution: { kind: "ignored" },
96
+ },
97
+ ];
98
+ assert.deepEqual(validateAgainst(REVIEW_SCHEMA, doc), []);
99
+ });
100
+
101
+ test("validateAgainst rechaza resolution.kind inválido", () => {
102
+ const doc = valid();
103
+ doc.findings = [
104
+ {
105
+ id: "f1",
106
+ class: "risk",
107
+ severity: "high",
108
+ blocking: true,
109
+ kind: "bug",
110
+ file: "src/a.ts",
111
+ what: "x",
112
+ resolution: { kind: "done" },
113
+ },
114
+ ];
115
+ assert.ok(validateAgainst(REVIEW_SCHEMA, doc).some((e) => e.includes("kind")));
116
+ });
117
+
71
118
  test("reviewIssues detecta un bloque que apunta a un grupo inexistente", () => {
72
119
  const doc = valid();
73
120
  doc.blocks[0].group = "g9";
@@ -6,7 +6,7 @@ import { describe, it } from "node:test";
6
6
  import { existingReviewOutput, waitForReviewOutput } from "./wait-output.mjs";
7
7
 
8
8
  const valid = () => ({
9
- meta: { agent: "Codex", model: "gpt-5" },
9
+ meta: { agent: "Claude Code", model: "opus" },
10
10
  intent: "Agrega el ABM de depósitos locales end-to-end para dejar de administrarlos por SQL.",
11
11
  groups: [{ id: "g1", kind: "feat", title: "feat(product): CRUD de depósitos", intent: "base de todo" }],
12
12
  blocks: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpkme/diff-review",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Local UI to prompt a coding agent for a branch review and browse the result.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "LICENSE"
16
16
  ],
17
17
  "scripts": {
18
- "test": "node --test lib/*.test.mjs",
18
+ "test": "node --test lib/*.test.mjs && node --experimental-strip-types --test web/src/lib/report/triage.test.ts",
19
19
  "web:install": "npm --prefix web install",
20
20
  "web:dev": "npm --prefix web run dev",
21
21
  "web:build": "npm --prefix web run build",
package/schema.json CHANGED
@@ -123,7 +123,22 @@
123
123
  "line": { "type": ["integer", "null"], "description": "Línea de la post-imagen (columna derecha / @@ +N)." },
124
124
  "block": { "type": ["string", "null"], "description": "Id del bloque, o vacío/null si es transversal." },
125
125
  "what": { "type": "string" },
126
- "fix": { "type": "string", "description": "Acción concreta." }
126
+ "fix": { "type": "string", "description": "Acción concreta." },
127
+ "resolution": {
128
+ "type": "object",
129
+ "description": "Triage de la UI (Ignorar / Corregir / Corregido). Lo escribe el viewer; el agente debe preservarlo al editar el JSON.",
130
+ "required": ["kind"],
131
+ "properties": {
132
+ "kind": {
133
+ "type": "string",
134
+ "enum": ["ignored", "fix", "corrected"]
135
+ },
136
+ "detail": {
137
+ "type": "string",
138
+ "description": "Resumen de la corrección aplicada (solo con kind=corrected)."
139
+ }
140
+ }
141
+ }
127
142
  }
128
143
  }
129
144
  },
@@ -0,0 +1 @@
1
+ :root{--bg:#0b0e14;--bg-elev:#11151d;--bg-card:#151a24;--bg-card-hover:#1a2130;--border:#242b3a;--border-soft:#1c2230;--text:#e6e9ef;--text-dim:#9aa4b8;--text-faint:#7a8499;--accent:#6ea8fe;--accent-soft:#1c2b45;--ok:#7ee2a8;--ok-bg:#0d2a1c;--warn:#e2c67e;--danger:#ff9a9a;--danger-bg:#2a1414;--danger-border:#4a1e1e;--radius:3px;--mono:ui-monospace, "JetBrains Mono", "Fira Code", Consolas, "SFMono-Regular", Menlo, monospace;--sans:-apple-system, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;--btn-primary-bg:#1c2b45;--btn-primary-border:#3a5a8f;--btn-primary-text:#cfe0ff;--kind-feat:#7ee2a8;--kind-fix:#ff9a9a;--kind-refactor:#c9a5ff;--kind-perf:#e2c67e;--kind-test:#6ea8fe;--kind-chore:#9aa4b8;--kind-docs:#7fd6e0;--kind-infra:#f0a868;--sev-high:#ff9a9a;--sev-med:#f08a5a;--sev-low:#c4b86a;--sev-nit:#7f8aa3;--diff-add-bg:#0f2419;--diff-add-fg:#7ee2a8;--diff-del-bg:#2a1418;--diff-del-fg:#ff9a9a;--diff-hunk-bg:#141924;--diff-gutter:#46516a;--diff-mark:#2b3f63;--diff-mark-edge:#6ea8fe;--diff-add-strong:#1f6b3c;--diff-del-strong:#6d2530;--diff-blank-bg:repeating-linear-gradient(135deg, #11151d 0 6px, #151a24 6px 12px);--fix-bg:#0e1a13;--fix-border:#2f6b46;--fix-text:#7ee2a8;--publish-bg:#0e1622;--publish-border:#3a5a8f;--publish-text:#7db8ff;--scrollbar-thumb:#2a3348;--scrollbar-thumb-hover:#3a4660;--selection-bg:#33507a;--header-bg:#0b0e14eb}:root[data-theme=light]{--bg:#fff;--bg-elev:#f6f7f9;--bg-card:#eef0f3;--bg-card-hover:#e4e7ec;--border:#d7dbe2;--border-soft:#e4e7ec;--text:#16181d;--text-dim:#4b5563;--text-faint:#5b6472;--accent:#2f6fe0;--accent-soft:#dce8ff;--ok:#1f9d55;--ok-bg:#e3f8ea;--warn:#a5720a;--danger:#c92a2a;--danger-bg:#fbe4e4;--danger-border:#f3b4b4;--btn-primary-bg:#2f6fe0;--btn-primary-border:#2358b8;--btn-primary-text:#fff;--kind-feat:#1f9d55;--kind-fix:#c92a2a;--kind-refactor:#7c4dd6;--kind-perf:#a5720a;--kind-test:#2f6fe0;--kind-chore:#5b6472;--kind-docs:#0f8fa3;--kind-infra:#c1690f;--sev-high:#c92a2a;--sev-med:#c44d1a;--sev-low:#8b7d2e;--sev-nit:#6b7280;--diff-add-bg:#e6f7ec;--diff-add-fg:#1f9d55;--diff-del-bg:#fbe9e9;--diff-del-fg:#c92a2a;--diff-hunk-bg:#eef1f5;--diff-gutter:#9aa3b2;--diff-mark:#cfe0ff;--diff-mark-edge:#2f6fe0;--diff-add-strong:#a7ebbd;--diff-del-strong:#f9b8b8;--diff-blank-bg:repeating-linear-gradient(135deg, #f6f7f9 0 6px, #eef0f3 6px 12px);--fix-bg:#e6f7ec;--fix-border:#1f9d55;--fix-text:#157a41;--publish-bg:#e8f0fe;--publish-border:#2f6fe0;--publish-text:#1d4fa8;--scrollbar-thumb:#c7cbd4;--scrollbar-thumb-hover:#aab0bb;--selection-bg:#cfe0ff;--header-bg:#ffffffeb}.code .chg{border-radius:2px}.code.del .chg{background:var(--diff-del-strong)}.code.add .chg{background:var(--diff-add-strong)}.hljs-comment,.hljs-quote{color:#6b7689;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-literal,.hljs-doctag,.hljs-formula{color:#c9a5ff}.hljs-string,.hljs-regexp,.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string{color:#a5d6a7}.hljs-number,.hljs-variable,.hljs-template-variable,.hljs-type,.hljs-selector-class,.hljs-selector-attr,.hljs-selector-pseudo{color:#f0a868}.hljs-title,.hljs-section,.hljs-title.class_,.hljs-title.function_{color:#7fd6e0}.hljs-attr,.hljs-property,.hljs-symbol,.hljs-bullet,.hljs-link{color:#6ea8fe}.hljs-built_in,.hljs-class,.hljs-params{color:#e6e9ef}.hljs-meta,.hljs-tag{color:#9aa4b8}:root[data-theme=light] .hljs-comment,:root[data-theme=light] .hljs-quote{color:#6b7280}:root[data-theme=light] .hljs-keyword,:root[data-theme=light] .hljs-selector-tag,:root[data-theme=light] .hljs-literal,:root[data-theme=light] .hljs-doctag,:root[data-theme=light] .hljs-formula{color:#7c4dd6}:root[data-theme=light] .hljs-string,:root[data-theme=light] .hljs-regexp,:root[data-theme=light] .hljs-addition,:root[data-theme=light] .hljs-attribute,:root[data-theme=light] .hljs-meta .hljs-string{color:#1f9d55}:root[data-theme=light] .hljs-number,:root[data-theme=light] .hljs-variable,:root[data-theme=light] .hljs-template-variable,:root[data-theme=light] .hljs-type,:root[data-theme=light] .hljs-selector-class,:root[data-theme=light] .hljs-selector-attr,:root[data-theme=light] .hljs-selector-pseudo{color:#b5650f}:root[data-theme=light] .hljs-title,:root[data-theme=light] .hljs-section,:root[data-theme=light] .hljs-title.class_,:root[data-theme=light] .hljs-title.function_{color:#0f8fa3}:root[data-theme=light] .hljs-attr,:root[data-theme=light] .hljs-property,:root[data-theme=light] .hljs-symbol,:root[data-theme=light] .hljs-bullet,:root[data-theme=light] .hljs-link{color:#2f6fe0}:root[data-theme=light] .hljs-built_in,:root[data-theme=light] .hljs-class,:root[data-theme=light] .hljs-params{color:#16181d}:root[data-theme=light] .hljs-meta,:root[data-theme=light] .hljs-tag{color:#5b6472}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.anchor-flash{animation:1.4s ease-out anchor-flash}@keyframes anchor-flash{0%{box-shadow:0 0 0 2px var(--accent)}to{box-shadow:0 0 0 2px #0000}}*{box-sizing:border-box}html,body{background:var(--bg);color:var(--text);font-family:var(--sans);min-height:100%;margin:0;padding:0}body{-webkit-font-smoothing:antialiased}button,input,select{font:inherit;color:inherit}button{cursor:pointer}::selection{background:var(--selection-bg)}html,body,*{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) transparent}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);background-clip:padding-box;border:2px solid #0000;border-radius:999px}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover);background-clip:padding-box;border:2px solid #0000}::-webkit-scrollbar-corner{background:0 0}.app-header{--app-header-h:52px;min-height:var(--app-header-h);box-sizing:border-box;border-bottom:1px solid var(--border-soft);background:var(--header-bg);flex-shrink:0;justify-content:space-between;align-items:center;gap:12px 16px;min-width:0;padding:0 16px 0 12px;display:flex}.app-brand{border-radius:var(--radius);letter-spacing:-.01em;color:var(--text);white-space:nowrap;flex-shrink:0;align-items:center;gap:7px;padding:4px 6px;font-size:14px;font-weight:600;line-height:1;text-decoration:none;display:flex}a.app-brand:hover{background:color-mix(in srgb, var(--text) 7%, transparent)}a.app-brand:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.boot.svelte-12qhfyh{color:var(--text-dim);font-family:var(--mono);padding:48px 28px;font-size:13px}.boot.svelte-12qhfyh button:where(.svelte-12qhfyh){background:var(--btn-primary-bg);border:1px solid var(--btn-primary-border);color:var(--btn-primary-text);border-radius:var(--radius);margin-top:12px;padding:8px 14px}
@@ -0,0 +1 @@
1
+ .live.svelte-18urbyr{background:radial-gradient(900px 420px at 8% -8%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 70%), var(--bg);flex-direction:column;flex:1;min-height:0;display:flex}.brand.svelte-18urbyr{align-items:center;gap:8px;min-width:0;display:flex}.pill.svelte-18urbyr{letter-spacing:.02em;border-radius:var(--radius);border:1px solid var(--border);color:var(--text-dim);background:var(--bg-card);text-transform:lowercase;padding:3px 9px;font-size:11px;font-weight:500}.pill.busy.svelte-18urbyr{border-color:color-mix(in srgb, var(--accent) 45%, var(--border));color:var(--accent);background:var(--accent-soft)}.pill.error.svelte-18urbyr{border-color:var(--danger-border,var(--danger));color:var(--danger);background:var(--danger-bg,transparent)}.pill.gone.svelte-18urbyr{color:var(--text-faint)}.actions.svelte-18urbyr{flex:none;align-items:center;gap:8px;display:flex}.ghost.svelte-18urbyr{border:1px solid var(--border);border-radius:var(--radius);color:var(--text);font:inherit;cursor:pointer;background:0 0;padding:6px 11px;font-size:13px}.ghost.svelte-18urbyr:hover{border-color:var(--accent)}.body.svelte-18urbyr{flex:1;grid-template-columns:minmax(280px,320px) 1fr;gap:20px;min-height:0;padding:20px 24px 24px;display:grid}.card.svelte-18urbyr{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-elev);align-self:start;padding:16px 16px 14px}.fields.svelte-18urbyr{flex-direction:column;gap:14px;margin:0;display:flex}.fields.svelte-18urbyr dt:where(.svelte-18urbyr){text-transform:uppercase;letter-spacing:.04em;color:var(--text-faint);margin:0 0 3px;font-size:11px}.fields.svelte-18urbyr dd:where(.svelte-18urbyr){font-size:13px;font-family:var(--mono);text-overflow:ellipsis;white-space:nowrap;color:var(--text);margin:0;overflow:hidden}.fields.svelte-18urbyr dd.account-dd:where(.svelte-18urbyr){white-space:normal;text-overflow:unset;font-family:inherit;overflow:visible}.fields.svelte-18urbyr dd.branch-base:where(.svelte-18urbyr){white-space:normal;text-overflow:unset;overflow-wrap:anywhere;line-height:1.35;overflow:visible}.branch.svelte-18urbyr{color:var(--accent);font-weight:600}.arrow.svelte-18urbyr{color:var(--text-faint);margin:0 2px}.base.svelte-18urbyr{color:var(--text)}.card-note.svelte-18urbyr{border-top:1px solid var(--border-soft,var(--border));color:var(--text-faint);margin:16px 0 0;padding-top:12px;font-size:12.5px;line-height:1.4}.stage.svelte-18urbyr{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-elev);flex-direction:column;min-width:0;min-height:0;display:flex;overflow:hidden}.center.svelte-18urbyr{text-align:center;flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:40px 24px;display:flex}.center.fail.svelte-18urbyr .title:where(.svelte-18urbyr){color:var(--danger)}.auth-actions.svelte-18urbyr{flex-wrap:wrap;justify-content:center;gap:8px;margin-top:12px;display:flex}.primary.svelte-18urbyr{border:1px solid color-mix(in srgb, var(--accent) 55%, var(--border));border-radius:var(--radius);background:var(--accent-soft);color:var(--accent);font:inherit;cursor:pointer;padding:7px 14px;font-size:13px;font-weight:600}.primary.svelte-18urbyr:hover:not(:disabled){border-color:var(--accent)}.primary.svelte-18urbyr:disabled{opacity:.6;cursor:default}.auth-note.svelte-18urbyr{max-width:28rem;color:var(--text-dim);margin:10px 0 0;font-size:13px}.copilot-host-pick.svelte-18urbyr{flex-direction:column;align-items:center;gap:4px;width:min(100%,28rem);display:flex}.copilot-host-field.svelte-18urbyr{width:100%;color:var(--text-dim);text-align:left;flex-direction:column;gap:4px;margin-top:8px;font-size:12px;display:flex}.copilot-host-field.svelte-18urbyr input:where(.svelte-18urbyr){border:1px solid var(--border);border-radius:var(--radius);background:var(--bg);color:var(--text);font:inherit;padding:7px 10px;font-size:13px}.auth-err.svelte-18urbyr{max-width:28rem;color:var(--danger);margin:8px 0 0;font-size:13px}.spinner.svelte-18urbyr{border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;width:34px;height:34px;margin-bottom:6px;animation:.8s linear infinite svelte-18urbyr-spin}@keyframes svelte-18urbyr-spin{to{transform:rotate(360deg)}}.title.svelte-18urbyr{margin:0;font-size:17px;font-weight:600}.detail.svelte-18urbyr{color:var(--text-dim);max-width:28rem;margin:0;font-size:14px}.hint.svelte-18urbyr{max-width:32rem;color:var(--text-faint);margin:6px 0 0;font-size:13px}.stage-header.svelte-18urbyr{border-bottom:1px solid var(--border);border-radius:0;flex:none;align-items:center;gap:8px;min-width:0;padding:10px 14px;display:flex}.stage-header.svelte-18urbyr strong:where(.svelte-18urbyr){font-size:13.5px;font-weight:650}.stage-header.svelte-18urbyr small:where(.svelte-18urbyr){font-family:var(--mono);color:var(--text-faint);text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:11.5px;overflow:hidden}.log-wrap.svelte-18urbyr{border-radius:0;flex-direction:column;flex:1;gap:10px;min-height:0;padding:12px 14px;display:flex}.log-wrap.svelte-18urbyr .log{flex:1;min-height:0}.stage-footer.svelte-18urbyr{border-top:1px solid var(--border);border-radius:0;flex-wrap:wrap;flex:none;align-items:center;gap:10px;min-width:0;margin-top:auto;padding-top:10px;display:flex}.stage-footer.svelte-18urbyr .trust{flex:0 auto;min-width:0;max-width:100%}.footer-spacer.svelte-18urbyr{flex:8px;min-width:8px}.stage-footer.svelte-18urbyr .perm-actions{flex:none}.launch-banner.svelte-18urbyr{border:1px solid var(--border);border-radius:var(--radius);background:color-mix(in srgb, var(--accent-soft) 70%, var(--bg-card));align-items:flex-start;gap:12px;padding:10px 12px;display:flex}.launch-banner.svelte-18urbyr .title:where(.svelte-18urbyr){font-size:14px}.launch-banner.svelte-18urbyr .detail:where(.svelte-18urbyr){margin-top:2px;font-size:12.5px}.spinner.sm.svelte-18urbyr{flex:none;width:18px;height:18px;margin:2px 0 0}@media (width<=800px){.body.svelte-18urbyr{grid-template-rows:auto 1fr;grid-template-columns:1fr;gap:14px;padding:16px}.card.svelte-18urbyr{align-self:stretch}.stage.svelte-18urbyr{min-height:280px}}@media (width<=480px){.bar.svelte-18urbyr{flex-wrap:wrap;padding-block:8px}.pill.svelte-18urbyr{font-size:10px}}.page.svelte-1uha8ag{flex-direction:column;min-height:100vh;display:flex}.page.live.svelte-1uha8ag{height:100vh;min-height:100vh;overflow:hidden}.diff-backdrop.svelte-1uha8ag{z-index:80;background:color-mix(in srgb, var(--bg) 55%, transparent);-webkit-backdrop-filter:blur(10px);place-items:center;padding:24px;animation:.12s ease-out svelte-1uha8ag-diff-fade;display:grid;position:fixed;inset:0}@keyframes svelte-1uha8ag-diff-fade{0%{opacity:0}}.diff-modal.svelte-1uha8ag{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);text-align:center;width:min(380px,100%);padding:28px 24px;box-shadow:0 16px 48px #00000047}.confirm-dialog.svelte-1uha8ag{z-index:81;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);text-align:left;outline:none;width:min(420px,100vw - 32px);padding:18px 20px;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);box-shadow:0 16px 48px #00000047}.confirm-dialog.svelte-1uha8ag h2:where(.svelte-1uha8ag){color:var(--text);margin:0 0 10px;font-size:15px;font-weight:650}.confirm-dialog.svelte-1uha8ag p:where(.svelte-1uha8ag){color:var(--text-dim);margin:0;font-size:13px;line-height:1.45}.confirm-dialog.svelte-1uha8ag code:where(.svelte-1uha8ag){color:var(--text);font-size:12px}.confirm-actions.svelte-1uha8ag{justify-content:flex-end;gap:8px;margin-top:16px;display:flex}.confirm-actions.svelte-1uha8ag button:where(.svelte-1uha8ag){background:var(--bg-elev);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);cursor:pointer;padding:7px 12px;font-size:13px}.confirm-actions.svelte-1uha8ag button.primary:where(.svelte-1uha8ag){background:var(--btn-primary-bg);border-color:var(--btn-primary-border);color:var(--btn-primary-text)}.diff-modal.svelte-1uha8ag p:where(.svelte-1uha8ag){color:var(--text);margin:14px 0 0;font-size:15px}.diff-modal-file.svelte-1uha8ag{text-overflow:ellipsis;white-space:nowrap;color:var(--text-faint);margin-top:6px;font-size:12px;display:block;overflow:hidden}.diff-spinner.svelte-1uha8ag{border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;width:28px;height:28px;margin:0 auto;animation:.7s linear infinite svelte-1uha8ag-diff-spin}.cli-wait.svelte-1uha8ag{text-align:center;background:var(--bg);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:12px;min-height:0;padding:32px 24px;display:flex}.cli-wait-title.svelte-1uha8ag{color:var(--text);margin:0;font-size:15px}.cli-wait-detail.svelte-1uha8ag{color:var(--text-faint);margin:0;font-size:13px}.cli-wait.svelte-1uha8ag .ghost:where(.svelte-1uha8ag){border:1px solid var(--border);border-radius:var(--radius);color:var(--text-dim);cursor:pointer;background:0 0;margin-top:8px;padding:6px 12px;font-size:13px}@keyframes svelte-1uha8ag-diff-spin{to{transform:rotate(360deg)}}.toast.svelte-1uha8ag{z-index:50;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-card);color:var(--text);padding:10px 16px;font-size:13px;position:fixed;top:16px;left:50%;transform:translate(-50%);box-shadow:0 8px 24px #00000029}.topbar.svelte-1uha8ag{z-index:5;position:sticky;top:0}.workspace.svelte-1uha8ag{flex:1;grid-template-columns:minmax(0,1fr) minmax(300px,380px);align-items:start;gap:28px;width:100%;max-width:1280px;margin:0 auto;padding:22px 28px 40px;display:grid;overflow:auto}.setup.svelte-1uha8ag{min-width:0}h2.svelte-1uha8ag{letter-spacing:.06em;text-transform:uppercase;color:var(--text-faint);margin:0 0 4px;font-size:13px;font-weight:600}.result-head.svelte-1uha8ag{margin-top:24px}.section-head.svelte-1uha8ag{justify-content:space-between;align-items:baseline;margin-top:22px;display:flex}.hint-text.svelte-1uha8ag{color:var(--text-dim);margin:0 0 12px;font-size:12.5px;line-height:1.5}.hint-text.svelte-1uha8ag code:where(.svelte-1uha8ag){font-family:var(--mono);color:var(--text);font-size:12px}.detect-wait.svelte-1uha8ag{color:var(--text-dim);margin:0 0 10px;font-size:13px}button.detect-open.svelte-1uha8ag{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-card);width:100%;color:var(--text);font:inherit;text-align:left;cursor:pointer;align-items:center;gap:12px;margin:0 0 12px;padding:12px 14px;transition:border-color .12s,background .12s;display:flex}button.detect-open.ready.svelte-1uha8ag{border-color:color-mix(in srgb, var(--accent) 55%, var(--border));background:var(--accent-soft)}button.detect-open.svelte-1uha8ag:hover{border-color:var(--accent)}button.detect-open.svelte-1uha8ag:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.detect-open-text.svelte-1uha8ag{flex-direction:column;flex:1;gap:3px;min-width:0;display:flex}.detect-open-label.svelte-1uha8ag{font-size:13px;font-weight:600}.detect-open-label.svelte-1uha8ag small:where(.svelte-1uha8ag){color:var(--text-dim);font-weight:400}.detect-open-text.svelte-1uha8ag code:where(.svelte-1uha8ag),.detect-open-meta.svelte-1uha8ag{text-overflow:ellipsis;white-space:nowrap;color:var(--text-dim);font-size:12px;overflow:hidden}.detect-open-cta.svelte-1uha8ag{color:var(--accent);font-size:13px;font-weight:600}.detect-open-chevron.svelte-1uha8ag{color:var(--accent);flex-shrink:0;transition:transform .12s}button.detect-open.svelte-1uha8ag:hover .detect-open-chevron:where(.svelte-1uha8ag){transform:translate(2px)}.error-line.svelte-1uha8ag{color:var(--danger);margin:0 0 10px;font-size:12px}.ok-line.svelte-1uha8ag{color:var(--ok);margin:0 0 10px;font-size:12px}.row.svelte-1uha8ag{flex-wrap:wrap;gap:8px;display:flex}.repo-setup.svelte-1uha8ag{flex-direction:column;gap:10px;display:flex}.field.svelte-1uha8ag{flex-direction:column;flex:1;gap:4px;min-width:180px;display:flex}.repo-field.svelte-1uha8ag{flex:none;width:100%;min-width:0}.branches-row.svelte-1uha8ag .field:where(.svelte-1uha8ag){flex:1;min-width:160px}.field.svelte-1uha8ag span:where(.svelte-1uha8ag){color:var(--text-faint);font-size:11px}.repo-row.svelte-1uha8ag{gap:6px;min-width:0;display:flex}.repo-row.svelte-1uha8ag input[type=text]:where(.svelte-1uha8ag){flex:1;min-width:0}.repo-row.svelte-1uha8ag button:where(.svelte-1uha8ag){flex-shrink:0}.source-toggle.svelte-1uha8ag{margin-bottom:10px}input[type=text].svelte-1uha8ag,select.svelte-1uha8ag{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);flex:1;min-width:0;padding:9px 12px}button.svelte-1uha8ag{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);padding:9px 12px;font-size:12.5px}button.svelte-1uha8ag:hover:not(:disabled){border-color:var(--accent)}button.svelte-1uha8ag:disabled{opacity:.5;cursor:not-allowed}.mode-toggle.svelte-1uha8ag{border:1px solid var(--border);border-radius:var(--radius);width:fit-content;margin-bottom:10px;display:flex;overflow:hidden}.mode-toggle.svelte-1uha8ag button:where(.svelte-1uha8ag){background:0 0;border:0;border-radius:0;padding:6px 12px;font-size:12px}.mode-toggle.svelte-1uha8ag button:where(.svelte-1uha8ag)+button:where(.svelte-1uha8ag){border-left:1px solid var(--border)}.mode-toggle.svelte-1uha8ag button.on:where(.svelte-1uha8ag){background:var(--btn-primary-bg);color:var(--btn-primary-text);font-weight:600}textarea.svelte-1uha8ag{resize:vertical;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);width:100%;height:220px;font-family:var(--mono);color:var(--text);padding:12px;font-size:12px;line-height:1.5}.loading-models.svelte-1uha8ag{color:var(--text-faint);font-weight:400}.models-error.svelte-1uha8ag{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}button.linkish.svelte-1uha8ag{color:var(--accent,#3b82f6);cursor:pointer;font:inherit;background:0 0;border:none;padding:0;text-decoration:underline}.actions.svelte-1uha8ag{gap:8px;margin-top:10px;display:flex}button.primary.svelte-1uha8ag{background:var(--btn-primary-bg);border-color:var(--btn-primary-border);color:var(--btn-primary-text);justify-content:center;align-items:center;gap:8px;font-weight:600;display:inline-flex}.btn-spinner.svelte-1uha8ag{border:2px solid color-mix(in srgb, var(--btn-primary-text) 35%, transparent);border-top-color:var(--btn-primary-text);border-radius:50%;flex-shrink:0;width:14px;height:14px;animation:.75s linear infinite svelte-1uha8ag-btn-spin}@keyframes svelte-1uha8ag-btn-spin{to{transform:rotate(360deg)}}.coverage-box.svelte-1uha8ag{border:1px solid var(--border);border-radius:var(--radius);border-left:3px solid var(--sev-med);background:var(--bg-card);flex-direction:column;gap:10px;margin-top:12px;padding:12px 14px;display:flex}.coverage-head.svelte-1uha8ag{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.coverage-head.svelte-1uha8ag strong:where(.svelte-1uha8ag){color:var(--sev-med);font-size:13px}.coverage-head.svelte-1uha8ag span:where(.svelte-1uha8ag){color:var(--text-faint);font-size:11.5px}.coverage-box.svelte-1uha8ag summary:where(.svelte-1uha8ag){color:var(--text-dim);cursor:pointer;font-size:12px}.coverage-list.svelte-1uha8ag{max-height:180px;font-family:var(--mono);color:var(--text-faint);margin:8px 0 0;padding-left:18px;font-size:11px;overflow:auto}.coverage-box.svelte-1uha8ag textarea:where(.svelte-1uha8ag){resize:vertical;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);width:100%;font-family:var(--mono);color:var(--text);padding:10px;font-size:11.5px;line-height:1.5}.coverage-actions.svelte-1uha8ag{flex-wrap:wrap;gap:8px;display:flex}.dropzone.svelte-1uha8ag{border:1.5px dashed var(--border);text-align:center;background:var(--bg-elev);padding:26px;transition:background .15s,border-color .15s}.dropzone.drag.svelte-1uha8ag{border-color:var(--accent);background:var(--accent-soft)}.dropzone.busy.svelte-1uha8ag{opacity:.7}.dropzone.svelte-1uha8ag p:where(.svelte-1uha8ag){color:var(--text-dim);margin:0 0 6px;font-size:14px}.visually-hidden.svelte-1uha8ag{clip:rect(0 0 0 0);width:1px;height:1px;position:absolute;overflow:hidden}.drop-errors.svelte-1uha8ag{background:var(--danger-bg);color:var(--danger);margin-top:10px;padding:10px 12px;font-size:12.5px}.drop-errors.svelte-1uha8ag strong:where(.svelte-1uha8ag){margin-bottom:4px;display:block}.drop-errors.svelte-1uha8ag ul:where(.svelte-1uha8ag){margin:0;padding-left:18px;line-height:1.6}.reports-col.svelte-1uha8ag{min-width:0;max-height:calc(100vh - 110px);position:sticky;top:88px;overflow:auto}.report-list.svelte-1uha8ag{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.report-item.svelte-1uha8ag{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-card);align-items:stretch;display:flex;position:relative;overflow:visible}.report-item.svelte-1uha8ag:hover{background:var(--bg-card-hover);border-color:#33507a}.report-card.svelte-1uha8ag{min-width:0;color:var(--text);flex:1;padding:12px 8px 12px 14px;text-decoration:none;display:block}.report-actions.svelte-1uha8ag{flex-shrink:0;padding:8px 8px 0 0;position:relative}.kebab.svelte-1uha8ag{border-radius:var(--radius);width:40px;min-width:40px;height:40px;min-height:40px;color:var(--text-dim);cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center;align-items:center;padding:0;display:inline-flex}.kebab.svelte-1uha8ag:hover{border-color:var(--border);color:var(--text);background:var(--bg)}.report-menu.svelte-1uha8ag{z-index:40;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-card);width:148px;padding:4px;position:fixed;box-shadow:0 8px 24px #0000002e}.report-menu.svelte-1uha8ag .menu-item:where(.svelte-1uha8ag){width:100%;color:var(--text);text-align:left;cursor:pointer;background:0 0;border:0;padding:8px 10px;font-size:12.5px;display:block}.report-menu.svelte-1uha8ag .menu-item:where(.svelte-1uha8ag):hover{background:var(--accent-soft)}.report-menu.svelte-1uha8ag .menu-item.danger:where(.svelte-1uha8ag){color:var(--danger)}.report-top.svelte-1uha8ag{justify-content:space-between;align-items:baseline;gap:12px;display:flex}.report-top.svelte-1uha8ag strong:where(.svelte-1uha8ag){font-size:14px}.report-prov.svelte-1uha8ag{min-width:0;color:var(--text-dim);align-items:center;gap:6px;margin-top:4px;font-size:12px;display:flex}.report-prov.svelte-1uha8ag .prov-agent:where(.svelte-1uha8ag){text-overflow:ellipsis;white-space:nowrap;flex:0 auto;min-width:0;overflow:hidden}.report-prov.svelte-1uha8ag .prov-sep:where(.svelte-1uha8ag){color:var(--text-faint);flex-shrink:0}.report-prov.svelte-1uha8ag .prov-model:where(.svelte-1uha8ag){min-width:0;font-family:var(--mono);text-overflow:ellipsis;white-space:nowrap;flex:auto;font-size:11.5px;overflow:hidden}.when.svelte-1uha8ag{color:var(--text-dim);font-size:12px}.report-meta.svelte-1uha8ag{color:var(--text-dim);justify-content:space-between;align-items:baseline;gap:12px;margin-top:6px;font-size:12px;display:flex}.report-base.svelte-1uha8ag{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.base-arrow.svelte-1uha8ag{color:var(--text-faint)}.report-counts.svelte-1uha8ag{flex-shrink:0;align-items:baseline;gap:8px;display:flex}.pill.svelte-1uha8ag{font-family:var(--mono);font-size:11px}.pill.bad.svelte-1uha8ag{color:var(--danger)}.report-intent.svelte-1uha8ag{color:var(--text-dim);-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;margin:8px 0 0;font-size:12.5px;line-height:1.45;display:-webkit-box;overflow:hidden}@media (width<=960px){.workspace.svelte-1uha8ag{grid-template-columns:1fr}.reports-col.svelte-1uha8ag{max-height:none;position:static}}@media (width<=768px){.repo-row.svelte-1uha8ag{flex-direction:column;align-items:stretch}.repo-row.svelte-1uha8ag button:where(.svelte-1uha8ag){width:100%}.branches-row.svelte-1uha8ag{flex-direction:column}.branches-row.svelte-1uha8ag .field:where(.svelte-1uha8ag){width:100%;min-width:0}.row.svelte-1uha8ag{flex-direction:column}}