@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.
Files changed (91) hide show
  1. package/README.md +25 -39
  2. package/package.json +1 -1
  3. package/src/cli/commands/context-command.ts +11 -9
  4. package/src/cli/commands/doctor-command.ts +5 -0
  5. package/src/cli/commands/findings-command.ts +13 -3
  6. package/src/cli/commands/review-command.ts +1 -2
  7. package/src/cli/commands/status-command.ts +46 -0
  8. package/src/cli/help.ts +8 -3
  9. package/src/cli/index.ts +5 -2
  10. package/src/core/commands/context.ts +29 -13
  11. package/src/core/commands/findings.ts +216 -125
  12. package/src/core/commands/review.ts +7 -8
  13. package/src/core/commands/scaffold.ts +15 -16
  14. package/src/core/commands/status.ts +63 -0
  15. package/src/core/config.ts +16 -7
  16. package/src/core/engine/houndsman-engine.ts +15 -1
  17. package/src/core/engine/review-scope.ts +26 -8
  18. package/src/core/engine/review-session.ts +37 -9
  19. package/src/core/findings-state.ts +53 -75
  20. package/src/core/github-review.ts +620 -57
  21. package/src/core/jev/jev-candidates.ts +49 -35
  22. package/src/core/orchestrator.ts +271 -134
  23. package/src/core/paths.ts +26 -17
  24. package/src/core/pr-context.ts +19 -5
  25. package/src/core/project-config.ts +4 -5
  26. package/src/core/renderers/manifest-writer.ts +29 -18
  27. package/src/core/renderers/resume-renderer.ts +151 -0
  28. package/src/core/report-builder.ts +7 -4
  29. package/src/core/run-layout.ts +4 -104
  30. package/src/core/run-status.ts +59 -50
  31. package/src/core/services/git-repository.ts +16 -0
  32. package/src/core/services/project-files.ts +13 -1
  33. package/src/core/storage/factory.ts +24 -0
  34. package/src/core/storage/file/file-storage.ts +271 -0
  35. package/src/core/storage/paths.ts +64 -0
  36. package/src/core/storage/repo-identity.ts +70 -0
  37. package/src/core/storage/sqlite/mappers.ts +162 -0
  38. package/src/core/storage/sqlite/schema.ts +116 -0
  39. package/src/core/storage/sqlite/sqlite-storage.ts +431 -0
  40. package/src/core/storage/types.ts +152 -0
  41. package/src/core/storage/work-area.ts +191 -0
  42. package/src/index.ts +27 -6
  43. package/src/server/artifacts.ts +8 -61
  44. package/src/server/client/App.tsx +26 -9
  45. package/src/server/client/ArtifactsPanel.tsx +144 -23
  46. package/src/server/client/AutoRefresh.tsx +1 -1
  47. package/src/server/client/Filters.tsx +7 -2
  48. package/src/server/client/FindingCard.tsx +50 -15
  49. package/src/server/client/FindingsList.tsx +2 -2
  50. package/src/server/client/MarkdownText.tsx +181 -0
  51. package/src/server/client/ResumePanel.tsx +368 -0
  52. package/src/server/client/RunList.tsx +3 -3
  53. package/src/server/client/SubmitModal.tsx +58 -10
  54. package/src/server/client/SummaryBar.tsx +1 -1
  55. package/src/server/client/icons.tsx +17 -0
  56. package/src/server/client/utils/code-highlight.ts +422 -0
  57. package/src/server/client/utils/constants.ts +226 -0
  58. package/src/server/client/utils/json-highlight.ts +55 -0
  59. package/src/server/client/utils/markdown.ts +319 -0
  60. package/src/server/client/{types.ts → utils/types.ts} +81 -0
  61. package/src/server/index.ts +154 -16
  62. package/src/server/run-repository.ts +153 -136
  63. package/src/skill/OUTPUT-FORMAT.md +6 -4
  64. package/src/skill/README.md +22 -14
  65. package/src/skill/SETUP.md +4 -4
  66. package/src/skill/example-review.md +22 -14
  67. package/src/skill/fix.md +5 -5
  68. package/src/skill/hunt.md +55 -32
  69. package/src/skill/train.md +1 -1
  70. package/src/skill/verify.md +7 -6
  71. package/src/tests/__snapshots__/baseline.test.ts.snap +74 -61
  72. package/src/tests/baseline.test.ts +79 -34
  73. package/src/tests/cli.test.ts +40 -19
  74. package/src/tests/code-highlight.test.ts +123 -0
  75. package/src/tests/domain.test.ts +1 -1
  76. package/src/tests/findings-commands.test.ts +153 -131
  77. package/src/tests/findings-state.test.ts +56 -5
  78. package/src/tests/fixtures/fake-project.ts +0 -1
  79. package/src/tests/github-review.test.ts +409 -60
  80. package/src/tests/infrastructure.test.ts +33 -38
  81. package/src/tests/markdown.test.ts +149 -0
  82. package/src/tests/review-resume.test.ts +101 -0
  83. package/src/tests/review-scope.test.ts +302 -0
  84. package/src/tests/review-scripts.test.ts +81 -5
  85. package/src/tests/run-status-view.test.ts +94 -0
  86. package/src/tests/run-status.test.ts +86 -23
  87. package/src/tests/server.test.ts +184 -96
  88. package/src/tests/storage.test.ts +449 -0
  89. package/src/server/client/constants.ts +0 -91
  90. /package/src/server/client/{api.ts → utils/api.ts} +0 -0
  91. /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 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",
@@ -122,8 +122,13 @@ export function SubmitModal({
122
122
  () => selected.filter((candidate) => includeSent || !candidate.sent),
123
123
  [selected, includeSent],
124
124
  );
125
- const onLine = toSend.filter((candidate) => candidate.kind === "line").length;
126
- const onFile = toSend.filter((candidate) => candidate.kind === "file").length;
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>{onLine}</b> na linha · <b>{onFile}</b> no
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
- <ExternalLink size={12} /> abrir
450
+ <GitHub size={12} /> abrir
403
451
  </a>
404
452
  </p>
405
453
  )}
@@ -3,7 +3,7 @@
3
3
  * progresso da correção (feitos ÷ total).
4
4
  */
5
5
 
6
- import type { Summary } from "./types";
6
+ import type { Summary } from "./utils/types";
7
7
 
8
8
  function Kpi({
9
9
  label,
@@ -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}>