@payconductor/houndsman 0.4.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 +12 -38
- package/package.json +1 -1
- package/src/cli/commands/status-command.ts +46 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +3 -0
- package/src/core/commands/findings.ts +61 -5
- package/src/core/commands/status.ts +63 -0
- package/src/core/engine/review-scope.ts +26 -8
- package/src/core/findings-state.ts +21 -4
- package/src/core/github-review.ts +620 -57
- package/src/core/orchestrator.ts +98 -8
- package/src/core/paths.ts +3 -0
- package/src/core/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/run-status.ts +39 -5
- package/src/core/storage/file/file-storage.ts +2 -0
- package/src/core/storage/sqlite/mappers.ts +2 -0
- package/src/core/storage/sqlite/schema.ts +9 -1
- package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
- package/src/core/storage/types.ts +9 -0
- package/src/server/client/App.tsx +26 -9
- package/src/server/client/ArtifactsPanel.tsx +12 -5
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +14 -10
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +92 -12
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +49 -6
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/{constants.ts → utils/constants.ts} +34 -5
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +58 -0
- package/src/server/index.ts +63 -5
- package/src/server/run-repository.ts +9 -0
- package/src/skill/example-review.md +22 -14
- package/src/skill/hunt.md +31 -17
- package/src/skill/verify.md +2 -1
- package/src/tests/__snapshots__/baseline.test.ts.snap +32 -30
- package/src/tests/baseline.test.ts +3 -0
- package/src/tests/code-highlight.test.ts +1 -1
- package/src/tests/findings-state.test.ts +50 -2
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/markdown.test.ts +61 -1
- 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 +73 -0
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +53 -0
- package/src/tests/server.test.ts +76 -21
- package/src/tests/storage.test.ts +63 -0
- package/src/server/client/markdown.ts +0 -153
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
- /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
|
@@ -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",
|
|
@@ -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
|
-
<
|
|
450
|
+
<GitHub size={12} /> abrir
|
|
408
451
|
</a>
|
|
409
452
|
</p>
|
|
410
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}>
|
|
@@ -39,15 +39,22 @@ export const RUN_STATUS_LABELS: Record<RunStatus, string> = {
|
|
|
39
39
|
fix_done: "Correção pronta",
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
/** Limiar de inatividade para exibir timeout (display-only
|
|
42
|
+
/** Limiar de inatividade da correção para exibir timeout (display-only). */
|
|
43
43
|
export const STATUS_TIMEOUT_MS = 10 * 60 * 1000;
|
|
44
44
|
|
|
45
|
+
/** Revisão despacha subagentes em ondas longas — janela maior que a correção. */
|
|
46
|
+
export const REVIEW_TIMEOUT_MS = 30 * 60 * 1000;
|
|
47
|
+
|
|
45
48
|
/** Intervalo do polling da UI (o badge conta o tempo desde a última sync). */
|
|
46
49
|
export const AUTO_REFRESH_MS = 10_000;
|
|
47
50
|
|
|
48
51
|
export type RunStatusView = { kind: string; label: string; timeout: boolean };
|
|
49
52
|
|
|
50
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* View do status de uma run — timeout só existe quando há um heartbeat de
|
|
55
|
+
* execução (`reviewing`/`fixing`). `updatedAt` muda com triagem/sync e não
|
|
56
|
+
* serve para provar que a skill está rodando.
|
|
57
|
+
*/
|
|
51
58
|
export function describeRunStatus(
|
|
52
59
|
run: RunSummary,
|
|
53
60
|
now: number = Date.now(),
|
|
@@ -55,8 +62,16 @@ export function describeRunStatus(
|
|
|
55
62
|
const status = run.status;
|
|
56
63
|
if (!status) return null;
|
|
57
64
|
const active = status === "reviewing" || status === "fixing";
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
if (!active) {
|
|
66
|
+
return { kind: status, label: RUN_STATUS_LABELS[status], timeout: false };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Sem heartbeat não há execução conhecida: evita o falso "timeout".
|
|
70
|
+
const heartbeat = run.heartbeatAt ? Date.parse(run.heartbeatAt) : Number.NaN;
|
|
71
|
+
if (!Number.isFinite(heartbeat)) return null;
|
|
72
|
+
|
|
73
|
+
const timeoutMs = status === "reviewing" ? REVIEW_TIMEOUT_MS : STATUS_TIMEOUT_MS;
|
|
74
|
+
if (now - heartbeat > timeoutMs) {
|
|
60
75
|
return {
|
|
61
76
|
kind: "timeout",
|
|
62
77
|
label: status === "reviewing" ? "Timeout na revisão" : "Timeout na correção",
|
|
@@ -69,6 +84,7 @@ export function describeRunStatus(
|
|
|
69
84
|
/** Ordem canônica dos tipos de artefato na lista da UI. */
|
|
70
85
|
const KIND_ORDER: ArtifactKind[] = [
|
|
71
86
|
"review",
|
|
87
|
+
"resume",
|
|
72
88
|
"domain-review",
|
|
73
89
|
"verify",
|
|
74
90
|
"brief",
|
|
@@ -86,6 +102,7 @@ const KIND_ORDER: ArtifactKind[] = [
|
|
|
86
102
|
const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: number }> =
|
|
87
103
|
{
|
|
88
104
|
review: { key: "report", label: "Relatórios", order: 0 },
|
|
105
|
+
resume: { key: "report", label: "Relatórios", order: 0 },
|
|
89
106
|
"domain-review": { key: "report", label: "Relatórios", order: 0 },
|
|
90
107
|
verify: { key: "verify", label: "Verificação", order: 1 },
|
|
91
108
|
brief: { key: "brief", label: "Briefs (auditoria)", order: 2 },
|
|
@@ -102,6 +119,7 @@ const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: n
|
|
|
102
119
|
/** Rótulo curto do tipo (badge do item). */
|
|
103
120
|
export const ARTIFACT_KIND_LABELS: Record<ArtifactKind, string> = {
|
|
104
121
|
review: "relatório",
|
|
122
|
+
resume: "resumo",
|
|
105
123
|
"domain-review": "domínio",
|
|
106
124
|
verify: "verify",
|
|
107
125
|
brief: "brief",
|
|
@@ -127,6 +145,8 @@ export function describeArtifact(meta: ArtifactMeta): string {
|
|
|
127
145
|
switch (meta.kind) {
|
|
128
146
|
case "review":
|
|
129
147
|
return "Relatório consolidado da revisão — o entregável.";
|
|
148
|
+
case "resume":
|
|
149
|
+
return "Resumo final da revisão: visão de negócio e pontos técnicos diretos (leitura humana).";
|
|
130
150
|
case "domain-review":
|
|
131
151
|
return "Relatório isolado do domínio: achados daquele watchdog.";
|
|
132
152
|
case "verify":
|
|
@@ -156,6 +176,14 @@ export function isJsonArtifact(meta: ArtifactMeta | undefined): boolean {
|
|
|
156
176
|
return meta.contentType.includes("json") || meta.name.toLowerCase().endsWith(".json");
|
|
157
177
|
}
|
|
158
178
|
|
|
179
|
+
/** `true` se o conteúdo deve ser renderizado como markdown. */
|
|
180
|
+
export function isMarkdownArtifact(meta: ArtifactMeta | undefined): boolean {
|
|
181
|
+
if (!meta) return false;
|
|
182
|
+
return (
|
|
183
|
+
meta.contentType.includes("markdown") || meta.name.toLowerCase().endsWith(".md")
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
159
187
|
/** Agrupa os artefatos para a navegação lateral (ordem estável). */
|
|
160
188
|
export function groupArtifacts(
|
|
161
189
|
items: ArtifactMeta[],
|
|
@@ -190,8 +218,9 @@ export function groupArtifacts(
|
|
|
190
218
|
|
|
191
219
|
/** Legenda única de cada aba (explica o que a aba é; mostrada quando selecionada). */
|
|
192
220
|
export const TAB_CAPTIONS = {
|
|
221
|
+
resume: "Resumo principal da revisão: o que muda em termos de negócio, números diretos (arquivos, churn, domínios) e os pontos técnicos que exigem ação.",
|
|
193
222
|
findings:
|
|
194
223
|
"Triagem dos achados da revisão: decida Executar, Ignorar ou deixe Pendente — o comentário orienta o auto-fix. Agente: houndsman findings list --decision pending --json · findings done <id>.",
|
|
195
224
|
artifacts:
|
|
196
|
-
"Artefatos persistidos da run: relatórios, briefs de auditoria, manifesto, classificação, Jev, contexto da PR e verificação. JSON com destaque de sintaxe.",
|
|
225
|
+
"Artefatos persistidos da run: relatórios, briefs de auditoria, manifesto, classificação, Jev, contexto da PR e verificação. JSON com destaque de sintaxe; markdown renderizado com highlight de código.",
|
|
197
226
|
} as const;
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown mínimo do cliente (campo `action` do finding e artefatos).
|
|
3
|
+
*
|
|
4
|
+
* Suporta o suficiente para instruções curtas e relatórios: parágrafos,
|
|
5
|
+
* quebras de linha, listas, blockquote, títulos, tabelas, blocos de código e
|
|
6
|
+
* formatação inline (`code`, **bold**, *itálico*, links). Sem lib externa e
|
|
7
|
+
* sem HTML cru.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Alinhamento de coluna em tabela markdown. */
|
|
11
|
+
export type MarkdownAlign = "left" | "center" | "right";
|
|
12
|
+
|
|
13
|
+
export type MarkdownBlock =
|
|
14
|
+
| { type: "paragraph"; text: string }
|
|
15
|
+
| { type: "list"; ordered: boolean; items: string[] }
|
|
16
|
+
| { type: "code"; language: string | null; code: string }
|
|
17
|
+
| { type: "quote"; text: string }
|
|
18
|
+
| { type: "heading"; level: number; text: string }
|
|
19
|
+
| {
|
|
20
|
+
type: "table";
|
|
21
|
+
align: (MarkdownAlign | undefined)[];
|
|
22
|
+
header: string[];
|
|
23
|
+
rows: string[][];
|
|
24
|
+
}
|
|
25
|
+
| { type: "hr" };
|
|
26
|
+
|
|
27
|
+
export type InlineToken =
|
|
28
|
+
| { type: "text"; text: string }
|
|
29
|
+
| { type: "code"; text: string }
|
|
30
|
+
| { type: "strong"; text: string }
|
|
31
|
+
| { type: "em"; text: string }
|
|
32
|
+
| { type: "link"; text: string; url: string }
|
|
33
|
+
| { type: "path"; text: string };
|
|
34
|
+
|
|
35
|
+
const FENCE = /^\s*```(\S*)\s*$/;
|
|
36
|
+
const BULLET = /^\s*[-*+]\s+(.*)$/;
|
|
37
|
+
const ORDERED = /^\s*\d+\.\s+(.*)$/;
|
|
38
|
+
const QUOTE = /^\s*>\s?(.*)$/;
|
|
39
|
+
const HEADING = /^(#{1,6})\s+(.*)$/;
|
|
40
|
+
const HR = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
|
|
41
|
+
const TABLE_SEP = /^\s*\|?\s*:?-{2,}:?\s*(?:\|\s*:?-{2,}:?\s*)+\|?\s*$/;
|
|
42
|
+
|
|
43
|
+
function isBlockStart(line: string): boolean {
|
|
44
|
+
return (
|
|
45
|
+
FENCE.test(line) ||
|
|
46
|
+
HEADING.test(line) ||
|
|
47
|
+
HR.test(line) ||
|
|
48
|
+
BULLET.test(line) ||
|
|
49
|
+
ORDERED.test(line) ||
|
|
50
|
+
QUOTE.test(line)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Quebra uma linha de tabela em células (remove os pipes das pontas). */
|
|
55
|
+
function splitTableRow(line: string): string[] {
|
|
56
|
+
return line
|
|
57
|
+
.trim()
|
|
58
|
+
.replace(/^\|/, "")
|
|
59
|
+
.replace(/\|$/, "")
|
|
60
|
+
.split("|")
|
|
61
|
+
.map((cell) => cell.trim());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `true` quando há um cabeçalho seguido da linha separadora de tabela. */
|
|
65
|
+
function isTableStart(lines: string[], index: number): boolean {
|
|
66
|
+
if (index + 1 >= lines.length) return false;
|
|
67
|
+
if (!lines[index].includes("|")) return false;
|
|
68
|
+
return TABLE_SEP.test(lines[index + 1]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Alinhamentos declarados na linha separadora (`:---`, `:---:`, `---:`). */
|
|
72
|
+
function parseAlignments(line: string): (MarkdownAlign | undefined)[] {
|
|
73
|
+
return splitTableRow(line).map((cell) => {
|
|
74
|
+
const left = cell.startsWith(":");
|
|
75
|
+
const right = cell.endsWith(":");
|
|
76
|
+
if (left && right) return "center";
|
|
77
|
+
if (right) return "right";
|
|
78
|
+
if (left) return "left";
|
|
79
|
+
return undefined;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Célula numérica ou razão (`12`, `3/4`) — colunas assim vão para a direita. */
|
|
84
|
+
const NUMERIC_CELL = /^[\d\s/]+$/;
|
|
85
|
+
|
|
86
|
+
/** Resolve o alinhamento: explícito > coluna só de números/barra > padrão. */
|
|
87
|
+
function resolveAlignments(
|
|
88
|
+
align: (MarkdownAlign | undefined)[],
|
|
89
|
+
rows: string[][],
|
|
90
|
+
): (MarkdownAlign | undefined)[] {
|
|
91
|
+
return align.map((value, column) => {
|
|
92
|
+
if (value) return value;
|
|
93
|
+
let filled = 0;
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
const cell = (row[column] ?? "").trim();
|
|
96
|
+
if (cell.length === 0) continue;
|
|
97
|
+
if (!NUMERIC_CELL.test(cell) || !/\d/.test(cell)) return undefined;
|
|
98
|
+
filled++;
|
|
99
|
+
}
|
|
100
|
+
return filled > 0 ? "right" : undefined;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Divide o texto em blocos simples (sem aninhamento profundo). */
|
|
105
|
+
export function parseMarkdown(text: string): MarkdownBlock[] {
|
|
106
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
107
|
+
const blocks: MarkdownBlock[] = [];
|
|
108
|
+
let index = 0;
|
|
109
|
+
|
|
110
|
+
while (index < lines.length) {
|
|
111
|
+
const line = lines[index];
|
|
112
|
+
if (line.trim().length === 0) {
|
|
113
|
+
index++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const fence = FENCE.exec(line);
|
|
118
|
+
if (fence) {
|
|
119
|
+
const language = fence[1] || null;
|
|
120
|
+
const code: string[] = [];
|
|
121
|
+
index++;
|
|
122
|
+
while (index < lines.length && !FENCE.test(lines[index])) {
|
|
123
|
+
code.push(lines[index]);
|
|
124
|
+
index++;
|
|
125
|
+
}
|
|
126
|
+
if (index < lines.length) index++;
|
|
127
|
+
blocks.push({ type: "code", language, code: code.join("\n") });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const heading = HEADING.exec(line);
|
|
132
|
+
if (heading) {
|
|
133
|
+
blocks.push({
|
|
134
|
+
type: "heading",
|
|
135
|
+
level: heading[1].length,
|
|
136
|
+
text: heading[2].trim(),
|
|
137
|
+
});
|
|
138
|
+
index++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (HR.test(line)) {
|
|
143
|
+
blocks.push({ type: "hr" });
|
|
144
|
+
index++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isTableStart(lines, index)) {
|
|
149
|
+
const header = splitTableRow(line);
|
|
150
|
+
const align = parseAlignments(lines[index + 1]);
|
|
151
|
+
const rows: string[][] = [];
|
|
152
|
+
index += 2;
|
|
153
|
+
while (
|
|
154
|
+
index < lines.length &&
|
|
155
|
+
lines[index].trim().length > 0 &&
|
|
156
|
+
lines[index].includes("|")
|
|
157
|
+
) {
|
|
158
|
+
rows.push(splitTableRow(lines[index]));
|
|
159
|
+
index++;
|
|
160
|
+
}
|
|
161
|
+
blocks.push({
|
|
162
|
+
type: "table",
|
|
163
|
+
align: resolveAlignments(align, rows),
|
|
164
|
+
header,
|
|
165
|
+
rows,
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (BULLET.test(line)) {
|
|
171
|
+
const items: string[] = [];
|
|
172
|
+
while (index < lines.length) {
|
|
173
|
+
const item = BULLET.exec(lines[index]);
|
|
174
|
+
if (!item) break;
|
|
175
|
+
items.push(item[1]);
|
|
176
|
+
index++;
|
|
177
|
+
}
|
|
178
|
+
blocks.push({ type: "list", ordered: false, items });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (ORDERED.test(line)) {
|
|
183
|
+
const items: string[] = [];
|
|
184
|
+
while (index < lines.length) {
|
|
185
|
+
const item = ORDERED.exec(lines[index]);
|
|
186
|
+
if (!item) break;
|
|
187
|
+
items.push(item[1]);
|
|
188
|
+
index++;
|
|
189
|
+
}
|
|
190
|
+
blocks.push({ type: "list", ordered: true, items });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (QUOTE.test(line)) {
|
|
195
|
+
const quote: string[] = [];
|
|
196
|
+
while (index < lines.length) {
|
|
197
|
+
const item = QUOTE.exec(lines[index]);
|
|
198
|
+
if (!item) break;
|
|
199
|
+
quote.push(item[1]);
|
|
200
|
+
index++;
|
|
201
|
+
}
|
|
202
|
+
blocks.push({ type: "quote", text: quote.join("\n") });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const paragraph: string[] = [line];
|
|
207
|
+
index++;
|
|
208
|
+
while (
|
|
209
|
+
index < lines.length &&
|
|
210
|
+
lines[index].trim().length > 0 &&
|
|
211
|
+
!isBlockStart(lines[index]) &&
|
|
212
|
+
!isTableStart(lines, index)
|
|
213
|
+
) {
|
|
214
|
+
paragraph.push(lines[index]);
|
|
215
|
+
index++;
|
|
216
|
+
}
|
|
217
|
+
blocks.push({ type: "paragraph", text: paragraph.join("\n") });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return blocks;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Blocos de uma seção `## <título>` (sem o heading); vazio se ausente. */
|
|
224
|
+
export function sectionBlocks(text: string, title: string): MarkdownBlock[] {
|
|
225
|
+
const wanted = title.trim().toLowerCase();
|
|
226
|
+
const blocks: MarkdownBlock[] = [];
|
|
227
|
+
let inside = false;
|
|
228
|
+
for (const block of parseMarkdown(text)) {
|
|
229
|
+
if (block.type === "heading") {
|
|
230
|
+
if (inside) break;
|
|
231
|
+
inside = block.level >= 2 && block.text.trim().toLowerCase() === wanted;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (inside) blocks.push(block);
|
|
235
|
+
}
|
|
236
|
+
return blocks;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Extensão reconhecida como arquivo (evita destacar texto comum). */
|
|
240
|
+
const FILE_EXTENSION =
|
|
241
|
+
/\.(?:[cm]?[jt]sx?|json|md|ya?ml|toml|css|scss|html?|py|go|rs|java|kt|cs|php|rb|sh|sql|txt|lock)$/i;
|
|
242
|
+
|
|
243
|
+
/** Caminho/arquivo dentro de um texto corrido (`src/foo.ts`, `review.md`). */
|
|
244
|
+
const PATH_TOKEN =
|
|
245
|
+
/(?:[\w.-]+[\\/])*[\w-]+\.(?:[cm]?[jt]sx?|json|md|ya?ml|toml|css|scss|html?|py|go|rs|java|kt|cs|php|rb|sh|sql|txt|lock)\b/g;
|
|
246
|
+
|
|
247
|
+
/** `true` se o texto parece um caminho/arquivo (sem espaços e com extensão). */
|
|
248
|
+
export function isFilePath(value: string): boolean {
|
|
249
|
+
const text = value.trim();
|
|
250
|
+
if (!text || /\s/.test(text)) return false;
|
|
251
|
+
if (/^(?:https?:|mailto:|#)/i.test(text)) return false;
|
|
252
|
+
const base = text.split(/[\\/]/).pop() ?? text;
|
|
253
|
+
if (FILE_EXTENSION.test(base)) return true;
|
|
254
|
+
return (
|
|
255
|
+
text.startsWith("./") ||
|
|
256
|
+
text.startsWith("../") ||
|
|
257
|
+
text.startsWith("/") ||
|
|
258
|
+
/^[A-Za-z]:[\\/]/.test(text)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Quebra texto corrido em tokens, destacando caminhos de arquivo. */
|
|
263
|
+
function pushTextTokens(tokens: InlineToken[], text: string): void {
|
|
264
|
+
let last = 0;
|
|
265
|
+
for (const match of text.matchAll(PATH_TOKEN)) {
|
|
266
|
+
const index = match.index ?? 0;
|
|
267
|
+
if (index > last) {
|
|
268
|
+
tokens.push({ type: "text", text: text.slice(last, index) });
|
|
269
|
+
}
|
|
270
|
+
tokens.push({ type: "path", text: match[0] });
|
|
271
|
+
last = index + match[0].length;
|
|
272
|
+
}
|
|
273
|
+
if (last < text.length) {
|
|
274
|
+
tokens.push({ type: "text", text: text.slice(last) });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Tokeniza formatação inline preservando o texto e a ordem dos marcadores. */
|
|
279
|
+
export function parseInline(text: string): InlineToken[] {
|
|
280
|
+
const tokens: InlineToken[] = [];
|
|
281
|
+
// Ordem importa: code/bold antes de itálico; `_` exige fronteira de palavra.
|
|
282
|
+
const pattern =
|
|
283
|
+
/`[^`\n]+`|\*\*[^*\n]+\*\*|__[^_\n]+__|\*[^*\n]+\*|(?<![\w])_[^_\n]+_(?![\w])|\[[^\]\n]+\]\([^)\s]+\)/g;
|
|
284
|
+
let last = 0;
|
|
285
|
+
for (const match of text.matchAll(pattern)) {
|
|
286
|
+
const start = match.index ?? 0;
|
|
287
|
+
if (start > last) {
|
|
288
|
+
pushTextTokens(tokens, text.slice(last, start));
|
|
289
|
+
}
|
|
290
|
+
const raw = match[0];
|
|
291
|
+
if (raw.startsWith("`")) {
|
|
292
|
+
tokens.push({ type: "code", text: raw.slice(1, -1) });
|
|
293
|
+
} else if (raw.startsWith("**") || raw.startsWith("__")) {
|
|
294
|
+
tokens.push({ type: "strong", text: raw.slice(2, -2) });
|
|
295
|
+
} else if (raw.startsWith("*") || raw.startsWith("_")) {
|
|
296
|
+
tokens.push({ type: "em", text: raw.slice(1, -1) });
|
|
297
|
+
} else {
|
|
298
|
+
const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(raw);
|
|
299
|
+
if (link) {
|
|
300
|
+
tokens.push({ type: "link", text: link[1], url: link[2] });
|
|
301
|
+
} else {
|
|
302
|
+
pushTextTokens(tokens, raw);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
last = start + raw.length;
|
|
306
|
+
}
|
|
307
|
+
if (last < text.length) {
|
|
308
|
+
pushTextTokens(tokens, text.slice(last));
|
|
309
|
+
}
|
|
310
|
+
return tokens;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** URL segura para links de markdown (evita `javascript:` e afins). */
|
|
314
|
+
export function safeUrl(url: string): string | null {
|
|
315
|
+
const value = url.trim();
|
|
316
|
+
if (/^(https?:|mailto:)/i.test(value)) return value;
|
|
317
|
+
if (value.startsWith("/") && !value.startsWith("//")) return value;
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
@@ -61,6 +61,7 @@ export type ArtifactKind =
|
|
|
61
61
|
| "context-summary"
|
|
62
62
|
| "finding-report"
|
|
63
63
|
| "review"
|
|
64
|
+
| "resume"
|
|
64
65
|
| "domain-review"
|
|
65
66
|
| "verify"
|
|
66
67
|
| "other";
|
|
@@ -77,6 +78,9 @@ export type RunSummary = {
|
|
|
77
78
|
slug: string;
|
|
78
79
|
branch: string;
|
|
79
80
|
updatedAt?: string;
|
|
81
|
+
startedAt?: string;
|
|
82
|
+
/** Último sinal de vida de uma execução ativa (revisão/correção). */
|
|
83
|
+
heartbeatAt?: string;
|
|
80
84
|
status?: RunStatus | null;
|
|
81
85
|
summary: Summary;
|
|
82
86
|
};
|
|
@@ -87,6 +91,52 @@ export type FindingsResponse = {
|
|
|
87
91
|
summary: Summary;
|
|
88
92
|
};
|
|
89
93
|
|
|
94
|
+
/** Manifesto da run (subset consumido pela aba de resumo). */
|
|
95
|
+
export type RunManifest = {
|
|
96
|
+
branch?: string;
|
|
97
|
+
base?: string;
|
|
98
|
+
head?: string;
|
|
99
|
+
generatedAt?: string;
|
|
100
|
+
churn?: { additions: number; deletions: number };
|
|
101
|
+
watchdogs?: {
|
|
102
|
+
id: string;
|
|
103
|
+
name: string;
|
|
104
|
+
files: string[];
|
|
105
|
+
overflow: number;
|
|
106
|
+
status: "pending" | "done";
|
|
107
|
+
}[];
|
|
108
|
+
context?: {
|
|
109
|
+
summarized: boolean;
|
|
110
|
+
source?: {
|
|
111
|
+
number?: number;
|
|
112
|
+
title?: string;
|
|
113
|
+
url?: string;
|
|
114
|
+
base?: string;
|
|
115
|
+
head?: string;
|
|
116
|
+
} | null;
|
|
117
|
+
};
|
|
118
|
+
excludedCount?: number;
|
|
119
|
+
deleted?: string[];
|
|
120
|
+
unclassified?: string[];
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Detalhe da run (`GET /api/runs/:slug`). */
|
|
124
|
+
export type RunDetail = {
|
|
125
|
+
slug: string;
|
|
126
|
+
branch: string;
|
|
127
|
+
manifest: RunManifest | null;
|
|
128
|
+
watchdogs: string[];
|
|
129
|
+
unclassified: string[];
|
|
130
|
+
deleted: string[];
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** `classification.json` — só o necessário para os cards do resumo. */
|
|
134
|
+
export type ClassificationArtifact = {
|
|
135
|
+
files?: Record<string, { status: string; oldPath?: string }>;
|
|
136
|
+
churn?: Record<string, { additions: number; deletions: number }>;
|
|
137
|
+
watchdogs?: Record<string, string[]>;
|
|
138
|
+
};
|
|
139
|
+
|
|
90
140
|
/** Candidato ao envio da revisão (espelha `core/github-review.ts`). */
|
|
91
141
|
export type SubmitCandidate = {
|
|
92
142
|
id: string;
|
|
@@ -134,6 +184,14 @@ export type SubmitOutcome = {
|
|
|
134
184
|
failed: { id: string; reason: string }[];
|
|
135
185
|
skipped: { id: string; reason: string }[];
|
|
136
186
|
general?: { commentId: number; url?: string; ids: string[] };
|
|
187
|
+
review?: {
|
|
188
|
+
id?: number;
|
|
189
|
+
url?: string;
|
|
190
|
+
created: boolean;
|
|
191
|
+
finalized: boolean;
|
|
192
|
+
event?: "COMMENT" | "REQUEST_CHANGES";
|
|
193
|
+
error?: string;
|
|
194
|
+
};
|
|
137
195
|
};
|
|
138
196
|
|
|
139
197
|
export type SubmitResponse = {
|