@payconductor/houndsman 0.3.0 → 0.5.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/README.md +25 -39
- package/package.json +1 -1
- package/src/cli/commands/context-command.ts +11 -9
- package/src/cli/commands/doctor-command.ts +5 -0
- package/src/cli/commands/findings-command.ts +13 -3
- package/src/cli/commands/review-command.ts +1 -2
- package/src/cli/commands/status-command.ts +46 -0
- package/src/cli/help.ts +8 -3
- package/src/cli/index.ts +5 -2
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +216 -125
- package/src/core/commands/review.ts +7 -8
- package/src/core/commands/scaffold.ts +15 -16
- package/src/core/commands/status.ts +63 -0
- package/src/core/config.ts +16 -7
- package/src/core/engine/houndsman-engine.ts +15 -1
- package/src/core/engine/review-scope.ts +26 -8
- package/src/core/engine/review-session.ts +37 -9
- package/src/core/findings-state.ts +53 -75
- package/src/core/github-review.ts +620 -57
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +271 -134
- package/src/core/paths.ts +26 -17
- package/src/core/pr-context.ts +19 -5
- package/src/core/project-config.ts +4 -5
- package/src/core/renderers/manifest-writer.ts +29 -18
- package/src/core/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/run-layout.ts +4 -104
- package/src/core/run-status.ts +59 -50
- package/src/core/services/git-repository.ts +16 -0
- package/src/core/services/project-files.ts +13 -1
- package/src/core/storage/factory.ts +24 -0
- package/src/core/storage/file/file-storage.ts +271 -0
- package/src/core/storage/paths.ts +64 -0
- package/src/core/storage/repo-identity.ts +70 -0
- package/src/core/storage/sqlite/mappers.ts +162 -0
- package/src/core/storage/sqlite/schema.ts +116 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +431 -0
- package/src/core/storage/types.ts +152 -0
- package/src/core/storage/work-area.ts +191 -0
- package/src/index.ts +27 -6
- package/src/server/artifacts.ts +8 -61
- package/src/server/client/App.tsx +26 -9
- package/src/server/client/ArtifactsPanel.tsx +144 -23
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +50 -15
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +181 -0
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +58 -10
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/utils/code-highlight.ts +422 -0
- package/src/server/client/utils/constants.ts +226 -0
- package/src/server/client/utils/json-highlight.ts +55 -0
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +81 -0
- package/src/server/index.ts +154 -16
- package/src/server/run-repository.ts +153 -136
- package/src/skill/OUTPUT-FORMAT.md +6 -4
- package/src/skill/README.md +22 -14
- package/src/skill/SETUP.md +4 -4
- package/src/skill/example-review.md +22 -14
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +55 -32
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +7 -6
- package/src/tests/__snapshots__/baseline.test.ts.snap +74 -61
- package/src/tests/baseline.test.ts +79 -34
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +1 -1
- package/src/tests/findings-commands.test.ts +153 -131
- package/src/tests/findings-state.test.ts +56 -5
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/infrastructure.test.ts +33 -38
- package/src/tests/markdown.test.ts +149 -0
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +81 -5
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +86 -23
- package/src/tests/server.test.ts +184 -96
- package/src/tests/storage.test.ts +449 -0
- package/src/server/client/constants.ts +0 -91
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
|
@@ -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
|
|
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 {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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",
|
|
@@ -122,8 +122,13 @@ export function SubmitModal({
|
|
|
122
122
|
() => selected.filter((candidate) => includeSent || !candidate.sent),
|
|
123
123
|
[selected, includeSent],
|
|
124
124
|
);
|
|
125
|
-
const
|
|
126
|
-
|
|
125
|
+
const totals = useMemo(
|
|
126
|
+
() => ({
|
|
127
|
+
line: toSend.filter((candidate) => candidate.kind === "line").length,
|
|
128
|
+
file: toSend.filter((candidate) => candidate.kind === "file").length,
|
|
129
|
+
}),
|
|
130
|
+
[toSend],
|
|
131
|
+
);
|
|
127
132
|
const skipped = selected.length - toSend.length;
|
|
128
133
|
|
|
129
134
|
const submit = useCallback(async () => {
|
|
@@ -277,8 +282,8 @@ export function SubmitModal({
|
|
|
277
282
|
|
|
278
283
|
<div class="submit-summary">
|
|
279
284
|
Serão disparados <b>{toSend.length}</b> comentário(s)
|
|
280
|
-
— <b>{
|
|
281
|
-
arquivo
|
|
285
|
+
— <b>{totals.line}</b> na linha · <b>{totals.file}</b>{" "}
|
|
286
|
+
no arquivo
|
|
282
287
|
{skipped > 0 ? (
|
|
283
288
|
<>
|
|
284
289
|
{" "}
|
|
@@ -328,6 +333,27 @@ export function SubmitModal({
|
|
|
328
333
|
arquivo
|
|
329
334
|
</span>
|
|
330
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
|
+
))}
|
|
331
357
|
</div>
|
|
332
358
|
))}
|
|
333
359
|
</div>
|
|
@@ -384,22 +410,44 @@ export function SubmitModal({
|
|
|
384
410
|
/** Resumo do resultado do envio. */
|
|
385
411
|
function ResultView({ outcome }: { outcome: SubmitOutcome }) {
|
|
386
412
|
const generalUrl = outcome.general?.url;
|
|
413
|
+
const review = outcome.review;
|
|
414
|
+
const reviewUrl = review?.url;
|
|
387
415
|
return (
|
|
388
416
|
<>
|
|
389
417
|
<div class="submit-summary">
|
|
390
418
|
Enviados <b>{outcome.posted.length}</b> · falhas{" "}
|
|
391
419
|
<b>{outcome.failed.length}</b> · pulados <b>{outcome.skipped.length}</b>.
|
|
392
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
|
+
)}
|
|
393
441
|
{generalUrl && (
|
|
394
442
|
<p class="submit-empty">
|
|
395
443
|
Comentário geral:{" "}
|
|
396
444
|
<a
|
|
397
|
-
class="sent-tag"
|
|
445
|
+
class="sent-tag gh"
|
|
398
446
|
href={generalUrl}
|
|
399
447
|
target="_blank"
|
|
400
448
|
rel="noreferrer"
|
|
401
449
|
>
|
|
402
|
-
<
|
|
450
|
+
<GitHub size={12} /> abrir
|
|
403
451
|
</a>
|
|
404
452
|
</p>
|
|
405
453
|
)}
|
|
@@ -51,6 +51,14 @@ export const ExternalLink = (p: IconProps) => (
|
|
|
51
51
|
</Svg>
|
|
52
52
|
);
|
|
53
53
|
|
|
54
|
+
/** Marca do GitHub (contorno) — usada em achados já publicados na PR. */
|
|
55
|
+
export const GitHub = (p: IconProps) => (
|
|
56
|
+
<Svg {...p}>
|
|
57
|
+
<path d="M9 19c-4.3 1.3-4.3-2.2-6-2.7" />
|
|
58
|
+
<path d="M15 22v-3.9a3.4 3.4 0 0 0-.9-2.6c3-.3 6.1-1.5 6.1-6.8a5.3 5.3 0 0 0-1.4-3.7 4.9 4.9 0 0 0-.1-3.6s-1.1-.4-3.7 1.4a12.7 12.7 0 0 0-6.8 0C5.6 1 4.5 1.4 4.5 1.4a4.9 4.9 0 0 0-.1 3.6A5.3 5.3 0 0 0 3 8.7c0 5.2 3.1 6.4 6.1 6.8a3.4 3.4 0 0 0-.9 2.6V22" />
|
|
59
|
+
</Svg>
|
|
60
|
+
);
|
|
61
|
+
|
|
54
62
|
export const Sparkles = (p: IconProps) => (
|
|
55
63
|
<Svg {...p}>
|
|
56
64
|
<path d="M11 3.5l1.7 4.3 4.3 1.7-4.3 1.7L11 15.5 9.3 11.2 5 9.5l4.3-1.7z" />
|
|
@@ -126,6 +134,15 @@ export const Layers = (p: IconProps) => (
|
|
|
126
134
|
</Svg>
|
|
127
135
|
);
|
|
128
136
|
|
|
137
|
+
/** Documento com linhas — aba de resumo. */
|
|
138
|
+
export const FileText = (p: IconProps) => (
|
|
139
|
+
<Svg {...p}>
|
|
140
|
+
<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" />
|
|
141
|
+
<path d="M14 3v5h5" />
|
|
142
|
+
<path d="M9 13h6M9 17h4" />
|
|
143
|
+
</Svg>
|
|
144
|
+
);
|
|
145
|
+
|
|
129
146
|
/** Olho — "quem auditou o trecho" (watchdogs envolvidos). */
|
|
130
147
|
export const Watch = (p: IconProps) => (
|
|
131
148
|
<Svg {...p}>
|