@payconductor/houndsman 0.4.0 → 0.6.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 +35 -41
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/grammars/web-tree-sitter.wasm +0 -0
- package/package.json +11 -3
- package/src/cli/commands/command.ts +5 -0
- package/src/cli/commands/doctor-command.ts +116 -24
- package/src/cli/commands/hunt-command.ts +25 -0
- package/src/cli/commands/status-command.ts +46 -0
- package/src/cli/help.ts +18 -1
- package/src/cli/index.ts +13 -6
- package/src/core/ai/ai-client.ts +221 -0
- package/src/core/ai/ai-sdk-model-provider.ts +101 -0
- package/src/core/ai/errors.ts +36 -0
- package/src/core/ai/features/brief-enricher.ts +62 -0
- package/src/core/ai/features/classifier.ts +50 -0
- package/src/core/ai/features/context-summarizer.ts +31 -0
- package/src/core/ai/features/pre-analyzer.ts +51 -0
- package/src/core/ai/features/verifier.ts +45 -0
- package/src/core/ai/index.ts +56 -0
- package/src/core/ai/model-factory.ts +137 -0
- package/src/core/ai/prompts/base.ts +36 -0
- package/src/core/ai/prompts/brief.ts +87 -0
- package/src/core/ai/prompts/classification.ts +36 -0
- package/src/core/ai/prompts/context.ts +29 -0
- package/src/core/ai/prompts/pre-analysis.ts +49 -0
- package/src/core/ai/prompts/verify.ts +40 -0
- package/src/core/ai/schemas.ts +110 -0
- package/src/core/ai/types.ts +76 -0
- package/src/core/ai/usage-budget.ts +75 -0
- package/src/core/ai/usage-recorder.ts +79 -0
- package/src/core/assets.ts +16 -0
- package/src/core/ast/diff.ts +220 -0
- package/src/core/ast/impact.ts +127 -0
- package/src/core/ast/parser.ts +167 -0
- package/src/core/ast/service.ts +233 -0
- package/src/core/ast/symbols.ts +294 -0
- package/src/core/ast/types.ts +125 -0
- package/src/core/brief.ts +138 -0
- package/src/core/commands/findings.ts +120 -6
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/status.ts +63 -0
- package/src/core/commands/update.ts +5 -3
- package/src/core/config.ts +85 -0
- package/src/core/defaults.ts +27 -0
- package/src/core/domain/watchdog.ts +43 -5
- package/src/core/engine/houndsman-engine.ts +16 -1
- package/src/core/engine/review-scope.ts +77 -13
- package/src/core/engine/review-session.ts +27 -2
- package/src/core/env.ts +101 -0
- package/src/core/file-classifier.ts +25 -4
- package/src/core/findings-state.ts +21 -4
- package/src/core/github-review.ts +620 -57
- package/src/core/jev/jev-client.ts +8 -7
- package/src/core/jev/jev-verify.ts +2 -1
- package/src/core/logging/index.ts +17 -0
- package/src/core/logging/logger.ts +187 -0
- package/src/core/orchestrator.ts +454 -16
- package/src/core/paths.ts +3 -0
- package/src/core/project-config.ts +123 -1
- package/src/core/registry-check.ts +148 -6
- package/src/core/renderers/manifest-writer.ts +33 -0
- package/src/core/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/rule-text.ts +97 -0
- package/src/core/run-status.ts +39 -5
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/file/file-storage.ts +2 -0
- package/src/core/storage/paths.ts +6 -5
- 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 +15 -0
- package/src/core/storage/work-area.ts +12 -0
- package/src/core/types.ts +37 -1
- package/src/core/verify.ts +17 -5
- package/src/harness/README.md +15 -0
- package/src/harness/agent/agent-context.ts +33 -0
- package/src/harness/agent/agent-errors.ts +30 -0
- package/src/harness/agent/agent-factory.ts +45 -0
- package/src/harness/agent/agent-reporter.ts +34 -0
- package/src/harness/agent/agent-result.ts +16 -0
- package/src/harness/agent/agent-runner.ts +32 -0
- package/src/harness/agent/agent-scheduler.ts +37 -0
- package/src/harness/agent/review-agent.ts +391 -0
- package/src/harness/config/harness-config.ts +84 -0
- package/src/harness/hunt/hunt-harness.ts +83 -0
- package/src/harness/hunt/hunt-options.ts +75 -0
- package/src/harness/hunt/hunt-result.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +372 -0
- package/src/harness/hunt/review-planner.ts +98 -0
- package/src/harness/index.ts +36 -0
- package/src/harness/investigation/agent-state.ts +134 -0
- package/src/harness/investigation/evidence-verifier.ts +129 -0
- package/src/harness/investigation/evidence.ts +110 -0
- package/src/harness/investigation/finding-critic.ts +96 -0
- package/src/harness/investigation/investigation-planner.ts +118 -0
- package/src/harness/investigation/investigation-task.ts +10 -0
- package/src/harness/investigation/review-journal.ts +87 -0
- package/src/harness/investigation/rule-analyzer.ts +178 -0
- package/src/harness/investigation/rule-signals.ts +110 -0
- package/src/harness/prompts/agent-schemas.ts +57 -0
- package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
- package/src/harness/prompts/states/conclude-prompt.ts +16 -0
- package/src/harness/prompts/states/investigate-prompt.ts +37 -0
- package/src/harness/prompts/states/orient-prompt.ts +15 -0
- package/src/harness/prompts/states/verify-prompt.ts +22 -0
- package/src/harness/tools/agent-tool.ts +102 -0
- package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
- package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
- package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
- package/src/harness/tools/builtin/find-references-tool.ts +66 -0
- package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
- package/src/harness/tools/builtin/helpers.ts +115 -0
- package/src/harness/tools/builtin/impact-tool.ts +48 -0
- package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
- package/src/harness/tools/builtin/index.ts +59 -0
- package/src/harness/tools/builtin/list-files-tool.ts +85 -0
- package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
- package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
- package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
- package/src/harness/tools/builtin/search-code-tool.ts +65 -0
- package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
- package/src/harness/tools/path-guard.ts +90 -0
- package/src/harness/tools/result-limiter.ts +49 -0
- package/src/harness/tools/secret-redactor.ts +54 -0
- package/src/harness/tools/tool-cache.ts +50 -0
- package/src/harness/tools/tool-call-budget.ts +60 -0
- package/src/harness/tools/tool-context.ts +5 -0
- package/src/harness/tools/tool-middleware.ts +105 -0
- package/src/harness/tools/tool-policy.ts +51 -0
- package/src/harness/tools/tool-registry.ts +112 -0
- package/src/harness/tools/tool-result.ts +15 -0
- package/src/harness/tools/toolbox.ts +36 -0
- package/src/harness/validation/finding-validator.ts +119 -0
- package/src/index.ts +46 -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} +61 -10
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +64 -0
- package/src/server/index.ts +66 -7
- package/src/server/run-repository.ts +9 -0
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/example-review.md +22 -14
- package/src/skill/hunt.md +45 -19
- package/src/skill/verify.md +3 -2
- package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
- package/src/tests/ai-core.test.ts +120 -0
- package/src/tests/ai-features.test.ts +183 -0
- package/src/tests/ast.test.ts +335 -0
- package/src/tests/baseline.test.ts +3 -0
- package/src/tests/code-highlight.test.ts +1 -1
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/findings-state.test.ts +50 -2
- package/src/tests/fixtures/fake-agent-tools.ts +69 -0
- package/src/tests/fixtures/fake-investigation.ts +64 -0
- package/src/tests/fixtures/fake-model-provider.ts +48 -0
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/harness-agent.test.ts +122 -0
- package/src/tests/harness-investigation.test.ts +206 -0
- package/src/tests/harness-tools.test.ts +148 -0
- package/src/tests/hunt-command.test.ts +212 -0
- package/src/tests/logger.test.ts +83 -0
- 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/rule-analyzer.test.ts +61 -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/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- 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
|
@@ -7,21 +7,22 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
|
10
|
-
import { api } from "./api";
|
|
11
|
-
import { highlightCode, languageFromFile } from "./code-highlight";
|
|
12
|
-
import { DECISION_LABELS, DECISIONS, SEVERITY_LABELS } from "./constants";
|
|
13
|
-
import { vscodeFileLink } from "./format";
|
|
14
10
|
import {
|
|
15
11
|
Check,
|
|
16
12
|
ChevronRight,
|
|
17
13
|
CodeIcon,
|
|
18
14
|
ExternalLink,
|
|
15
|
+
GitHub,
|
|
19
16
|
Sparkles,
|
|
20
17
|
Spinner,
|
|
21
18
|
Watch,
|
|
22
19
|
} from "./icons";
|
|
23
20
|
import { MarkdownText } from "./MarkdownText";
|
|
24
|
-
import
|
|
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";
|
|
25
26
|
|
|
26
27
|
/** Debounce do auto-save do comentário (ms). */
|
|
27
28
|
const AUTOSAVE_MS = 700;
|
|
@@ -155,17 +156,20 @@ export function FindingCard({
|
|
|
155
156
|
{finding.github &&
|
|
156
157
|
(finding.github.url ? (
|
|
157
158
|
<a
|
|
158
|
-
class="sent-tag"
|
|
159
|
+
class="sent-tag gh"
|
|
159
160
|
href={finding.github.url}
|
|
160
161
|
target="_blank"
|
|
161
162
|
rel="noreferrer"
|
|
162
|
-
title=
|
|
163
|
+
title={`Comentário enviado ao GitHub (PR #${finding.github.pr}) — abrir`}
|
|
163
164
|
>
|
|
164
|
-
<
|
|
165
|
+
<GitHub size={12} /> enviado ao GitHub
|
|
165
166
|
</a>
|
|
166
167
|
) : (
|
|
167
|
-
<span
|
|
168
|
-
|
|
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
|
|
169
173
|
</span>
|
|
170
174
|
))}
|
|
171
175
|
<span class="spacer" />
|
|
@@ -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
|
|
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,
|
|
@@ -1,28 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Render de markdown mínimo para o campo `action` do finding.
|
|
2
|
+
* Render de markdown mínimo para o campo `action` do finding e artefatos.
|
|
3
3
|
*
|
|
4
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
|
|
5
|
+
* highlight de código; links passam por `safeUrl` e caminhos de arquivo ganham
|
|
6
|
+
* destaque próprio.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { useMemo } from "preact/hooks";
|
|
9
|
-
import { highlightCode, languageFromName } from "./code-highlight";
|
|
10
|
-
import {
|
|
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";
|
|
11
19
|
|
|
12
20
|
function renderInlineToken(token: InlineToken) {
|
|
13
21
|
switch (token.type) {
|
|
14
22
|
case "code":
|
|
15
|
-
return
|
|
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>;
|
|
16
30
|
case "strong":
|
|
17
31
|
return <strong>{token.text}</strong>;
|
|
18
32
|
case "em":
|
|
19
33
|
return <em>{token.text}</em>;
|
|
20
34
|
case "link": {
|
|
35
|
+
const label = token.text.replace(/^`+|`+$/g, "");
|
|
21
36
|
const url = safeUrl(token.url);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
|
|
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>
|
|
26
46
|
) : (
|
|
27
47
|
token.text
|
|
28
48
|
);
|
|
@@ -59,9 +79,8 @@ function CodeBlock({ code, language }: { code: string; language: string | null }
|
|
|
59
79
|
);
|
|
60
80
|
}
|
|
61
81
|
|
|
62
|
-
/** Renderiza
|
|
63
|
-
export function
|
|
64
|
-
const blocks = useMemo(() => parseMarkdown(text), [text]);
|
|
82
|
+
/** Renderiza blocos markdown já parseados (seções de artefato, ação etc.). */
|
|
83
|
+
export function MarkdownBlocks({ blocks }: { blocks: MarkdownBlock[] }) {
|
|
65
84
|
return (
|
|
66
85
|
<>
|
|
67
86
|
{blocks.map((block) => {
|
|
@@ -72,6 +91,21 @@ export function MarkdownText({ text }: { text: string }) {
|
|
|
72
91
|
<InlineTokens text={block.text} />
|
|
73
92
|
</p>
|
|
74
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
|
+
}
|
|
75
109
|
case "list": {
|
|
76
110
|
const items = block.items.map((item) => (
|
|
77
111
|
<li>
|
|
@@ -84,6 +118,44 @@ export function MarkdownText({ text }: { text: string }) {
|
|
|
84
118
|
<ul class="md-list">{items}</ul>
|
|
85
119
|
);
|
|
86
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
|
+
);
|
|
87
159
|
case "code":
|
|
88
160
|
return <CodeBlock code={block.code} language={block.language} />;
|
|
89
161
|
case "quote":
|
|
@@ -92,6 +164,8 @@ export function MarkdownText({ text }: { text: string }) {
|
|
|
92
164
|
<InlineTokens text={block.text} />
|
|
93
165
|
</blockquote>
|
|
94
166
|
);
|
|
167
|
+
case "hr":
|
|
168
|
+
return <hr class="md-hr" />;
|
|
95
169
|
default:
|
|
96
170
|
return null;
|
|
97
171
|
}
|
|
@@ -99,3 +173,9 @@ export function MarkdownText({ text }: { text: string }) {
|
|
|
99
173
|
</>
|
|
100
174
|
);
|
|
101
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
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aba "Resumo" (inicial): visão principal da revisão.
|
|
3
|
+
*
|
|
4
|
+
* O texto narrativo vem do artefato `review-resume.md` (Resumo de negócio); os
|
|
5
|
+
* números viram cards a partir do `classification.json`/`manifest.json` e o
|
|
6
|
+
* card de domínios resume os achados por severidade em cada watchdog — assim a
|
|
7
|
+
* UI mostra os mesmos dados do resumo de forma mais organizada, sem duplicar o
|
|
8
|
+
* texto.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ComponentChildren } from "preact";
|
|
12
|
+
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
13
|
+
import { ExternalLink, Layers } from "./icons";
|
|
14
|
+
import { MarkdownBlocks } from "./MarkdownText";
|
|
15
|
+
import { api } from "./utils/api";
|
|
16
|
+
import { SEVERITY_LABELS, SEVERITY_ORDER } from "./utils/constants";
|
|
17
|
+
import { formatRunTime } from "./utils/format";
|
|
18
|
+
import { sectionBlocks } from "./utils/markdown";
|
|
19
|
+
import type {
|
|
20
|
+
ClassificationArtifact,
|
|
21
|
+
Finding,
|
|
22
|
+
RunDetail,
|
|
23
|
+
RunManifest,
|
|
24
|
+
Severity,
|
|
25
|
+
Summary,
|
|
26
|
+
} from "./utils/types";
|
|
27
|
+
|
|
28
|
+
const STATUS_VIEW = {
|
|
29
|
+
blocking: { label: "Mudanças necessárias", tone: "blocking" },
|
|
30
|
+
attention: { label: "Aprovado com ressalvas", tone: "attention" },
|
|
31
|
+
clean: { label: "Aprovado", tone: "clean" },
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const STATUS_LABELS: Record<string, string> = {
|
|
35
|
+
added: "adicionado(s)",
|
|
36
|
+
modified: "modificado(s)",
|
|
37
|
+
deleted: "removido(s)",
|
|
38
|
+
renamed: "renomeado(s)",
|
|
39
|
+
copied: "copiado(s)",
|
|
40
|
+
unknown: "alterado(s)",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Soma do churn quando o manifesto não traz os totais. */
|
|
44
|
+
function churnTotals(churn: ClassificationArtifact["churn"]): {
|
|
45
|
+
additions: number;
|
|
46
|
+
deletions: number;
|
|
47
|
+
} {
|
|
48
|
+
let additions = 0;
|
|
49
|
+
let deletions = 0;
|
|
50
|
+
for (const entry of Object.values(churn ?? {})) {
|
|
51
|
+
additions += entry.additions;
|
|
52
|
+
deletions += entry.deletions;
|
|
53
|
+
}
|
|
54
|
+
return { additions, deletions };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ResumeKpi({
|
|
58
|
+
label,
|
|
59
|
+
value,
|
|
60
|
+
hint,
|
|
61
|
+
tone,
|
|
62
|
+
}: {
|
|
63
|
+
label: string;
|
|
64
|
+
value: ComponentChildren;
|
|
65
|
+
hint?: string;
|
|
66
|
+
tone?: "blocking" | "attention" | "total";
|
|
67
|
+
}) {
|
|
68
|
+
return (
|
|
69
|
+
<div class={`kpi${tone ? ` kpi-${tone}` : ""}`} title={hint}>
|
|
70
|
+
<span class="kpi-label">{label}</span>
|
|
71
|
+
<span class="kpi-value">{value}</span>
|
|
72
|
+
{hint && <span class="kpi-hint">{hint}</span>}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function ResumePanel({
|
|
78
|
+
slug,
|
|
79
|
+
branch,
|
|
80
|
+
findings,
|
|
81
|
+
summary,
|
|
82
|
+
onOpenArtifacts,
|
|
83
|
+
}: {
|
|
84
|
+
slug: string;
|
|
85
|
+
branch: string;
|
|
86
|
+
findings: Finding[];
|
|
87
|
+
summary: Summary;
|
|
88
|
+
onOpenArtifacts: () => void;
|
|
89
|
+
}) {
|
|
90
|
+
const [resume, setResume] = useState("");
|
|
91
|
+
const [detail, setDetail] = useState<RunDetail | null>(null);
|
|
92
|
+
const [classification, setClassification] = useState<ClassificationArtifact | null>(
|
|
93
|
+
null,
|
|
94
|
+
);
|
|
95
|
+
const [error, setError] = useState("");
|
|
96
|
+
const [loading, setLoading] = useState(true);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
let cancelled = false;
|
|
100
|
+
setLoading(true);
|
|
101
|
+
setError("");
|
|
102
|
+
Promise.all([
|
|
103
|
+
api<RunDetail>(`/api/runs/${slug}`).catch(() => null),
|
|
104
|
+
fetch(`/api/runs/${slug}/artifacts/review-resume.md`)
|
|
105
|
+
.then((res) => (res.ok ? res.text() : ""))
|
|
106
|
+
.catch(() => ""),
|
|
107
|
+
api<ClassificationArtifact>(
|
|
108
|
+
`/api/runs/${slug}/artifacts/classification.json`,
|
|
109
|
+
).catch(() => null),
|
|
110
|
+
])
|
|
111
|
+
.then(([runDetail, markdown, classified]) => {
|
|
112
|
+
if (cancelled) return;
|
|
113
|
+
setDetail(runDetail);
|
|
114
|
+
setResume(markdown);
|
|
115
|
+
setClassification(classified);
|
|
116
|
+
})
|
|
117
|
+
.catch((err) => {
|
|
118
|
+
if (!cancelled) setError(String(err));
|
|
119
|
+
})
|
|
120
|
+
.finally(() => {
|
|
121
|
+
if (!cancelled) setLoading(false);
|
|
122
|
+
});
|
|
123
|
+
return () => {
|
|
124
|
+
cancelled = true;
|
|
125
|
+
};
|
|
126
|
+
}, [slug]);
|
|
127
|
+
|
|
128
|
+
const manifest: RunManifest | null = detail?.manifest ?? null;
|
|
129
|
+
const business = useMemo(() => sectionBlocks(resume, "Resumo de negócio"), [resume]);
|
|
130
|
+
const status =
|
|
131
|
+
summary.bySeverity.blocking > 0
|
|
132
|
+
? STATUS_VIEW.blocking
|
|
133
|
+
: summary.bySeverity.attention > 0
|
|
134
|
+
? STATUS_VIEW.attention
|
|
135
|
+
: STATUS_VIEW.clean;
|
|
136
|
+
|
|
137
|
+
const files = classification?.files ?? {};
|
|
138
|
+
const fileCount = Object.keys(files).length;
|
|
139
|
+
/** Arquivos com conteúdo para revisar (deletados não entram no escopo). */
|
|
140
|
+
const reviewableCount = Object.values(files).filter(
|
|
141
|
+
(file) => file.status !== "deleted",
|
|
142
|
+
).length;
|
|
143
|
+
const statuses = useMemo(() => {
|
|
144
|
+
const counts: Record<string, number> = {};
|
|
145
|
+
for (const file of Object.values(files)) {
|
|
146
|
+
counts[file.status] = (counts[file.status] ?? 0) + 1;
|
|
147
|
+
}
|
|
148
|
+
return counts;
|
|
149
|
+
}, [files]);
|
|
150
|
+
const statusLabel = Object.entries(statuses)
|
|
151
|
+
.map(([state, total]) => `${total} ${STATUS_LABELS[state] ?? state}`)
|
|
152
|
+
.join(" · ");
|
|
153
|
+
const churn = manifest?.churn ?? churnTotals(classification?.churn);
|
|
154
|
+
const pr = manifest?.context?.source;
|
|
155
|
+
const reviewedCount = useMemo(() => {
|
|
156
|
+
const set = new Set<string>();
|
|
157
|
+
for (const domain of manifest?.watchdogs ?? []) {
|
|
158
|
+
for (const file of domain.files) set.add(file);
|
|
159
|
+
}
|
|
160
|
+
return set.size;
|
|
161
|
+
}, [manifest]);
|
|
162
|
+
const coverage =
|
|
163
|
+
manifest && reviewableCount > 0
|
|
164
|
+
? `${Math.min(100, Math.round((reviewedCount / reviewableCount) * 100))}%`
|
|
165
|
+
: "—";
|
|
166
|
+
|
|
167
|
+
const domains = useMemo(() => {
|
|
168
|
+
const base = manifest?.watchdogs?.length
|
|
169
|
+
? manifest.watchdogs.map((domain) => ({
|
|
170
|
+
id: domain.id,
|
|
171
|
+
name: domain.name || domain.id,
|
|
172
|
+
files: domain.files.length,
|
|
173
|
+
overflow: domain.overflow,
|
|
174
|
+
}))
|
|
175
|
+
: (detail?.watchdogs ?? []).map((id) => ({
|
|
176
|
+
id,
|
|
177
|
+
name: id,
|
|
178
|
+
files: 0,
|
|
179
|
+
overflow: 0,
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
return base
|
|
183
|
+
.map((domain) => {
|
|
184
|
+
const bySeverity: Record<Severity, number> = {
|
|
185
|
+
blocking: 0,
|
|
186
|
+
attention: 0,
|
|
187
|
+
suggestion: 0,
|
|
188
|
+
nit: 0,
|
|
189
|
+
};
|
|
190
|
+
for (const finding of findings) {
|
|
191
|
+
if (!finding.watchdogIds.includes(domain.id)) continue;
|
|
192
|
+
bySeverity[finding.severity] += 1;
|
|
193
|
+
}
|
|
194
|
+
const total = SEVERITY_ORDER.reduce(
|
|
195
|
+
(sum, severity) => sum + bySeverity[severity],
|
|
196
|
+
0,
|
|
197
|
+
);
|
|
198
|
+
return { ...domain, bySeverity, total };
|
|
199
|
+
})
|
|
200
|
+
.sort(
|
|
201
|
+
(a, b) =>
|
|
202
|
+
b.bySeverity.blocking - a.bySeverity.blocking ||
|
|
203
|
+
b.bySeverity.attention - a.bySeverity.attention ||
|
|
204
|
+
b.total - a.total ||
|
|
205
|
+
a.name.localeCompare(b.name),
|
|
206
|
+
);
|
|
207
|
+
}, [manifest, detail, findings]);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div class="resume">
|
|
211
|
+
<section class="resume-hero">
|
|
212
|
+
<div class="resume-hero-text">
|
|
213
|
+
<span class="resume-kicker">Resumo principal</span>
|
|
214
|
+
<h2 class="resume-title">{branch}</h2>
|
|
215
|
+
<p class="resume-meta">
|
|
216
|
+
{manifest?.generatedAt && (
|
|
217
|
+
<span>{formatRunTime(manifest.generatedAt)}</span>
|
|
218
|
+
)}
|
|
219
|
+
{domains.length > 0 && (
|
|
220
|
+
<span>
|
|
221
|
+
{domains.length}{" "}
|
|
222
|
+
{domains.length === 1 ? "domínio" : "domínios"}
|
|
223
|
+
</span>
|
|
224
|
+
)}
|
|
225
|
+
{pr?.url ? (
|
|
226
|
+
<a
|
|
227
|
+
class="resume-pr"
|
|
228
|
+
href={pr.url}
|
|
229
|
+
target="_blank"
|
|
230
|
+
rel="noreferrer"
|
|
231
|
+
>
|
|
232
|
+
{pr.number ? `PR #${pr.number}` : "Abrir PR"}
|
|
233
|
+
<ExternalLink size={12} />
|
|
234
|
+
</a>
|
|
235
|
+
) : pr?.number ? (
|
|
236
|
+
<span>PR #{pr.number}</span>
|
|
237
|
+
) : null}
|
|
238
|
+
</p>
|
|
239
|
+
</div>
|
|
240
|
+
<div class="resume-hero-side">
|
|
241
|
+
<span class={`resume-status ${status.tone}`}>{status.label}</span>
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
class="btn"
|
|
245
|
+
title="Ver os artefatos brutos da revisão"
|
|
246
|
+
onClick={onOpenArtifacts}
|
|
247
|
+
>
|
|
248
|
+
<Layers size={14} />
|
|
249
|
+
Ver artefatos
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</section>
|
|
253
|
+
|
|
254
|
+
{error && <p class="error">{error}</p>}
|
|
255
|
+
|
|
256
|
+
<div class="kpis resume-kpis">
|
|
257
|
+
<ResumeKpi
|
|
258
|
+
label="Arquivos"
|
|
259
|
+
value={fileCount}
|
|
260
|
+
hint={statusLabel || "sem classificação"}
|
|
261
|
+
/>
|
|
262
|
+
<ResumeKpi
|
|
263
|
+
label="Churn"
|
|
264
|
+
value={
|
|
265
|
+
<>
|
|
266
|
+
<span class="churn-add">+{churn.additions}</span>
|
|
267
|
+
<span class="churn-sep">/</span>
|
|
268
|
+
<span class="churn-del">-{churn.deletions}</span>
|
|
269
|
+
</>
|
|
270
|
+
}
|
|
271
|
+
hint="linhas adicionadas / removidas"
|
|
272
|
+
/>
|
|
273
|
+
<ResumeKpi
|
|
274
|
+
label="Cobertura"
|
|
275
|
+
value={coverage}
|
|
276
|
+
hint={
|
|
277
|
+
reviewableCount > 0
|
|
278
|
+
? `${reviewedCount}/${reviewableCount} revisáveis no escopo`
|
|
279
|
+
: "sem classificação"
|
|
280
|
+
}
|
|
281
|
+
/>
|
|
282
|
+
<ResumeKpi
|
|
283
|
+
label="Achados"
|
|
284
|
+
value={summary.total}
|
|
285
|
+
hint={`${summary.bySeverity.blocking} bloqueantes · ${summary.bySeverity.attention} atenção`}
|
|
286
|
+
tone={
|
|
287
|
+
summary.bySeverity.blocking > 0
|
|
288
|
+
? "blocking"
|
|
289
|
+
: summary.bySeverity.attention > 0
|
|
290
|
+
? "attention"
|
|
291
|
+
: "total"
|
|
292
|
+
}
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div class="resume-grid">
|
|
297
|
+
<section class="resume-card">
|
|
298
|
+
<h3 class="resume-card-title">Resumo de negócio</h3>
|
|
299
|
+
{loading ? (
|
|
300
|
+
<p class="empty">Carregando resumo…</p>
|
|
301
|
+
) : business.length > 0 ? (
|
|
302
|
+
<div class="resume-narrative">
|
|
303
|
+
<MarkdownBlocks blocks={business} />
|
|
304
|
+
</div>
|
|
305
|
+
) : (
|
|
306
|
+
<p class="empty">
|
|
307
|
+
O resumo narrativo ainda não foi gerado para esta run. Rode o{" "}
|
|
308
|
+
<code>--finalize</code> para criar{" "}
|
|
309
|
+
<code>review-resume.md</code>.
|
|
310
|
+
</p>
|
|
311
|
+
)}
|
|
312
|
+
</section>
|
|
313
|
+
|
|
314
|
+
<section class="resume-card">
|
|
315
|
+
<div class="resume-card-head">
|
|
316
|
+
<h3 class="resume-card-title">Domínios</h3>
|
|
317
|
+
{domains.length > 0 && (
|
|
318
|
+
<span class="resume-card-count">{domains.length}</span>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
{domains.length > 0 ? (
|
|
322
|
+
<ul class="resume-domains">
|
|
323
|
+
{domains.map((domain) => (
|
|
324
|
+
<li class="resume-domain" key={domain.id}>
|
|
325
|
+
<div class="resume-domain-head">
|
|
326
|
+
<span class="resume-domain-name">
|
|
327
|
+
{domain.name}
|
|
328
|
+
</span>
|
|
329
|
+
<span class="resume-domain-files">
|
|
330
|
+
{domain.files > 0
|
|
331
|
+
? `${domain.files} ${domain.files === 1 ? "arquivo" : "arquivos"}`
|
|
332
|
+
: "—"}
|
|
333
|
+
{domain.overflow > 0
|
|
334
|
+
? ` · ${domain.overflow} fora do orçamento`
|
|
335
|
+
: ""}
|
|
336
|
+
</span>
|
|
337
|
+
</div>
|
|
338
|
+
<div class="resume-domain-sev">
|
|
339
|
+
{domain.total === 0 ? (
|
|
340
|
+
<span class="resume-domain-ok">
|
|
341
|
+
Sem achados
|
|
342
|
+
</span>
|
|
343
|
+
) : (
|
|
344
|
+
SEVERITY_ORDER.filter(
|
|
345
|
+
(severity) =>
|
|
346
|
+
domain.bySeverity[severity] > 0,
|
|
347
|
+
).map((severity) => (
|
|
348
|
+
<span
|
|
349
|
+
class={`sev-badge sev-${severity}`}
|
|
350
|
+
key={severity}
|
|
351
|
+
>
|
|
352
|
+
{SEVERITY_LABELS[severity]}{" "}
|
|
353
|
+
{domain.bySeverity[severity]}
|
|
354
|
+
</span>
|
|
355
|
+
))
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
</li>
|
|
359
|
+
))}
|
|
360
|
+
</ul>
|
|
361
|
+
) : (
|
|
362
|
+
<p class="empty">Nenhum domínio ativo na revisão.</p>
|
|
363
|
+
)}
|
|
364
|
+
</section>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* com data/hora, relação de findings presentes e badge de correção pronta.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { describeRunStatus } from "./constants";
|
|
7
|
-
import { formatRunTime } from "./format";
|
|
8
6
|
import { BranchIcon, Clock, Sparkles } from "./icons";
|
|
9
|
-
import
|
|
7
|
+
import { describeRunStatus } from "./utils/constants";
|
|
8
|
+
import { formatRunTime } from "./utils/format";
|
|
9
|
+
import type { RunSummary } from "./utils/types";
|
|
10
10
|
|
|
11
11
|
export function RunList({
|
|
12
12
|
runs,
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { GitHub, Send, Spinner } from "./icons";
|
|
13
|
+
import { api } from "./utils/api";
|
|
14
|
+
import { EXEC_STATES, SEVERITY_LABELS, SEVERITY_ORDER } from "./utils/constants";
|
|
15
15
|
import type {
|
|
16
16
|
ExecState,
|
|
17
17
|
Severity,
|
|
18
18
|
SubmitOutcome,
|
|
19
19
|
SubmitPreview,
|
|
20
20
|
SubmitResponse,
|
|
21
|
-
} from "./types";
|
|
21
|
+
} from "./utils/types";
|
|
22
22
|
|
|
23
23
|
const EXEC_LABELS: Record<ExecState, string> = {
|
|
24
24
|
pending: "Pendente",
|
|
@@ -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
|
)}
|