@payconductor/houndsman 0.1.0 → 0.1.3

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 (61) hide show
  1. package/README.md +68 -89
  2. package/assets/logo-legacy.png +0 -0
  3. package/assets/logo.png +0 -0
  4. package/package.json +3 -2
  5. package/src/cli/index.ts +101 -10
  6. package/src/core/assets.ts +11 -0
  7. package/src/core/brief.ts +27 -5
  8. package/src/core/commands/findings.ts +618 -0
  9. package/src/core/commands/review.ts +2 -0
  10. package/src/core/commands/scaffold.ts +33 -44
  11. package/src/core/config.ts +42 -0
  12. package/src/core/diff-source.ts +140 -0
  13. package/src/core/file-classifier.ts +50 -2
  14. package/src/core/finding-anchor.ts +147 -0
  15. package/src/core/findings-state.ts +556 -0
  16. package/src/core/fix-batch.ts +236 -0
  17. package/src/core/git-revision.ts +105 -0
  18. package/src/core/jev/index.ts +1 -0
  19. package/src/core/jev/jev-client.ts +72 -11
  20. package/src/core/jev/jev-detector.ts +21 -19
  21. package/src/core/jev/jev-verify.ts +178 -0
  22. package/src/core/orchestrator.ts +362 -23
  23. package/src/core/paths.ts +9 -0
  24. package/src/core/registry-check.ts +15 -0
  25. package/src/core/report-builder.ts +118 -9
  26. package/src/core/run-status.ts +123 -0
  27. package/src/core/types.ts +10 -0
  28. package/src/core/verify.ts +175 -0
  29. package/src/core/watchdog-registry.ts +1 -2
  30. package/src/index.ts +78 -2
  31. package/src/server/artifacts.ts +67 -0
  32. package/src/server/client/App.tsx +158 -0
  33. package/src/server/client/ArtifactsPanel.tsx +48 -0
  34. package/src/server/client/Filters.tsx +83 -0
  35. package/src/server/client/FindingCard.tsx +122 -0
  36. package/src/server/client/FindingsList.tsx +54 -0
  37. package/src/server/client/RunList.tsx +52 -0
  38. package/src/server/client/SummaryBar.tsx +21 -0
  39. package/src/server/client/api.ts +37 -0
  40. package/src/server/client/constants.ts +94 -0
  41. package/src/server/client/index.tsx +12 -0
  42. package/src/server/client/types.ts +52 -0
  43. package/src/server/client-build.ts +47 -0
  44. package/src/server/index.ts +376 -0
  45. package/src/skill/AUDIT-PROTOCOL.md +2 -2
  46. package/src/skill/README.md +43 -11
  47. package/src/skill/SETUP.md +16 -4
  48. package/src/skill/SKILL.md +48 -0
  49. package/src/skill/fix.md +206 -0
  50. package/src/skill/{houndsman.md → hunt.md} +83 -33
  51. package/src/skill/{houndsman-update.md → train.md} +12 -12
  52. package/src/skill/verify.md +70 -0
  53. package/src/tests/findings-anchor.test.ts +120 -0
  54. package/src/tests/findings-commands.test.ts +476 -0
  55. package/src/tests/findings-state.test.ts +325 -0
  56. package/src/tests/fix-batch.test.ts +257 -0
  57. package/src/tests/review-scripts.test.ts +299 -0
  58. package/src/tests/run-status.test.ts +147 -0
  59. package/src/tests/scaffold.test.ts +59 -0
  60. package/src/tests/server.test.ts +242 -0
  61. package/src/tests/verify.test.ts +170 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * App: layout do servidor — sidebar de runs, abas (Findings/Artefatos), legenda
3
+ * da aba ativa e os painéis. Concentra o estado e as chamadas à API.
4
+ */
5
+
6
+ import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
7
+ import { ArtifactsPanel } from "./ArtifactsPanel";
8
+ import { api, computeSummary } from "./api";
9
+ import { TAB_CAPTIONS } from "./constants";
10
+ import { Filters, type FiltersState } from "./Filters";
11
+ import { FindingsList } from "./FindingsList";
12
+ import { RunList } from "./RunList";
13
+ import { SummaryBar } from "./SummaryBar";
14
+ import type { Finding, FindingsResponse, RunSummary } from "./types";
15
+
16
+ type Tab = "findings" | "artifacts";
17
+
18
+ export function App() {
19
+ const [runs, setRuns] = useState<RunSummary[]>([]);
20
+ const [slug, setSlug] = useState<string | null>(null);
21
+ const [data, setData] = useState<FindingsResponse | null>(null);
22
+ const [tab, setTab] = useState<Tab>("findings");
23
+ const [filters, setFilters] = useState<FiltersState>({
24
+ query: "",
25
+ decision: "all",
26
+ severity: "all",
27
+ showAbsent: false,
28
+ });
29
+ const [error, setError] = useState("");
30
+
31
+ const reloadRuns = useCallback(async () => {
32
+ try {
33
+ const { runs: list } = await api<{ runs: RunSummary[] }>("/api/runs");
34
+ setRuns(list);
35
+ setSlug((current) => current ?? list[0]?.slug ?? null);
36
+ } catch (err) {
37
+ setError(String(err));
38
+ }
39
+ }, []);
40
+
41
+ const loadFindings = useCallback(async (target: string) => {
42
+ try {
43
+ setData(await api<FindingsResponse>(`/api/runs/${target}/findings?all=1`));
44
+ setError("");
45
+ } catch (err) {
46
+ setError(String(err));
47
+ }
48
+ }, []);
49
+
50
+ useEffect(() => {
51
+ reloadRuns();
52
+ }, [reloadRuns]);
53
+
54
+ useEffect(() => {
55
+ if (slug) loadFindings(slug);
56
+ }, [slug, loadFindings]);
57
+
58
+ const onUpdated = useCallback(
59
+ (updated: Finding) => {
60
+ setData((current) =>
61
+ current
62
+ ? {
63
+ ...current,
64
+ findings: current.findings.map((finding) =>
65
+ finding.id === updated.id ? updated : finding,
66
+ ),
67
+ }
68
+ : current,
69
+ );
70
+ reloadRuns();
71
+ },
72
+ [reloadRuns],
73
+ );
74
+
75
+ const sync = useCallback(async () => {
76
+ if (!slug) return;
77
+ await api(`/api/runs/${slug}/sync`, { method: "POST" });
78
+ await loadFindings(slug);
79
+ await reloadRuns();
80
+ }, [slug, loadFindings, reloadRuns]);
81
+
82
+ const findings = useMemo(() => {
83
+ const needle = filters.query.trim().toLowerCase();
84
+ return (data?.findings ?? [])
85
+ .filter((finding) => filters.showAbsent || finding.present)
86
+ .filter(
87
+ (finding) =>
88
+ filters.decision === "all" || finding.decision === filters.decision,
89
+ )
90
+ .filter(
91
+ (finding) =>
92
+ filters.severity === "all" || finding.severity === filters.severity,
93
+ )
94
+ .filter(
95
+ (finding) =>
96
+ !needle ||
97
+ finding.file.toLowerCase().includes(needle) ||
98
+ finding.rule.toLowerCase().includes(needle) ||
99
+ finding.message.toLowerCase().includes(needle),
100
+ );
101
+ }, [data, filters]);
102
+
103
+ const summary = useMemo(() => computeSummary(data?.findings ?? []), [data]);
104
+
105
+ return (
106
+ <div class="app">
107
+ <RunList runs={runs} slug={slug} onSelect={setSlug} />
108
+ <main class="main">
109
+ <header class="topbar">
110
+ <div class="tabs">
111
+ <button
112
+ type="button"
113
+ title="Achados da revisão: triagem e decisões por finding"
114
+ class={`btn tab${tab === "findings" ? " active" : ""}`}
115
+ onClick={() => setTab("findings")}
116
+ >
117
+ Findings
118
+ </button>
119
+ <button
120
+ type="button"
121
+ title="Arquivos brutos gerados pela revisão (manifesto, classificação, relatório…)"
122
+ class={`btn tab${tab === "artifacts" ? " active" : ""}`}
123
+ onClick={() => setTab("artifacts")}
124
+ >
125
+ Artefatos
126
+ </button>
127
+ </div>
128
+ {slug && (
129
+ <button
130
+ type="button"
131
+ title="Reancorar: recalcula a linha de cada finding lendo o arquivo atual — use após um auto-fix que deslocou as linhas"
132
+ class="btn sync"
133
+ onClick={sync}
134
+ >
135
+ ↻ Reancorar (sync)
136
+ </button>
137
+ )}
138
+ </header>
139
+
140
+ <p class="caption">{TAB_CAPTIONS[tab]}</p>
141
+ {error && <p class="error">{error}</p>}
142
+
143
+ {slug && tab === "findings" && data && (
144
+ <>
145
+ <SummaryBar summary={summary} />
146
+ <Filters value={filters} onChange={setFilters} />
147
+ <FindingsList
148
+ slug={slug}
149
+ findings={findings}
150
+ onUpdated={onUpdated}
151
+ />
152
+ </>
153
+ )}
154
+ {slug && tab === "artifacts" && <ArtifactsPanel slug={slug} />}
155
+ </main>
156
+ </div>
157
+ );
158
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Painel de artefatos: abas para ver o conteúdo bruto dos arquivos da run.
3
+ */
4
+
5
+ import { useEffect, useState } from "preact/hooks";
6
+ import { ARTIFACT_INFO, ARTIFACTS, type ArtifactName } from "./constants";
7
+
8
+ export function ArtifactsPanel({ slug }: { slug: string }) {
9
+ const [name, setName] = useState<ArtifactName>(ARTIFACTS[0]);
10
+ const [content, setContent] = useState("");
11
+ const [error, setError] = useState("");
12
+
13
+ useEffect(() => {
14
+ setError("");
15
+ fetch(`/api/runs/${slug}/artifacts/${name}`)
16
+ .then(async (res) => {
17
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
18
+ setContent(await res.text());
19
+ })
20
+ .catch((err) => {
21
+ setContent("");
22
+ setError(String(err));
23
+ });
24
+ }, [slug, name]);
25
+
26
+ return (
27
+ <div class="artifacts">
28
+ <div class="tabs">
29
+ {ARTIFACTS.map((artifact) => (
30
+ <button
31
+ type="button"
32
+ title={ARTIFACT_INFO[artifact]}
33
+ class={`btn tab${artifact === name ? " active" : ""}`}
34
+ onClick={() => setName(artifact)}
35
+ >
36
+ {artifact}
37
+ </button>
38
+ ))}
39
+ </div>
40
+ <p class="artifact-desc">{ARTIFACT_INFO[name]}</p>
41
+ {error ? (
42
+ <p class="error">{error}</p>
43
+ ) : (
44
+ <pre class="artifact-view">{content}</pre>
45
+ )}
46
+ </div>
47
+ );
48
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Barra de filtros dos findings: busca, decisão, severidade e "mostrar ausentes".
3
+ */
4
+
5
+ import { DECISION_LABELS, DECISIONS, SEVERITY_ORDER } from "./constants";
6
+ import type { Decision, Severity } from "./types";
7
+
8
+ export type FiltersState = {
9
+ query: string;
10
+ decision: "all" | Decision;
11
+ severity: "all" | Severity;
12
+ showAbsent: boolean;
13
+ };
14
+
15
+ export function Filters({
16
+ value,
17
+ onChange,
18
+ }: {
19
+ value: FiltersState;
20
+ onChange: (next: FiltersState) => void;
21
+ }) {
22
+ const patch = (part: Partial<FiltersState>) => onChange({ ...value, ...part });
23
+
24
+ return (
25
+ <div class="filters">
26
+ <input
27
+ title="Filtrar por arquivo, regra ou mensagem"
28
+ placeholder="Buscar arquivo, regra ou mensagem…"
29
+ value={value.query}
30
+ onInput={(event) =>
31
+ patch({ query: (event.target as HTMLInputElement).value })
32
+ }
33
+ />
34
+ <select
35
+ title="Filtrar por decisão de triagem"
36
+ value={value.decision}
37
+ onChange={(event) =>
38
+ patch({
39
+ decision: (event.target as HTMLSelectElement).value as
40
+ | "all"
41
+ | Decision,
42
+ })
43
+ }
44
+ >
45
+ <option value="all">Todas as decisões</option>
46
+ {DECISIONS.map((decision) => (
47
+ <option value={decision}>{DECISION_LABELS[decision]}</option>
48
+ ))}
49
+ </select>
50
+ <select
51
+ title="Filtrar por severidade"
52
+ value={value.severity}
53
+ onChange={(event) =>
54
+ patch({
55
+ severity: (event.target as HTMLSelectElement).value as
56
+ | "all"
57
+ | Severity,
58
+ })
59
+ }
60
+ >
61
+ <option value="all">Todas as severidades</option>
62
+ {SEVERITY_ORDER.map((severity) => (
63
+ <option value={severity}>{severity}</option>
64
+ ))}
65
+ </select>
66
+ <label
67
+ class="toggle"
68
+ title="Incluir findings que sumiram do último relatório"
69
+ >
70
+ <input
71
+ type="checkbox"
72
+ checked={value.showAbsent}
73
+ onChange={(event) =>
74
+ patch({
75
+ showAbsent: (event.target as HTMLInputElement).checked,
76
+ })
77
+ }
78
+ />
79
+ mostrar ausentes
80
+ </label>
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Cartão de um finding: dados + decisão (Executar/Ignorar), comentário e "feito".
3
+ */
4
+
5
+ import { useCallback, useEffect, useState } from "preact/hooks";
6
+ import { api } from "./api";
7
+ import { DECISION_LABELS, DECISIONS, EXEC_LABELS, SEVERITY_EMOJI } from "./constants";
8
+ import type { Finding } from "./types";
9
+
10
+ export function FindingCard({
11
+ slug,
12
+ finding,
13
+ onUpdated,
14
+ }: {
15
+ slug: string;
16
+ finding: Finding;
17
+ onUpdated: (finding: Finding) => void;
18
+ }) {
19
+ const [comment, setComment] = useState(finding.comment ?? "");
20
+ const [busy, setBusy] = useState(false);
21
+
22
+ useEffect(() => {
23
+ setComment(finding.comment ?? "");
24
+ }, [finding.comment]);
25
+
26
+ const patch = useCallback(
27
+ async (body: Record<string, unknown>) => {
28
+ setBusy(true);
29
+ try {
30
+ const { finding: updated } = await api<{ finding: Finding }>(
31
+ `/api/runs/${slug}/findings/${finding.id}`,
32
+ {
33
+ method: "PATCH",
34
+ headers: { "content-type": "application/json" },
35
+ body: JSON.stringify(body),
36
+ },
37
+ );
38
+ onUpdated(updated);
39
+ } finally {
40
+ setBusy(false);
41
+ }
42
+ },
43
+ [finding.id, onUpdated, slug],
44
+ );
45
+
46
+ const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
47
+ const count = finding.count > 1 ? `×${finding.count}` : "";
48
+ const isDone = finding.execState === "done";
49
+
50
+ return (
51
+ <article
52
+ class={`card sev-${finding.severity}${finding.present ? "" : " absent"}`}
53
+ >
54
+ <header class="card-head">
55
+ <span class="sev">
56
+ {SEVERITY_EMOJI[finding.severity]} {finding.severity}
57
+ </span>
58
+ <code class="loc">{location}</code>
59
+ {count && <span class="count">{count}</span>}
60
+ <span class="spacer" />
61
+ <code class="id" title="id do finding">
62
+ {finding.id}
63
+ </code>
64
+ </header>
65
+ <h3 class="rule">{finding.rule}</h3>
66
+ <p class="msg">{finding.message}</p>
67
+ <p class="action">
68
+ <strong>Ação:</strong> {finding.action}
69
+ </p>
70
+ {finding.evidence && <pre class="evidence">{finding.evidence}</pre>}
71
+ <footer class="card-foot">
72
+ <span class="watchdogs">{finding.watchdogIds.join(", ") || "—"}</span>
73
+ <span class="exec-state">{EXEC_LABELS[finding.execState]}</span>
74
+ </footer>
75
+ <div class="decisions">
76
+ {DECISIONS.map((decision) => (
77
+ <button
78
+ type="button"
79
+ title={`Marcar como ${DECISION_LABELS[decision]}`}
80
+ class={`btn dec-${decision}${finding.decision === decision ? " active" : ""}`}
81
+ disabled={busy}
82
+ onClick={() =>
83
+ patch({ decision, comment: comment.trim() || undefined })
84
+ }
85
+ >
86
+ {DECISION_LABELS[decision]}
87
+ </button>
88
+ ))}
89
+ </div>
90
+ <div class="comment-row">
91
+ <textarea
92
+ title="Comentário ou instrução adicional para o auto-fix"
93
+ placeholder="Comentário / instrução para o auto-fix (opcional)"
94
+ value={comment}
95
+ onInput={(event) =>
96
+ setComment((event.target as HTMLTextAreaElement).value)
97
+ }
98
+ />
99
+ <div class="comment-actions">
100
+ <button
101
+ type="button"
102
+ title="Salvar o comentário neste finding"
103
+ class="btn"
104
+ disabled={busy}
105
+ onClick={() => patch({ comment: comment.trim() })}
106
+ >
107
+ Salvar comentário
108
+ </button>
109
+ <button
110
+ type="button"
111
+ title="Alternar o estado de execução deste finding (feito/pendente)"
112
+ class={`btn done${isDone ? " active" : ""}`}
113
+ disabled={busy}
114
+ onClick={() => patch({ execState: isDone ? "pending" : "done" })}
115
+ >
116
+ {isDone ? "✓ Feito" : "Marcar feito"}
117
+ </button>
118
+ </div>
119
+ </div>
120
+ </article>
121
+ );
122
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Lista de findings agrupada por severidade (❌ > ⚠️ > 💡 > 🔎).
3
+ */
4
+
5
+ import { useMemo } from "preact/hooks";
6
+ import { SEVERITY_EMOJI, SEVERITY_ORDER } from "./constants";
7
+ import { FindingCard } from "./FindingCard";
8
+ import type { Finding, Severity } from "./types";
9
+
10
+ export function FindingsList({
11
+ slug,
12
+ findings,
13
+ onUpdated,
14
+ }: {
15
+ slug: string;
16
+ findings: Finding[];
17
+ onUpdated: (finding: Finding) => void;
18
+ }) {
19
+ const grouped = useMemo(() => {
20
+ const bySeverity = new Map<Severity, Finding[]>();
21
+ for (const severity of SEVERITY_ORDER) bySeverity.set(severity, []);
22
+ for (const finding of findings) bySeverity.get(finding.severity)?.push(finding);
23
+ return bySeverity;
24
+ }, [findings]);
25
+
26
+ if (findings.length === 0) {
27
+ return <p class="empty">Nenhum finding para os filtros.</p>;
28
+ }
29
+
30
+ return (
31
+ <>
32
+ {SEVERITY_ORDER.map((severity) => {
33
+ const items = grouped.get(severity) ?? [];
34
+ if (items.length === 0) return null;
35
+ return (
36
+ <section class="sev-group">
37
+ <h2>
38
+ {SEVERITY_EMOJI[severity]} {severity}{" "}
39
+ <span class="n">({items.length})</span>
40
+ </h2>
41
+ {items.map((finding) => (
42
+ <FindingCard
43
+ key={finding.id}
44
+ slug={slug}
45
+ finding={finding}
46
+ onUpdated={onUpdated}
47
+ />
48
+ ))}
49
+ </section>
50
+ );
51
+ })}
52
+ </>
53
+ );
54
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Sidebar: logo do houndsman + lista de runs (branch e nº de findings).
3
+ */
4
+
5
+ import { describeRunStatus } from "./constants";
6
+ import type { RunSummary } from "./types";
7
+
8
+ export function RunList({
9
+ runs,
10
+ slug,
11
+ onSelect,
12
+ }: {
13
+ runs: RunSummary[];
14
+ slug: string | null;
15
+ onSelect: (slug: string) => void;
16
+ }) {
17
+ return (
18
+ <aside class="sidebar">
19
+ <div class="brand">
20
+ <img class="logo" src="/assets/logo.png" alt="houndsman" />
21
+ <div>
22
+ <h1>Houndsman</h1>
23
+ <p class="sub">Revisões e triagem</p>
24
+ </div>
25
+ </div>
26
+ <nav class="runs">
27
+ {runs.length === 0 && <p class="empty">Nenhuma run encontrada.</p>}
28
+ {runs.map((run) => {
29
+ const status = describeRunStatus(run);
30
+ return (
31
+ <button
32
+ type="button"
33
+ title={`Abrir a run da branch ${run.branch}`}
34
+ class={`run${run.slug === slug ? " active" : ""}`}
35
+ onClick={() => onSelect(run.slug)}
36
+ >
37
+ <span class="run-branch">{run.branch}</span>
38
+ <span class="run-meta">
39
+ {run.summary.present}/{run.summary.total} presentes
40
+ </span>
41
+ {status && (
42
+ <span class={`run-status status-${status.kind}`}>
43
+ {status.label}
44
+ </span>
45
+ )}
46
+ </button>
47
+ );
48
+ })}
49
+ </nav>
50
+ </aside>
51
+ );
52
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Barra de resumo da triagem (total, presentes e contagens por decisão/execução).
3
+ */
4
+
5
+ import { DECISION_LABELS, DECISIONS } from "./constants";
6
+ import type { Summary } from "./types";
7
+
8
+ export function SummaryBar({ summary }: { summary: Summary }) {
9
+ return (
10
+ <div class="summary-bar">
11
+ <span>Total: {summary.total}</span>
12
+ <span>Presentes: {summary.present}</span>
13
+ {DECISIONS.filter((decision) => decision !== "pending").map((decision) => (
14
+ <span key={decision}>
15
+ {DECISION_LABELS[decision]}: {summary.byDecision[decision] ?? 0}
16
+ </span>
17
+ ))}
18
+ <span>Feitos: {summary.byExecState.done ?? 0}</span>
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Helpers de acesso à API local do servidor.
3
+ */
4
+
5
+ import { DECISIONS, EXEC_STATES, SEVERITY_ORDER } from "./constants";
6
+ import type { Decision, ExecState, Finding, Severity, Summary } from "./types";
7
+
8
+ /** `fetch` JSON: lança com o corpo do erro quando a resposta não é 2xx. */
9
+ export async function api<T>(path: string, init?: RequestInit): Promise<T> {
10
+ const res = await fetch(path, init);
11
+ if (!res.ok) throw new Error((await res.text()) || `HTTP ${res.status}`);
12
+ return (await res.json()) as T;
13
+ }
14
+
15
+ /** Recalcula o resumo a partir dos findings atuais (contadores vivos na UI). */
16
+ export function computeSummary(findings: Finding[]): Summary {
17
+ const byDecision = Object.fromEntries(DECISIONS.map((d) => [d, 0])) as Record<
18
+ Decision,
19
+ number
20
+ >;
21
+ const byExecState = Object.fromEntries(EXEC_STATES.map((e) => [e, 0])) as Record<
22
+ ExecState,
23
+ number
24
+ >;
25
+ const bySeverity = Object.fromEntries(SEVERITY_ORDER.map((s) => [s, 0])) as Record<
26
+ Severity,
27
+ number
28
+ >;
29
+ let present = 0;
30
+ for (const finding of findings) {
31
+ if (finding.present) present++;
32
+ byDecision[finding.decision]++;
33
+ byExecState[finding.execState]++;
34
+ bySeverity[finding.severity]++;
35
+ }
36
+ return { total: findings.length, present, byDecision, byExecState, bySeverity };
37
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Constantes de apresentação do cliente (labels, ordem, emojis).
3
+ */
4
+
5
+ import type { Decision, ExecState, RunStatus, RunSummary, Severity } from "./types";
6
+
7
+ export const SEVERITY_EMOJI: Record<Severity, string> = {
8
+ blocking: "❌",
9
+ attention: "⚠️",
10
+ suggestion: "💡",
11
+ nit: "🔎",
12
+ };
13
+
14
+ export const SEVERITY_ORDER: Severity[] = ["blocking", "attention", "suggestion", "nit"];
15
+
16
+ export const DECISIONS: Decision[] = ["pending", "execute", "ignore"];
17
+
18
+ export const DECISION_LABELS: Record<Decision, string> = {
19
+ pending: "Pendente",
20
+ execute: "Executar",
21
+ ignore: "Ignorar",
22
+ };
23
+
24
+ export const EXEC_STATES: ExecState[] = ["pending", "in_progress", "done", "stale"];
25
+
26
+ export const EXEC_LABELS: Record<ExecState, string> = {
27
+ pending: "exec: pendente",
28
+ in_progress: "exec: em andamento",
29
+ done: "exec: feito",
30
+ stale: "exec: stale",
31
+ };
32
+
33
+ export const RUN_STATUS_LABELS: Record<RunStatus, string> = {
34
+ reviewing: "Revisando",
35
+ review_done: "Revisão pronta",
36
+ fixing: "Corrigindo",
37
+ fix_done: "Correção pronta",
38
+ };
39
+
40
+ /** Limiar de inatividade para exibir timeout (display-only; não é status). */
41
+ export const STATUS_TIMEOUT_MS = 10 * 60 * 1000;
42
+
43
+ export type RunStatusView = { kind: string; label: string; timeout: boolean };
44
+
45
+ /** View do status de uma run — label; vira timeout quando ativo e sem ping recente. */
46
+ export function describeRunStatus(
47
+ run: RunSummary,
48
+ now: number = Date.now(),
49
+ ): RunStatusView | null {
50
+ const status = run.status;
51
+ if (!status) return null;
52
+ const active = status === "reviewing" || status === "fixing";
53
+ const updated = run.updatedAt ? Date.parse(run.updatedAt) : Number.NaN;
54
+ if (active && Number.isFinite(updated) && now - updated > STATUS_TIMEOUT_MS) {
55
+ return {
56
+ kind: "timeout",
57
+ label: status === "reviewing" ? "Timeout na revisão" : "Timeout na correção",
58
+ timeout: true,
59
+ };
60
+ }
61
+ return { kind: status, label: RUN_STATUS_LABELS[status], timeout: false };
62
+ }
63
+
64
+ /** Artefatos que podem ser lidos na aba "Artefatos". */
65
+ export const ARTIFACTS = [
66
+ "manifest.json",
67
+ "classification.json",
68
+ "candidates.json",
69
+ "context-summary.json",
70
+ "review.md",
71
+ ] as const;
72
+
73
+ export type ArtifactName = (typeof ARTIFACTS)[number];
74
+
75
+ /** Descrição de cada artefato (mostrada ao selecioná-lo na aba). */
76
+ export const ARTIFACT_INFO: Record<ArtifactName, string> = {
77
+ "manifest.json":
78
+ "Resumo da execução: watchdogs ativos, arquivos, orçamento, churn, Jev e contexto da PR.",
79
+ "classification.json":
80
+ "Classificação do diff: watchdogs ativados por arquivo, status (A/M/D/R) e churn por arquivo.",
81
+ "candidates.json":
82
+ "Indícios da pré-análise Jev (regra + arquivo + confiança) — não são achados.",
83
+ "context-summary.json":
84
+ "Resumo da PR (objetivo, motivação e alterações) gerado no Passo 0.",
85
+ "review.md": "Relatório consolidado da revisão — o entregável.",
86
+ };
87
+
88
+ /** Legenda única de cada aba (explica o que a aba é; mostrada quando selecionada). */
89
+ export const TAB_CAPTIONS = {
90
+ findings:
91
+ "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>.",
92
+ artifacts:
93
+ "Artefatos brutos da run: manifesto, classificação, candidatos Jev, contexto da PR e o relatório consolidado (review.md).",
94
+ } as const;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Entry do cliente (Preact): monta o `<App />` no `#app`.
3
+ *
4
+ * Bundle gerado em memória por `Bun.build` (`client-build.ts`). Os componentes
5
+ * vivem em arquivos irmãos nesta pasta (`App.tsx`, `RunList.tsx`, ...).
6
+ */
7
+
8
+ import { render } from "preact";
9
+ import { App } from "./App";
10
+
11
+ const root = document.getElementById("app");
12
+ if (root) render(<App />, root);