@payconductor/houndsman 0.4.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.
Files changed (196) hide show
  1. package/README.md +35 -41
  2. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  3. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  4. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  5. package/assets/grammars/web-tree-sitter.wasm +0 -0
  6. package/package.json +11 -3
  7. package/src/cli/commands/command.ts +5 -0
  8. package/src/cli/commands/doctor-command.ts +116 -24
  9. package/src/cli/commands/hunt-command.ts +25 -0
  10. package/src/cli/commands/status-command.ts +46 -0
  11. package/src/cli/help.ts +18 -1
  12. package/src/cli/index.ts +13 -6
  13. package/src/core/ai/ai-client.ts +221 -0
  14. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  15. package/src/core/ai/errors.ts +36 -0
  16. package/src/core/ai/features/brief-enricher.ts +62 -0
  17. package/src/core/ai/features/classifier.ts +50 -0
  18. package/src/core/ai/features/context-summarizer.ts +31 -0
  19. package/src/core/ai/features/pre-analyzer.ts +51 -0
  20. package/src/core/ai/features/verifier.ts +45 -0
  21. package/src/core/ai/index.ts +56 -0
  22. package/src/core/ai/model-factory.ts +137 -0
  23. package/src/core/ai/prompts/base.ts +36 -0
  24. package/src/core/ai/prompts/brief.ts +87 -0
  25. package/src/core/ai/prompts/classification.ts +36 -0
  26. package/src/core/ai/prompts/context.ts +29 -0
  27. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  28. package/src/core/ai/prompts/verify.ts +40 -0
  29. package/src/core/ai/schemas.ts +110 -0
  30. package/src/core/ai/types.ts +76 -0
  31. package/src/core/ai/usage-budget.ts +75 -0
  32. package/src/core/ai/usage-recorder.ts +79 -0
  33. package/src/core/assets.ts +16 -0
  34. package/src/core/ast/diff.ts +220 -0
  35. package/src/core/ast/impact.ts +127 -0
  36. package/src/core/ast/parser.ts +167 -0
  37. package/src/core/ast/service.ts +233 -0
  38. package/src/core/ast/symbols.ts +294 -0
  39. package/src/core/ast/types.ts +125 -0
  40. package/src/core/brief.ts +138 -0
  41. package/src/core/commands/findings.ts +120 -6
  42. package/src/core/commands/review.ts +2 -0
  43. package/src/core/commands/scaffold.ts +159 -10
  44. package/src/core/commands/status.ts +63 -0
  45. package/src/core/commands/update.ts +5 -3
  46. package/src/core/config.ts +85 -0
  47. package/src/core/defaults.ts +27 -0
  48. package/src/core/domain/watchdog.ts +43 -5
  49. package/src/core/engine/houndsman-engine.ts +16 -1
  50. package/src/core/engine/review-scope.ts +77 -13
  51. package/src/core/engine/review-session.ts +27 -2
  52. package/src/core/env.ts +101 -0
  53. package/src/core/file-classifier.ts +25 -4
  54. package/src/core/findings-state.ts +21 -4
  55. package/src/core/github-review.ts +620 -57
  56. package/src/core/jev/jev-client.ts +8 -7
  57. package/src/core/jev/jev-verify.ts +2 -1
  58. package/src/core/logging/index.ts +17 -0
  59. package/src/core/logging/logger.ts +187 -0
  60. package/src/core/orchestrator.ts +454 -16
  61. package/src/core/paths.ts +3 -0
  62. package/src/core/project-config.ts +123 -1
  63. package/src/core/registry-check.ts +148 -6
  64. package/src/core/renderers/manifest-writer.ts +33 -0
  65. package/src/core/renderers/resume-renderer.ts +151 -0
  66. package/src/core/report-builder.ts +7 -4
  67. package/src/core/rule-text.ts +97 -0
  68. package/src/core/run-status.ts +39 -5
  69. package/src/core/services/file-classifier.ts +10 -3
  70. package/src/core/services/verifier.ts +14 -3
  71. package/src/core/storage/file/file-storage.ts +2 -0
  72. package/src/core/storage/paths.ts +6 -5
  73. package/src/core/storage/sqlite/mappers.ts +2 -0
  74. package/src/core/storage/sqlite/schema.ts +9 -1
  75. package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
  76. package/src/core/storage/types.ts +15 -0
  77. package/src/core/storage/work-area.ts +12 -0
  78. package/src/core/types.ts +37 -1
  79. package/src/core/verify.ts +17 -5
  80. package/src/harness/README.md +15 -0
  81. package/src/harness/agent/agent-context.ts +33 -0
  82. package/src/harness/agent/agent-errors.ts +30 -0
  83. package/src/harness/agent/agent-factory.ts +45 -0
  84. package/src/harness/agent/agent-reporter.ts +34 -0
  85. package/src/harness/agent/agent-result.ts +16 -0
  86. package/src/harness/agent/agent-runner.ts +32 -0
  87. package/src/harness/agent/agent-scheduler.ts +37 -0
  88. package/src/harness/agent/review-agent.ts +391 -0
  89. package/src/harness/config/harness-config.ts +84 -0
  90. package/src/harness/hunt/hunt-harness.ts +83 -0
  91. package/src/harness/hunt/hunt-options.ts +75 -0
  92. package/src/harness/hunt/hunt-result.ts +17 -0
  93. package/src/harness/hunt/hunt-session.ts +372 -0
  94. package/src/harness/hunt/review-planner.ts +98 -0
  95. package/src/harness/index.ts +36 -0
  96. package/src/harness/investigation/agent-state.ts +134 -0
  97. package/src/harness/investigation/evidence-verifier.ts +129 -0
  98. package/src/harness/investigation/evidence.ts +110 -0
  99. package/src/harness/investigation/finding-critic.ts +96 -0
  100. package/src/harness/investigation/investigation-planner.ts +118 -0
  101. package/src/harness/investigation/investigation-task.ts +10 -0
  102. package/src/harness/investigation/review-journal.ts +87 -0
  103. package/src/harness/investigation/rule-analyzer.ts +178 -0
  104. package/src/harness/investigation/rule-signals.ts +110 -0
  105. package/src/harness/prompts/agent-schemas.ts +57 -0
  106. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  107. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  108. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  109. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  110. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  111. package/src/harness/tools/agent-tool.ts +102 -0
  112. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  113. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  114. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  115. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  116. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  117. package/src/harness/tools/builtin/helpers.ts +115 -0
  118. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  119. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  120. package/src/harness/tools/builtin/index.ts +59 -0
  121. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  122. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  123. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  124. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  125. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  126. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  127. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  128. package/src/harness/tools/path-guard.ts +90 -0
  129. package/src/harness/tools/result-limiter.ts +49 -0
  130. package/src/harness/tools/secret-redactor.ts +54 -0
  131. package/src/harness/tools/tool-cache.ts +50 -0
  132. package/src/harness/tools/tool-call-budget.ts +60 -0
  133. package/src/harness/tools/tool-context.ts +5 -0
  134. package/src/harness/tools/tool-middleware.ts +105 -0
  135. package/src/harness/tools/tool-policy.ts +51 -0
  136. package/src/harness/tools/tool-registry.ts +112 -0
  137. package/src/harness/tools/tool-result.ts +15 -0
  138. package/src/harness/tools/toolbox.ts +36 -0
  139. package/src/harness/validation/finding-validator.ts +119 -0
  140. package/src/index.ts +46 -0
  141. package/src/server/client/App.tsx +26 -9
  142. package/src/server/client/ArtifactsPanel.tsx +12 -5
  143. package/src/server/client/AutoRefresh.tsx +1 -1
  144. package/src/server/client/Filters.tsx +7 -2
  145. package/src/server/client/FindingCard.tsx +14 -10
  146. package/src/server/client/FindingsList.tsx +2 -2
  147. package/src/server/client/MarkdownText.tsx +92 -12
  148. package/src/server/client/ResumePanel.tsx +368 -0
  149. package/src/server/client/RunList.tsx +3 -3
  150. package/src/server/client/SubmitModal.tsx +49 -6
  151. package/src/server/client/SummaryBar.tsx +1 -1
  152. package/src/server/client/icons.tsx +17 -0
  153. package/src/server/client/{constants.ts → utils/constants.ts} +61 -10
  154. package/src/server/client/utils/markdown.ts +319 -0
  155. package/src/server/client/{types.ts → utils/types.ts} +64 -0
  156. package/src/server/index.ts +66 -7
  157. package/src/server/run-repository.ts +9 -0
  158. package/src/skill/OUTPUT-FORMAT.md +12 -0
  159. package/src/skill/SETUP.md +6 -2
  160. package/src/skill/example-review.md +22 -14
  161. package/src/skill/hunt.md +45 -19
  162. package/src/skill/verify.md +3 -2
  163. package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
  164. package/src/tests/ai-core.test.ts +120 -0
  165. package/src/tests/ai-features.test.ts +183 -0
  166. package/src/tests/ast.test.ts +335 -0
  167. package/src/tests/baseline.test.ts +3 -0
  168. package/src/tests/code-highlight.test.ts +1 -1
  169. package/src/tests/env.test.ts +69 -0
  170. package/src/tests/fallback.test.ts +172 -0
  171. package/src/tests/findings-state.test.ts +50 -2
  172. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  173. package/src/tests/fixtures/fake-investigation.ts +64 -0
  174. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  175. package/src/tests/github-review.test.ts +409 -60
  176. package/src/tests/harness-agent.test.ts +122 -0
  177. package/src/tests/harness-investigation.test.ts +206 -0
  178. package/src/tests/harness-tools.test.ts +148 -0
  179. package/src/tests/hunt-command.test.ts +212 -0
  180. package/src/tests/logger.test.ts +83 -0
  181. package/src/tests/markdown.test.ts +61 -1
  182. package/src/tests/review-resume.test.ts +101 -0
  183. package/src/tests/review-scope.test.ts +302 -0
  184. package/src/tests/review-scripts.test.ts +73 -0
  185. package/src/tests/rule-analyzer.test.ts +61 -0
  186. package/src/tests/run-status-view.test.ts +94 -0
  187. package/src/tests/run-status.test.ts +53 -0
  188. package/src/tests/server.test.ts +76 -21
  189. package/src/tests/storage.test.ts +63 -0
  190. package/src/tests/tool-contracts.test.ts +82 -0
  191. package/src/tests/verify.test.ts +1 -1
  192. package/src/server/client/markdown.ts +0 -153
  193. /package/src/server/client/{api.ts → utils/api.ts} +0 -0
  194. /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
  195. /package/src/server/client/{format.ts → utils/format.ts} +0 -0
  196. /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
@@ -7,21 +7,22 @@
7
7
  */
8
8
 
9
9
  import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
10
- import { api } from "./api";
11
- import { highlightCode, languageFromFile } from "./code-highlight";
12
- import { DECISION_LABELS, DECISIONS, SEVERITY_LABELS } from "./constants";
13
- import { vscodeFileLink } from "./format";
14
10
  import {
15
11
  Check,
16
12
  ChevronRight,
17
13
  CodeIcon,
18
14
  ExternalLink,
15
+ GitHub,
19
16
  Sparkles,
20
17
  Spinner,
21
18
  Watch,
22
19
  } from "./icons";
23
20
  import { MarkdownText } from "./MarkdownText";
24
- import type { Finding } from "./types";
21
+ import { api } from "./utils/api";
22
+ import { highlightCode, languageFromFile } from "./utils/code-highlight";
23
+ import { DECISION_LABELS, DECISIONS, SEVERITY_LABELS } from "./utils/constants";
24
+ import { vscodeFileLink } from "./utils/format";
25
+ import type { Finding } from "./utils/types";
25
26
 
26
27
  /** Debounce do auto-save do comentário (ms). */
27
28
  const AUTOSAVE_MS = 700;
@@ -155,17 +156,20 @@ export function FindingCard({
155
156
  {finding.github &&
156
157
  (finding.github.url ? (
157
158
  <a
158
- class="sent-tag"
159
+ class="sent-tag gh"
159
160
  href={finding.github.url}
160
161
  target="_blank"
161
162
  rel="noreferrer"
162
- title="Comentário enviado à PR"
163
+ title={`Comentário enviado ao GitHub (PR #${finding.github.pr}) — abrir`}
163
164
  >
164
- <ExternalLink size={12} /> enviado
165
+ <GitHub size={12} /> enviado ao GitHub
165
166
  </a>
166
167
  ) : (
167
- <span class="sent-tag" title="Comentário enviado à PR">
168
- <ExternalLink size={12} /> enviado
168
+ <span
169
+ class="sent-tag gh"
170
+ title={`Comentário enviado ao GitHub (PR #${finding.github.pr})`}
171
+ >
172
+ <GitHub size={12} /> enviado ao GitHub
169
173
  </span>
170
174
  ))}
171
175
  <span class="spacer" />
@@ -3,10 +3,10 @@
3
3
  */
4
4
 
5
5
  import { useMemo } from "preact/hooks";
6
- import { SEVERITY_LABELS, SEVERITY_ORDER } from "./constants";
7
6
  import { FindingCard } from "./FindingCard";
8
7
  import { Alert } from "./icons";
9
- import type { Finding, Severity } from "./types";
8
+ import { SEVERITY_LABELS, SEVERITY_ORDER } from "./utils/constants";
9
+ import type { Finding, Severity } from "./utils/types";
10
10
 
11
11
  export function FindingsList({
12
12
  slug,
@@ -1,28 +1,48 @@
1
1
  /**
2
- * Render de markdown mínimo para o campo `action` do finding.
2
+ * Render de markdown mínimo para o campo `action` do finding e artefatos.
3
3
  *
4
4
  * Sem HTML cru: tudo vira nós do Preact. Blocos de código reutilizam o
5
- * highlight de código; links passam por `safeUrl`.
5
+ * highlight de código; links passam por `safeUrl` e caminhos de arquivo ganham
6
+ * destaque próprio.
6
7
  */
7
8
 
8
9
  import { useMemo } from "preact/hooks";
9
- import { highlightCode, languageFromName } from "./code-highlight";
10
- import { type InlineToken, parseInline, parseMarkdown, safeUrl } from "./markdown";
10
+ import { highlightCode, languageFromName } from "./utils/code-highlight";
11
+ import {
12
+ type InlineToken,
13
+ isFilePath,
14
+ type MarkdownBlock,
15
+ parseInline,
16
+ parseMarkdown,
17
+ safeUrl,
18
+ } from "./utils/markdown";
11
19
 
12
20
  function renderInlineToken(token: InlineToken) {
13
21
  switch (token.type) {
14
22
  case "code":
15
- return <code class="md-code">{token.text}</code>;
23
+ return (
24
+ <code class={`md-code${isFilePath(token.text) ? " md-path" : ""}`}>
25
+ {token.text}
26
+ </code>
27
+ );
28
+ case "path":
29
+ return <code class="md-path">{token.text}</code>;
16
30
  case "strong":
17
31
  return <strong>{token.text}</strong>;
18
32
  case "em":
19
33
  return <em>{token.text}</em>;
20
34
  case "link": {
35
+ const label = token.text.replace(/^`+|`+$/g, "");
21
36
  const url = safeUrl(token.url);
22
- return url ? (
23
- <a href={url} target="_blank" rel="noreferrer">
24
- {token.text}
25
- </a>
37
+ if (url) {
38
+ return (
39
+ <a href={url} target="_blank" rel="noreferrer">
40
+ {label}
41
+ </a>
42
+ );
43
+ }
44
+ return isFilePath(label) || isFilePath(token.url) ? (
45
+ <code class="md-path">{label}</code>
26
46
  ) : (
27
47
  token.text
28
48
  );
@@ -59,9 +79,8 @@ function CodeBlock({ code, language }: { code: string; language: string | null }
59
79
  );
60
80
  }
61
81
 
62
- /** Renderiza o markdown do action preservando quebras de linha e listas. */
63
- export function MarkdownText({ text }: { text: string }) {
64
- const blocks = useMemo(() => parseMarkdown(text), [text]);
82
+ /** Renderiza blocos markdown já parseados (seções de artefato, ação etc.). */
83
+ export function MarkdownBlocks({ blocks }: { blocks: MarkdownBlock[] }) {
65
84
  return (
66
85
  <>
67
86
  {blocks.map((block) => {
@@ -72,6 +91,21 @@ export function MarkdownText({ text }: { text: string }) {
72
91
  <InlineTokens text={block.text} />
73
92
  </p>
74
93
  );
94
+ case "heading": {
95
+ const level = Math.min(Math.max(block.level, 1), 6);
96
+ const Tag = `h${level}` as
97
+ | "h1"
98
+ | "h2"
99
+ | "h3"
100
+ | "h4"
101
+ | "h5"
102
+ | "h6";
103
+ return (
104
+ <Tag class={`md-h md-h${level}`}>
105
+ <InlineTokens text={block.text} />
106
+ </Tag>
107
+ );
108
+ }
75
109
  case "list": {
76
110
  const items = block.items.map((item) => (
77
111
  <li>
@@ -84,6 +118,44 @@ export function MarkdownText({ text }: { text: string }) {
84
118
  <ul class="md-list">{items}</ul>
85
119
  );
86
120
  }
121
+ case "table":
122
+ return (
123
+ <div class="md-table-wrap">
124
+ <table class="md-table">
125
+ <thead>
126
+ <tr>
127
+ {block.header.map((cell, column) => (
128
+ <th
129
+ style={{
130
+ textAlign: block.align[column],
131
+ }}
132
+ >
133
+ <InlineTokens text={cell} />
134
+ </th>
135
+ ))}
136
+ </tr>
137
+ </thead>
138
+ <tbody>
139
+ {block.rows.map((row) => (
140
+ <tr key={row.join("|")}>
141
+ {block.header.map((_, column) => (
142
+ <td
143
+ style={{
144
+ textAlign:
145
+ block.align[column],
146
+ }}
147
+ >
148
+ <InlineTokens
149
+ text={row[column] ?? ""}
150
+ />
151
+ </td>
152
+ ))}
153
+ </tr>
154
+ ))}
155
+ </tbody>
156
+ </table>
157
+ </div>
158
+ );
87
159
  case "code":
88
160
  return <CodeBlock code={block.code} language={block.language} />;
89
161
  case "quote":
@@ -92,6 +164,8 @@ export function MarkdownText({ text }: { text: string }) {
92
164
  <InlineTokens text={block.text} />
93
165
  </blockquote>
94
166
  );
167
+ case "hr":
168
+ return <hr class="md-hr" />;
95
169
  default:
96
170
  return null;
97
171
  }
@@ -99,3 +173,9 @@ export function MarkdownText({ text }: { text: string }) {
99
173
  </>
100
174
  );
101
175
  }
176
+
177
+ /** Renderiza o markdown do action/artefato preservando blocos e listas. */
178
+ export function MarkdownText({ text }: { text: string }) {
179
+ const blocks = useMemo(() => parseMarkdown(text), [text]);
180
+ return <MarkdownBlocks blocks={blocks} />;
181
+ }
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Aba "Resumo" (inicial): visão principal da revisão.
3
+ *
4
+ * O texto narrativo vem do artefato `review-resume.md` (Resumo de negócio); os
5
+ * números viram cards a partir do `classification.json`/`manifest.json` e o
6
+ * card de domínios resume os achados por severidade em cada watchdog — assim a
7
+ * UI mostra os mesmos dados do resumo de forma mais organizada, sem duplicar o
8
+ * texto.
9
+ */
10
+
11
+ import type { ComponentChildren } from "preact";
12
+ import { useEffect, useMemo, useState } from "preact/hooks";
13
+ import { ExternalLink, Layers } from "./icons";
14
+ import { MarkdownBlocks } from "./MarkdownText";
15
+ import { api } from "./utils/api";
16
+ import { SEVERITY_LABELS, SEVERITY_ORDER } from "./utils/constants";
17
+ import { formatRunTime } from "./utils/format";
18
+ import { sectionBlocks } from "./utils/markdown";
19
+ import type {
20
+ ClassificationArtifact,
21
+ Finding,
22
+ RunDetail,
23
+ RunManifest,
24
+ Severity,
25
+ Summary,
26
+ } from "./utils/types";
27
+
28
+ const STATUS_VIEW = {
29
+ blocking: { label: "Mudanças necessárias", tone: "blocking" },
30
+ attention: { label: "Aprovado com ressalvas", tone: "attention" },
31
+ clean: { label: "Aprovado", tone: "clean" },
32
+ } as const;
33
+
34
+ const STATUS_LABELS: Record<string, string> = {
35
+ added: "adicionado(s)",
36
+ modified: "modificado(s)",
37
+ deleted: "removido(s)",
38
+ renamed: "renomeado(s)",
39
+ copied: "copiado(s)",
40
+ unknown: "alterado(s)",
41
+ };
42
+
43
+ /** Soma do churn quando o manifesto não traz os totais. */
44
+ function churnTotals(churn: ClassificationArtifact["churn"]): {
45
+ additions: number;
46
+ deletions: number;
47
+ } {
48
+ let additions = 0;
49
+ let deletions = 0;
50
+ for (const entry of Object.values(churn ?? {})) {
51
+ additions += entry.additions;
52
+ deletions += entry.deletions;
53
+ }
54
+ return { additions, deletions };
55
+ }
56
+
57
+ function ResumeKpi({
58
+ label,
59
+ value,
60
+ hint,
61
+ tone,
62
+ }: {
63
+ label: string;
64
+ value: ComponentChildren;
65
+ hint?: string;
66
+ tone?: "blocking" | "attention" | "total";
67
+ }) {
68
+ return (
69
+ <div class={`kpi${tone ? ` kpi-${tone}` : ""}`} title={hint}>
70
+ <span class="kpi-label">{label}</span>
71
+ <span class="kpi-value">{value}</span>
72
+ {hint && <span class="kpi-hint">{hint}</span>}
73
+ </div>
74
+ );
75
+ }
76
+
77
+ export function ResumePanel({
78
+ slug,
79
+ branch,
80
+ findings,
81
+ summary,
82
+ onOpenArtifacts,
83
+ }: {
84
+ slug: string;
85
+ branch: string;
86
+ findings: Finding[];
87
+ summary: Summary;
88
+ onOpenArtifacts: () => void;
89
+ }) {
90
+ const [resume, setResume] = useState("");
91
+ const [detail, setDetail] = useState<RunDetail | null>(null);
92
+ const [classification, setClassification] = useState<ClassificationArtifact | null>(
93
+ null,
94
+ );
95
+ const [error, setError] = useState("");
96
+ const [loading, setLoading] = useState(true);
97
+
98
+ useEffect(() => {
99
+ let cancelled = false;
100
+ setLoading(true);
101
+ setError("");
102
+ Promise.all([
103
+ api<RunDetail>(`/api/runs/${slug}`).catch(() => null),
104
+ fetch(`/api/runs/${slug}/artifacts/review-resume.md`)
105
+ .then((res) => (res.ok ? res.text() : ""))
106
+ .catch(() => ""),
107
+ api<ClassificationArtifact>(
108
+ `/api/runs/${slug}/artifacts/classification.json`,
109
+ ).catch(() => null),
110
+ ])
111
+ .then(([runDetail, markdown, classified]) => {
112
+ if (cancelled) return;
113
+ setDetail(runDetail);
114
+ setResume(markdown);
115
+ setClassification(classified);
116
+ })
117
+ .catch((err) => {
118
+ if (!cancelled) setError(String(err));
119
+ })
120
+ .finally(() => {
121
+ if (!cancelled) setLoading(false);
122
+ });
123
+ return () => {
124
+ cancelled = true;
125
+ };
126
+ }, [slug]);
127
+
128
+ const manifest: RunManifest | null = detail?.manifest ?? null;
129
+ const business = useMemo(() => sectionBlocks(resume, "Resumo de negócio"), [resume]);
130
+ const status =
131
+ summary.bySeverity.blocking > 0
132
+ ? STATUS_VIEW.blocking
133
+ : summary.bySeverity.attention > 0
134
+ ? STATUS_VIEW.attention
135
+ : STATUS_VIEW.clean;
136
+
137
+ const files = classification?.files ?? {};
138
+ const fileCount = Object.keys(files).length;
139
+ /** Arquivos com conteúdo para revisar (deletados não entram no escopo). */
140
+ const reviewableCount = Object.values(files).filter(
141
+ (file) => file.status !== "deleted",
142
+ ).length;
143
+ const statuses = useMemo(() => {
144
+ const counts: Record<string, number> = {};
145
+ for (const file of Object.values(files)) {
146
+ counts[file.status] = (counts[file.status] ?? 0) + 1;
147
+ }
148
+ return counts;
149
+ }, [files]);
150
+ const statusLabel = Object.entries(statuses)
151
+ .map(([state, total]) => `${total} ${STATUS_LABELS[state] ?? state}`)
152
+ .join(" · ");
153
+ const churn = manifest?.churn ?? churnTotals(classification?.churn);
154
+ const pr = manifest?.context?.source;
155
+ const reviewedCount = useMemo(() => {
156
+ const set = new Set<string>();
157
+ for (const domain of manifest?.watchdogs ?? []) {
158
+ for (const file of domain.files) set.add(file);
159
+ }
160
+ return set.size;
161
+ }, [manifest]);
162
+ const coverage =
163
+ manifest && reviewableCount > 0
164
+ ? `${Math.min(100, Math.round((reviewedCount / reviewableCount) * 100))}%`
165
+ : "—";
166
+
167
+ const domains = useMemo(() => {
168
+ const base = manifest?.watchdogs?.length
169
+ ? manifest.watchdogs.map((domain) => ({
170
+ id: domain.id,
171
+ name: domain.name || domain.id,
172
+ files: domain.files.length,
173
+ overflow: domain.overflow,
174
+ }))
175
+ : (detail?.watchdogs ?? []).map((id) => ({
176
+ id,
177
+ name: id,
178
+ files: 0,
179
+ overflow: 0,
180
+ }));
181
+
182
+ return base
183
+ .map((domain) => {
184
+ const bySeverity: Record<Severity, number> = {
185
+ blocking: 0,
186
+ attention: 0,
187
+ suggestion: 0,
188
+ nit: 0,
189
+ };
190
+ for (const finding of findings) {
191
+ if (!finding.watchdogIds.includes(domain.id)) continue;
192
+ bySeverity[finding.severity] += 1;
193
+ }
194
+ const total = SEVERITY_ORDER.reduce(
195
+ (sum, severity) => sum + bySeverity[severity],
196
+ 0,
197
+ );
198
+ return { ...domain, bySeverity, total };
199
+ })
200
+ .sort(
201
+ (a, b) =>
202
+ b.bySeverity.blocking - a.bySeverity.blocking ||
203
+ b.bySeverity.attention - a.bySeverity.attention ||
204
+ b.total - a.total ||
205
+ a.name.localeCompare(b.name),
206
+ );
207
+ }, [manifest, detail, findings]);
208
+
209
+ return (
210
+ <div class="resume">
211
+ <section class="resume-hero">
212
+ <div class="resume-hero-text">
213
+ <span class="resume-kicker">Resumo principal</span>
214
+ <h2 class="resume-title">{branch}</h2>
215
+ <p class="resume-meta">
216
+ {manifest?.generatedAt && (
217
+ <span>{formatRunTime(manifest.generatedAt)}</span>
218
+ )}
219
+ {domains.length > 0 && (
220
+ <span>
221
+ {domains.length}{" "}
222
+ {domains.length === 1 ? "domínio" : "domínios"}
223
+ </span>
224
+ )}
225
+ {pr?.url ? (
226
+ <a
227
+ class="resume-pr"
228
+ href={pr.url}
229
+ target="_blank"
230
+ rel="noreferrer"
231
+ >
232
+ {pr.number ? `PR #${pr.number}` : "Abrir PR"}
233
+ <ExternalLink size={12} />
234
+ </a>
235
+ ) : pr?.number ? (
236
+ <span>PR #{pr.number}</span>
237
+ ) : null}
238
+ </p>
239
+ </div>
240
+ <div class="resume-hero-side">
241
+ <span class={`resume-status ${status.tone}`}>{status.label}</span>
242
+ <button
243
+ type="button"
244
+ class="btn"
245
+ title="Ver os artefatos brutos da revisão"
246
+ onClick={onOpenArtifacts}
247
+ >
248
+ <Layers size={14} />
249
+ Ver artefatos
250
+ </button>
251
+ </div>
252
+ </section>
253
+
254
+ {error && <p class="error">{error}</p>}
255
+
256
+ <div class="kpis resume-kpis">
257
+ <ResumeKpi
258
+ label="Arquivos"
259
+ value={fileCount}
260
+ hint={statusLabel || "sem classificação"}
261
+ />
262
+ <ResumeKpi
263
+ label="Churn"
264
+ value={
265
+ <>
266
+ <span class="churn-add">+{churn.additions}</span>
267
+ <span class="churn-sep">/</span>
268
+ <span class="churn-del">-{churn.deletions}</span>
269
+ </>
270
+ }
271
+ hint="linhas adicionadas / removidas"
272
+ />
273
+ <ResumeKpi
274
+ label="Cobertura"
275
+ value={coverage}
276
+ hint={
277
+ reviewableCount > 0
278
+ ? `${reviewedCount}/${reviewableCount} revisáveis no escopo`
279
+ : "sem classificação"
280
+ }
281
+ />
282
+ <ResumeKpi
283
+ label="Achados"
284
+ value={summary.total}
285
+ hint={`${summary.bySeverity.blocking} bloqueantes · ${summary.bySeverity.attention} atenção`}
286
+ tone={
287
+ summary.bySeverity.blocking > 0
288
+ ? "blocking"
289
+ : summary.bySeverity.attention > 0
290
+ ? "attention"
291
+ : "total"
292
+ }
293
+ />
294
+ </div>
295
+
296
+ <div class="resume-grid">
297
+ <section class="resume-card">
298
+ <h3 class="resume-card-title">Resumo de negócio</h3>
299
+ {loading ? (
300
+ <p class="empty">Carregando resumo…</p>
301
+ ) : business.length > 0 ? (
302
+ <div class="resume-narrative">
303
+ <MarkdownBlocks blocks={business} />
304
+ </div>
305
+ ) : (
306
+ <p class="empty">
307
+ O resumo narrativo ainda não foi gerado para esta run. Rode o{" "}
308
+ <code>--finalize</code> para criar{" "}
309
+ <code>review-resume.md</code>.
310
+ </p>
311
+ )}
312
+ </section>
313
+
314
+ <section class="resume-card">
315
+ <div class="resume-card-head">
316
+ <h3 class="resume-card-title">Domínios</h3>
317
+ {domains.length > 0 && (
318
+ <span class="resume-card-count">{domains.length}</span>
319
+ )}
320
+ </div>
321
+ {domains.length > 0 ? (
322
+ <ul class="resume-domains">
323
+ {domains.map((domain) => (
324
+ <li class="resume-domain" key={domain.id}>
325
+ <div class="resume-domain-head">
326
+ <span class="resume-domain-name">
327
+ {domain.name}
328
+ </span>
329
+ <span class="resume-domain-files">
330
+ {domain.files > 0
331
+ ? `${domain.files} ${domain.files === 1 ? "arquivo" : "arquivos"}`
332
+ : "—"}
333
+ {domain.overflow > 0
334
+ ? ` · ${domain.overflow} fora do orçamento`
335
+ : ""}
336
+ </span>
337
+ </div>
338
+ <div class="resume-domain-sev">
339
+ {domain.total === 0 ? (
340
+ <span class="resume-domain-ok">
341
+ Sem achados
342
+ </span>
343
+ ) : (
344
+ SEVERITY_ORDER.filter(
345
+ (severity) =>
346
+ domain.bySeverity[severity] > 0,
347
+ ).map((severity) => (
348
+ <span
349
+ class={`sev-badge sev-${severity}`}
350
+ key={severity}
351
+ >
352
+ {SEVERITY_LABELS[severity]}{" "}
353
+ {domain.bySeverity[severity]}
354
+ </span>
355
+ ))
356
+ )}
357
+ </div>
358
+ </li>
359
+ ))}
360
+ </ul>
361
+ ) : (
362
+ <p class="empty">Nenhum domínio ativo na revisão.</p>
363
+ )}
364
+ </section>
365
+ </div>
366
+ </div>
367
+ );
368
+ }
@@ -3,10 +3,10 @@
3
3
  * com data/hora, relação de findings presentes e badge de correção pronta.
4
4
  */
5
5
 
6
- import { describeRunStatus } from "./constants";
7
- import { formatRunTime } from "./format";
8
6
  import { BranchIcon, Clock, Sparkles } from "./icons";
9
- import type { RunSummary } from "./types";
7
+ import { describeRunStatus } from "./utils/constants";
8
+ import { formatRunTime } from "./utils/format";
9
+ import type { RunSummary } from "./utils/types";
10
10
 
11
11
  export function RunList({
12
12
  runs,
@@ -9,16 +9,16 @@
9
9
  */
10
10
 
11
11
  import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
12
- import { api } from "./api";
13
- import { EXEC_STATES, SEVERITY_LABELS, SEVERITY_ORDER } from "./constants";
14
- import { ExternalLink, Send, Spinner } from "./icons";
12
+ import { GitHub, Send, Spinner } from "./icons";
13
+ import { api } from "./utils/api";
14
+ import { EXEC_STATES, SEVERITY_LABELS, SEVERITY_ORDER } from "./utils/constants";
15
15
  import type {
16
16
  ExecState,
17
17
  Severity,
18
18
  SubmitOutcome,
19
19
  SubmitPreview,
20
20
  SubmitResponse,
21
- } from "./types";
21
+ } from "./utils/types";
22
22
 
23
23
  const EXEC_LABELS: Record<ExecState, string> = {
24
24
  pending: "Pendente",
@@ -333,6 +333,27 @@ export function SubmitModal({
333
333
  arquivo
334
334
  </span>
335
335
  )}
336
+ {candidate.sent &&
337
+ (candidate.sentUrl ? (
338
+ <a
339
+ class="sent-tag gh"
340
+ href={candidate.sentUrl}
341
+ target="_blank"
342
+ rel="noreferrer"
343
+ title="Já enviado ao GitHub — abrir comentário"
344
+ >
345
+ <GitHub size={12} />
346
+ enviado
347
+ </a>
348
+ ) : (
349
+ <span
350
+ class="sent-tag gh"
351
+ title="Já enviado ao GitHub"
352
+ >
353
+ <GitHub size={12} />
354
+ enviado
355
+ </span>
356
+ ))}
336
357
  </div>
337
358
  ))}
338
359
  </div>
@@ -389,22 +410,44 @@ export function SubmitModal({
389
410
  /** Resumo do resultado do envio. */
390
411
  function ResultView({ outcome }: { outcome: SubmitOutcome }) {
391
412
  const generalUrl = outcome.general?.url;
413
+ const review = outcome.review;
414
+ const reviewUrl = review?.url;
392
415
  return (
393
416
  <>
394
417
  <div class="submit-summary">
395
418
  Enviados <b>{outcome.posted.length}</b> · falhas{" "}
396
419
  <b>{outcome.failed.length}</b> · pulados <b>{outcome.skipped.length}</b>.
397
420
  </div>
421
+ {review && (
422
+ <p class="submit-empty">
423
+ {review.created
424
+ ? review.finalized
425
+ ? `Revisão #${review.id} finalizada (${review.event}).`
426
+ : `Revisão #${review.id} criada, mas não finalizada.`
427
+ : `Comentários adicionados à revisão pendente #${review.id}.`}{" "}
428
+ {reviewUrl && (
429
+ <a
430
+ class="sent-tag gh"
431
+ href={reviewUrl}
432
+ target="_blank"
433
+ rel="noreferrer"
434
+ >
435
+ <GitHub size={12} /> abrir
436
+ </a>
437
+ )}
438
+ {review.error && <> ❌ {review.error}</>}
439
+ </p>
440
+ )}
398
441
  {generalUrl && (
399
442
  <p class="submit-empty">
400
443
  Comentário geral:{" "}
401
444
  <a
402
- class="sent-tag"
445
+ class="sent-tag gh"
403
446
  href={generalUrl}
404
447
  target="_blank"
405
448
  rel="noreferrer"
406
449
  >
407
- <ExternalLink size={12} /> abrir
450
+ <GitHub size={12} /> abrir
408
451
  </a>
409
452
  </p>
410
453
  )}