@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
@@ -1,48 +1,169 @@
1
1
  /**
2
- * Painel de artefatos: abas para ver o conteúdo bruto dos arquivos da run.
2
+ * Painel de artefatos: lista todos os artefatos persistidos da run e renderiza
3
+ * o conteúdo bruto. JSON ganha destaque de sintaxe; markdown é renderizado com
4
+ * o mesmo parser/highlight do restante da UI. Briefs e relatórios ficam
5
+ * auditáveis pela mesma navegação.
3
6
  */
4
7
 
5
- import { useEffect, useState } from "preact/hooks";
6
- import { ARTIFACT_INFO, ARTIFACTS, type ArtifactName } from "./constants";
8
+ import { useEffect, useMemo, useState } from "preact/hooks";
9
+ import { MarkdownText } from "./MarkdownText";
10
+ import { api } from "./utils/api";
11
+ import {
12
+ ARTIFACT_KIND_LABELS,
13
+ describeArtifact,
14
+ displayArtifactName,
15
+ groupArtifacts,
16
+ isJsonArtifact,
17
+ isMarkdownArtifact,
18
+ } from "./utils/constants";
19
+ import { formatJson, highlightJson } from "./utils/json-highlight";
20
+ import type { ArtifactMeta } from "./utils/types";
21
+
22
+ /** Prefere o relatório consolidado; senão, o primeiro artefato disponível. */
23
+ function pickDefault(items: ArtifactMeta[]): string {
24
+ return (items.find((item) => item.name === "review.md") ?? items[0])?.name ?? "";
25
+ }
26
+
27
+ /** JSON com destaque de sintaxe (tokens em spans, sem lib externa). */
28
+ function JsonView({ content }: { content: string }) {
29
+ const tokens = useMemo(() => highlightJson(formatJson(content)), [content]);
30
+ return (
31
+ <pre class="artifact-view json-view">
32
+ {tokens.map((token) => (
33
+ <span class={token.type === "plain" ? undefined : `json-${token.type}`}>
34
+ {token.text}
35
+ </span>
36
+ ))}
37
+ </pre>
38
+ );
39
+ }
7
40
 
8
41
  export function ArtifactsPanel({ slug }: { slug: string }) {
9
- const [name, setName] = useState<ArtifactName>(ARTIFACTS[0]);
42
+ const [items, setItems] = useState<ArtifactMeta[]>([]);
43
+ const [name, setName] = useState("");
10
44
  const [content, setContent] = useState("");
11
45
  const [error, setError] = useState("");
46
+ const [loading, setLoading] = useState(false);
47
+
48
+ useEffect(() => {
49
+ let cancelled = false;
50
+ setError("");
51
+ api<{ artifacts: ArtifactMeta[] }>(`/api/runs/${slug}/artifacts`)
52
+ .then(({ artifacts }) => {
53
+ if (cancelled) return;
54
+ setItems(artifacts);
55
+ setName((current) =>
56
+ artifacts.some((item) => item.name === current)
57
+ ? current
58
+ : pickDefault(artifacts),
59
+ );
60
+ })
61
+ .catch((err) => {
62
+ if (cancelled) return;
63
+ setItems([]);
64
+ setName("");
65
+ setError(String(err));
66
+ });
67
+ return () => {
68
+ cancelled = true;
69
+ };
70
+ }, [slug]);
12
71
 
13
72
  useEffect(() => {
73
+ if (!name) {
74
+ setContent("");
75
+ return;
76
+ }
77
+ let cancelled = false;
78
+ setLoading(true);
14
79
  setError("");
15
- fetch(`/api/runs/${slug}/artifacts/${name}`)
80
+ fetch(`/api/runs/${slug}/artifacts/${encodeURI(name)}`)
16
81
  .then(async (res) => {
17
82
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
18
- setContent(await res.text());
83
+ const text = await res.text();
84
+ if (!cancelled) setContent(text);
19
85
  })
20
86
  .catch((err) => {
87
+ if (cancelled) return;
21
88
  setContent("");
22
89
  setError(String(err));
90
+ })
91
+ .finally(() => {
92
+ if (!cancelled) setLoading(false);
23
93
  });
94
+ return () => {
95
+ cancelled = true;
96
+ };
24
97
  }, [slug, name]);
25
98
 
99
+ const groups = useMemo(() => groupArtifacts(items), [items]);
100
+ const selected = useMemo(
101
+ () => items.find((item) => item.name === name),
102
+ [items, name],
103
+ );
104
+
26
105
  return (
27
106
  <div class="artifacts">
28
- <div class="artifact-tabs">
29
- {ARTIFACTS.map((artifact) => (
30
- <button
31
- type="button"
32
- title={ARTIFACT_INFO[artifact]}
33
- class={`chip${artifact === name ? " active" : ""}`}
34
- onClick={() => setName(artifact)}
35
- >
36
- {artifact}
37
- </button>
38
- ))}
107
+ <div class="artifact-layout">
108
+ <nav class="artifact-list" aria-label="Artefatos da revisão">
109
+ {groups.map((group) => (
110
+ <div class="artifact-group" key={group.key}>
111
+ <p class="artifact-group-label">{group.label}</p>
112
+ {group.items.map((item) => (
113
+ <button
114
+ type="button"
115
+ key={item.name}
116
+ title={item.name}
117
+ class={`artifact-item${item.name === name ? " active" : ""}`}
118
+ onClick={() => setName(item.name)}
119
+ >
120
+ <span class="artifact-item-name">
121
+ {displayArtifactName(item)}
122
+ </span>
123
+ <span class="artifact-item-kind">
124
+ {ARTIFACT_KIND_LABELS[item.kind] ?? item.kind}
125
+ </span>
126
+ </button>
127
+ ))}
128
+ </div>
129
+ ))}
130
+ {items.length === 0 && !loading && (
131
+ <p class="empty">Nenhum artefato persistido nesta run.</p>
132
+ )}
133
+ </nav>
134
+
135
+ <section class="artifact-pane">
136
+ {selected ? (
137
+ <header class="artifact-head">
138
+ <div>
139
+ <h3 class="artifact-title">{selected.name}</h3>
140
+ <p class="artifact-desc">{describeArtifact(selected)}</p>
141
+ </div>
142
+ <span class="artifact-kind-badge">
143
+ {ARTIFACT_KIND_LABELS[selected.kind] ?? selected.kind}
144
+ </span>
145
+ </header>
146
+ ) : null}
147
+
148
+ {error ? (
149
+ <p class="error">{error}</p>
150
+ ) : loading ? (
151
+ <p class="empty">Carregando artefato…</p>
152
+ ) : selected ? (
153
+ isJsonArtifact(selected) ? (
154
+ <JsonView content={content} />
155
+ ) : isMarkdownArtifact(selected) ? (
156
+ <div class="artifact-view artifact-markdown">
157
+ <MarkdownText text={content} />
158
+ </div>
159
+ ) : (
160
+ <pre class="artifact-view">{content || "—"}</pre>
161
+ )
162
+ ) : (
163
+ <p class="empty">Selecione um artefato para visualizar.</p>
164
+ )}
165
+ </section>
39
166
  </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
167
  </div>
47
168
  );
48
169
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { useEffect, useState } from "preact/hooks";
9
- import { formatElapsed } from "./format";
9
+ import { formatElapsed } from "./utils/format";
10
10
 
11
11
  export function AutoRefresh({
12
12
  lastSync,
@@ -3,9 +3,14 @@
3
3
  * o toggle "ausentes".
4
4
  */
5
5
 
6
- import { DECISION_LABELS, DECISIONS, SEVERITY_LABELS, SEVERITY_ORDER } from "./constants";
7
6
  import { Search, Watch } from "./icons";
8
- import type { Decision, Severity } from "./types";
7
+ import {
8
+ DECISION_LABELS,
9
+ DECISIONS,
10
+ SEVERITY_LABELS,
11
+ SEVERITY_ORDER,
12
+ } from "./utils/constants";
13
+ import type { Decision, Severity } from "./utils/types";
9
14
 
10
15
  export type FiltersState = {
11
16
  query: string;
@@ -6,20 +6,23 @@
6
6
  * poluição visual da triagem; reabrir volta a expandir.
7
7
  */
8
8
 
9
- import { useCallback, useEffect, useRef, useState } from "preact/hooks";
10
- import { api } from "./api";
11
- import { DECISION_LABELS, DECISIONS, SEVERITY_LABELS } from "./constants";
12
- import { vscodeFileLink } from "./format";
9
+ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
13
10
  import {
14
11
  Check,
15
12
  ChevronRight,
16
13
  CodeIcon,
17
14
  ExternalLink,
15
+ GitHub,
18
16
  Sparkles,
19
17
  Spinner,
20
18
  Watch,
21
19
  } from "./icons";
22
- import type { Finding } from "./types";
20
+ import { MarkdownText } from "./MarkdownText";
21
+ import { api } from "./utils/api";
22
+ import { highlightCode, languageFromFile } from "./utils/code-highlight";
23
+ import { DECISION_LABELS, DECISIONS, SEVERITY_LABELS } from "./utils/constants";
24
+ import { vscodeFileLink } from "./utils/format";
25
+ import type { Finding } from "./utils/types";
23
26
 
24
27
  /** Debounce do auto-save do comentário (ms). */
25
28
  const AUTOSAVE_MS = 700;
@@ -102,6 +105,13 @@ export function FindingCard({
102
105
 
103
106
  const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
104
107
  const href = finding.absFile ? vscodeFileLink(finding.absFile, finding.line) : null;
108
+ const highlighted = useMemo(
109
+ () =>
110
+ finding.evidence
111
+ ? highlightCode(finding.evidence, languageFromFile(finding.file))
112
+ : null,
113
+ [finding.evidence, finding.file],
114
+ );
105
115
  const toggle = () => setOpen((current) => !current);
106
116
 
107
117
  return (
@@ -146,17 +156,20 @@ export function FindingCard({
146
156
  {finding.github &&
147
157
  (finding.github.url ? (
148
158
  <a
149
- class="sent-tag"
159
+ class="sent-tag gh"
150
160
  href={finding.github.url}
151
161
  target="_blank"
152
162
  rel="noreferrer"
153
- title="Comentário enviado à PR"
163
+ title={`Comentário enviado ao GitHub (PR #${finding.github.pr}) — abrir`}
154
164
  >
155
- <ExternalLink size={12} /> enviado
165
+ <GitHub size={12} /> enviado ao GitHub
156
166
  </a>
157
167
  ) : (
158
- <span class="sent-tag" title="Comentário enviado à PR">
159
- <ExternalLink size={12} /> enviado
168
+ <span
169
+ class="sent-tag gh"
170
+ title={`Comentário enviado ao GitHub (PR #${finding.github.pr})`}
171
+ >
172
+ <GitHub size={12} /> enviado ao GitHub
160
173
  </span>
161
174
  ))}
162
175
  <span class="spacer" />
@@ -186,14 +199,36 @@ export function FindingCard({
186
199
  <div class="meta-row">{finding.rule}</div>
187
200
 
188
201
  <div class="action-block">
189
- <p>
190
- <Sparkles size={16} class="action-icon" />
202
+ <Sparkles size={16} class="action-icon" />
203
+ <div class="action-text">
191
204
  <span class="action-label">Ação:</span>
192
- {finding.action}
193
- </p>
205
+ <MarkdownText text={finding.action} />
206
+ </div>
194
207
  </div>
195
208
 
196
- {finding.evidence && <pre class="evidence">{finding.evidence}</pre>}
209
+ {finding.evidence &&
210
+ (highlighted ? (
211
+ <div class="evidence-block">
212
+ <div class="evidence-head">
213
+ <span class="code-lang">{highlighted.label}</span>
214
+ </div>
215
+ <pre class="evidence code-evidence">
216
+ {highlighted.tokens.map((token) => (
217
+ <span
218
+ class={
219
+ token.type === "plain"
220
+ ? undefined
221
+ : `tok-${token.type}`
222
+ }
223
+ >
224
+ {token.text}
225
+ </span>
226
+ ))}
227
+ </pre>
228
+ </div>
229
+ ) : (
230
+ <pre class="evidence">{finding.evidence}</pre>
231
+ ))}
197
232
 
198
233
  <div class="decision-tags-block">
199
234
  <div class="decisions">
@@ -3,10 +3,10 @@
3
3
  */
4
4
 
5
5
  import { useMemo } from "preact/hooks";
6
- import { SEVERITY_LABELS, SEVERITY_ORDER } from "./constants";
7
6
  import { FindingCard } from "./FindingCard";
8
7
  import { Alert } from "./icons";
9
- import type { Finding, Severity } from "./types";
8
+ import { SEVERITY_LABELS, SEVERITY_ORDER } from "./utils/constants";
9
+ import type { Finding, Severity } from "./utils/types";
10
10
 
11
11
  export function FindingsList({
12
12
  slug,
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Render de markdown mínimo para o campo `action` do finding e artefatos.
3
+ *
4
+ * Sem HTML cru: tudo vira nós do Preact. Blocos de código reutilizam o
5
+ * highlight de código; links passam por `safeUrl` e caminhos de arquivo ganham
6
+ * destaque próprio.
7
+ */
8
+
9
+ import { useMemo } from "preact/hooks";
10
+ import { highlightCode, languageFromName } from "./utils/code-highlight";
11
+ import {
12
+ type InlineToken,
13
+ isFilePath,
14
+ type MarkdownBlock,
15
+ parseInline,
16
+ parseMarkdown,
17
+ safeUrl,
18
+ } from "./utils/markdown";
19
+
20
+ function renderInlineToken(token: InlineToken) {
21
+ switch (token.type) {
22
+ case "code":
23
+ return (
24
+ <code class={`md-code${isFilePath(token.text) ? " md-path" : ""}`}>
25
+ {token.text}
26
+ </code>
27
+ );
28
+ case "path":
29
+ return <code class="md-path">{token.text}</code>;
30
+ case "strong":
31
+ return <strong>{token.text}</strong>;
32
+ case "em":
33
+ return <em>{token.text}</em>;
34
+ case "link": {
35
+ const label = token.text.replace(/^`+|`+$/g, "");
36
+ const url = safeUrl(token.url);
37
+ if (url) {
38
+ return (
39
+ <a href={url} target="_blank" rel="noreferrer">
40
+ {label}
41
+ </a>
42
+ );
43
+ }
44
+ return isFilePath(label) || isFilePath(token.url) ? (
45
+ <code class="md-path">{label}</code>
46
+ ) : (
47
+ token.text
48
+ );
49
+ }
50
+ default:
51
+ return token.text;
52
+ }
53
+ }
54
+
55
+ function InlineTokens({ text }: { text: string }) {
56
+ const tokens = useMemo(() => parseInline(text), [text]);
57
+ return <>{tokens.map((token) => renderInlineToken(token))}</>;
58
+ }
59
+
60
+ function CodeBlock({ code, language }: { code: string; language: string | null }) {
61
+ const highlighted = useMemo(
62
+ () => highlightCode(code, languageFromName(language)),
63
+ [code, language],
64
+ );
65
+ return (
66
+ <pre class="md-code-block">
67
+ {highlighted
68
+ ? highlighted.tokens.map((token) => (
69
+ <span
70
+ class={
71
+ token.type === "plain" ? undefined : `tok-${token.type}`
72
+ }
73
+ >
74
+ {token.text}
75
+ </span>
76
+ ))
77
+ : code}
78
+ </pre>
79
+ );
80
+ }
81
+
82
+ /** Renderiza blocos markdown já parseados (seções de artefato, ação etc.). */
83
+ export function MarkdownBlocks({ blocks }: { blocks: MarkdownBlock[] }) {
84
+ return (
85
+ <>
86
+ {blocks.map((block) => {
87
+ switch (block.type) {
88
+ case "paragraph":
89
+ return (
90
+ <p class="md-p">
91
+ <InlineTokens text={block.text} />
92
+ </p>
93
+ );
94
+ case "heading": {
95
+ const level = Math.min(Math.max(block.level, 1), 6);
96
+ const Tag = `h${level}` as
97
+ | "h1"
98
+ | "h2"
99
+ | "h3"
100
+ | "h4"
101
+ | "h5"
102
+ | "h6";
103
+ return (
104
+ <Tag class={`md-h md-h${level}`}>
105
+ <InlineTokens text={block.text} />
106
+ </Tag>
107
+ );
108
+ }
109
+ case "list": {
110
+ const items = block.items.map((item) => (
111
+ <li>
112
+ <InlineTokens text={item} />
113
+ </li>
114
+ ));
115
+ return block.ordered ? (
116
+ <ol class="md-list">{items}</ol>
117
+ ) : (
118
+ <ul class="md-list">{items}</ul>
119
+ );
120
+ }
121
+ case "table":
122
+ return (
123
+ <div class="md-table-wrap">
124
+ <table class="md-table">
125
+ <thead>
126
+ <tr>
127
+ {block.header.map((cell, column) => (
128
+ <th
129
+ style={{
130
+ textAlign: block.align[column],
131
+ }}
132
+ >
133
+ <InlineTokens text={cell} />
134
+ </th>
135
+ ))}
136
+ </tr>
137
+ </thead>
138
+ <tbody>
139
+ {block.rows.map((row) => (
140
+ <tr key={row.join("|")}>
141
+ {block.header.map((_, column) => (
142
+ <td
143
+ style={{
144
+ textAlign:
145
+ block.align[column],
146
+ }}
147
+ >
148
+ <InlineTokens
149
+ text={row[column] ?? ""}
150
+ />
151
+ </td>
152
+ ))}
153
+ </tr>
154
+ ))}
155
+ </tbody>
156
+ </table>
157
+ </div>
158
+ );
159
+ case "code":
160
+ return <CodeBlock code={block.code} language={block.language} />;
161
+ case "quote":
162
+ return (
163
+ <blockquote class="md-quote">
164
+ <InlineTokens text={block.text} />
165
+ </blockquote>
166
+ );
167
+ case "hr":
168
+ return <hr class="md-hr" />;
169
+ default:
170
+ return null;
171
+ }
172
+ })}
173
+ </>
174
+ );
175
+ }
176
+
177
+ /** Renderiza o markdown do action/artefato preservando blocos e listas. */
178
+ export function MarkdownText({ text }: { text: string }) {
179
+ const blocks = useMemo(() => parseMarkdown(text), [text]);
180
+ return <MarkdownBlocks blocks={blocks} />;
181
+ }