@payconductor/houndsman 0.13.0 → 0.14.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 +6 -1
- package/package.json +1 -1
- package/src/cli/commands/compare-command.ts +112 -0
- package/src/cli/commands/context-command.ts +1 -0
- package/src/cli/commands/findings-command.ts +11 -1
- package/src/cli/commands/hunt-command.ts +20 -3
- package/src/cli/commands/review-command.ts +38 -0
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/index.ts +3 -1
- package/src/cli/progress.ts +7 -2
- package/src/core/ai/ai-client.ts +38 -2
- package/src/core/ai/features/brief-enricher.ts +1 -1
- package/src/core/ai/features/business-analyzer.ts +61 -0
- package/src/core/ai/features/business-summarizer.ts +44 -0
- package/src/core/ai/index.ts +8 -0
- package/src/core/ai/prompts/brief.ts +33 -7
- package/src/core/ai/prompts/business.ts +76 -0
- package/src/core/ai/provider-capabilities.ts +12 -0
- package/src/core/ai/schemas.ts +70 -2
- package/src/core/ai/usage-recorder.ts +53 -1
- package/src/core/brief.ts +246 -12
- package/src/core/builtins.ts +108 -0
- package/src/core/business-summary.ts +583 -0
- package/src/core/commands/compare.ts +233 -0
- package/src/core/commands/context.ts +6 -2
- package/src/core/commands/findings.ts +38 -30
- package/src/core/commands/review.ts +6 -0
- package/src/core/commands/scaffold.ts +27 -0
- package/src/core/commands/status.ts +9 -4
- package/src/core/compare.ts +243 -0
- package/src/core/concurrency.ts +30 -0
- package/src/core/config.ts +31 -3
- package/src/core/defaults.ts +50 -4
- package/src/core/domain/budget.ts +62 -4
- package/src/core/domain/classification.ts +12 -2
- package/src/core/engine/carryover.ts +4 -0
- package/src/core/engine/finalize-pipeline.ts +347 -0
- package/src/core/engine/prepare-pipeline.ts +1294 -0
- package/src/core/engine/review-context.ts +51 -0
- package/src/core/engine/review-options.ts +47 -0
- package/src/core/engine/review-scope.ts +14 -1
- package/src/core/engine/review-session.ts +11 -13
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/findings-state.ts +13 -1
- package/src/core/github-review.ts +5 -2
- package/src/core/hunt/hunt-runner.ts +90 -0
- package/src/core/hunt/index.ts +2 -0
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-prompt.ts +13 -1
- package/src/core/hunt/pool.ts +4 -26
- package/src/core/logging/index.ts +6 -1
- package/src/core/logging/progress.ts +85 -1
- package/src/core/orchestrator.ts +33 -1289
- package/src/core/paths.ts +6 -3
- package/src/core/pr-context.ts +2 -4
- package/src/core/prepare-report.ts +61 -0
- package/src/core/project-config.ts +8 -1
- package/src/core/propagation.ts +127 -0
- package/src/core/registry-check.ts +60 -21
- package/src/core/renderers/manifest-writer.ts +13 -0
- package/src/core/renderers/resume-renderer.ts +151 -11
- package/src/core/report-builder.ts +108 -278
- package/src/core/storage/file/file-storage.ts +7 -3
- package/src/core/storage/sqlite/mappers.ts +10 -0
- package/src/core/storage/sqlite/schema.ts +104 -23
- package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
- package/src/core/storage/types.ts +16 -4
- package/src/core/storage/work-area.ts +59 -1
- package/src/core/types.ts +31 -0
- package/src/core/watchdog-registry.ts +21 -3
- package/src/index.ts +29 -4
- package/src/server/client/ArtifactsPanel.tsx +4 -2
- package/src/server/client/FindingCard.tsx +21 -1
- package/src/server/client/RunsSection.tsx +5 -0
- package/src/server/client/utils/constants.ts +21 -11
- package/src/server/client/utils/markdown.ts +1 -1
- package/src/server/client/utils/types.ts +14 -2
- package/src/server/run-repository.ts +35 -14
- package/src/server/styles.css +2 -0
- package/src/skill/OUTPUT-FORMAT.md +36 -10
- package/src/skill/README.md +18 -14
- package/src/skill/SETUP.md +1 -0
- package/src/skill/SKILL.md +13 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +145 -58
- package/src/tests/__snapshots__/baseline.test.ts.snap +187 -178
- package/src/tests/ai-core.test.ts +46 -0
- package/src/tests/ai-features.test.ts +95 -4
- package/src/tests/baseline.test.ts +29 -121
- package/src/tests/brief-scope.test.ts +250 -0
- package/src/tests/business-summary.test.ts +258 -0
- package/src/tests/compare.test.ts +233 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/hunt-maki.test.ts +58 -0
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +29 -1
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +38 -29
- package/src/tests/review-scripts.test.ts +106 -218
- package/src/tests/server.test.ts +7 -4
- package/src/tests/storage.test.ts +194 -9
- package/src/core/renderers/report-renderer.ts +0 -66
- package/src/skill/example-review.md +0 -66
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando `compare` (biblioteca) — compara duas runs da mesma branch.
|
|
3
|
+
*
|
|
4
|
+
* Resolve o par por slug (como na URL da UI) ou por `(branch, variante)`, lê os
|
|
5
|
+
* artefatos de cada review e grava `compare-<a>--<b>.md|.json` na run B.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
buildComparison,
|
|
10
|
+
type ComparisonFinding,
|
|
11
|
+
comparisonToJson,
|
|
12
|
+
countBySeverity,
|
|
13
|
+
type ReviewSnapshot,
|
|
14
|
+
renderComparison,
|
|
15
|
+
} from "../compare";
|
|
16
|
+
import { currentBranch } from "../pr-context";
|
|
17
|
+
import type { RepoIdentity, ReviewRecord, StorageProvider } from "../storage/types";
|
|
18
|
+
import type { Severity } from "../types";
|
|
19
|
+
|
|
20
|
+
/** Seleção do par A/B. */
|
|
21
|
+
export type CompareOptions = {
|
|
22
|
+
/** Slugs prontos (como exibidos na UI). */
|
|
23
|
+
slugA?: string;
|
|
24
|
+
slugB?: string;
|
|
25
|
+
/** Identidade alternativa: branch + rótulos de variante. */
|
|
26
|
+
branch?: string;
|
|
27
|
+
variantA?: string;
|
|
28
|
+
variantB?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Resultado do comando. */
|
|
32
|
+
export type CompareResult = {
|
|
33
|
+
markdown: string;
|
|
34
|
+
json: string;
|
|
35
|
+
a: ReviewRecord;
|
|
36
|
+
b: ReviewRecord;
|
|
37
|
+
artifacts: { markdown: string; json: string };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type ManifestShape = {
|
|
41
|
+
watchdogs?: { files?: string[]; overflow?: number }[];
|
|
42
|
+
ai?: { model?: string };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type ReportShape = { durationMs?: number };
|
|
46
|
+
type HuntShape = {
|
|
47
|
+
model?: string;
|
|
48
|
+
durationMs?: number;
|
|
49
|
+
totals?: { totalTokens?: number; costUsd?: number };
|
|
50
|
+
};
|
|
51
|
+
type AiUsageShape = { total?: { totalTokens?: number } };
|
|
52
|
+
|
|
53
|
+
/** Resolve a run pelo slug dentro do repositório. */
|
|
54
|
+
function bySlug(
|
|
55
|
+
storage: StorageProvider,
|
|
56
|
+
repo: RepoIdentity,
|
|
57
|
+
slug: string,
|
|
58
|
+
): ReviewRecord {
|
|
59
|
+
const review = storage
|
|
60
|
+
.listReviews(repo.key)
|
|
61
|
+
.find((candidate) => candidate.slug === slug);
|
|
62
|
+
if (!review) throw new Error(`Nenhuma run com slug '${slug}' neste repositório.`);
|
|
63
|
+
return review;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Resolve o par A/B (slug ou branch + variantes). */
|
|
67
|
+
export function resolveComparePair(
|
|
68
|
+
storage: StorageProvider,
|
|
69
|
+
repo: RepoIdentity,
|
|
70
|
+
options: CompareOptions,
|
|
71
|
+
): [ReviewRecord, ReviewRecord] {
|
|
72
|
+
if (options.slugA && options.slugB) {
|
|
73
|
+
return [
|
|
74
|
+
bySlug(storage, repo, options.slugA),
|
|
75
|
+
bySlug(storage, repo, options.slugB),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
const branch = options.branch ?? currentBranch();
|
|
79
|
+
const variantB = options.variantB?.trim();
|
|
80
|
+
if (!variantB) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
"Informe --b <variante> (ou passe dois slugs: `houndsman compare <a> <b>`).",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const a = storage.findReview(repo.key, branch, options.variantA);
|
|
86
|
+
const b = storage.findReview(repo.key, branch, variantB);
|
|
87
|
+
if (!a) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Nenhuma revisão para '${branch}'${options.variantA ? ` (variante '${options.variantA}')` : ""}.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (!b) throw new Error(`Nenhuma revisão para '${branch}' (variante '${variantB}').`);
|
|
93
|
+
return [a, b];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readJson<T>(storage: StorageProvider, reviewId: string, name: string): T | null {
|
|
97
|
+
const artifact = storage.readArtifact(reviewId, name);
|
|
98
|
+
if (!artifact) return null;
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(artifact.content) as T;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Extrai o snapshot comparável de uma run. */
|
|
107
|
+
function snapshotOf(
|
|
108
|
+
storage: StorageProvider,
|
|
109
|
+
review: ReviewRecord,
|
|
110
|
+
label: string,
|
|
111
|
+
): ReviewSnapshot {
|
|
112
|
+
const state = storage.readFindings(review.id);
|
|
113
|
+
const findings: ComparisonFinding[] = Object.values(state?.findings ?? {})
|
|
114
|
+
.filter((finding) => finding.present)
|
|
115
|
+
.map((finding) => ({
|
|
116
|
+
fingerprint: finding.fingerprint,
|
|
117
|
+
rule: finding.rule,
|
|
118
|
+
severity: finding.severity,
|
|
119
|
+
file: finding.file,
|
|
120
|
+
line: finding.line ?? undefined,
|
|
121
|
+
message: finding.message,
|
|
122
|
+
watchdogs: finding.watchdogIds,
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
const manifest = readJson<ManifestShape>(storage, review.id, "manifest.json");
|
|
126
|
+
const prepare = readJson<ReportShape>(storage, review.id, "prepare-report.json");
|
|
127
|
+
const hunt = readJson<HuntShape>(storage, review.id, "hunt-report.json");
|
|
128
|
+
const aiUsage = readJson<AiUsageShape>(storage, review.id, "ai-usage.json");
|
|
129
|
+
|
|
130
|
+
const started = review.startedAt ? Date.parse(review.startedAt) : Number.NaN;
|
|
131
|
+
const reviewed = review.reviewedAt ? Date.parse(review.reviewedAt) : Number.NaN;
|
|
132
|
+
const durationMs =
|
|
133
|
+
Number.isFinite(started) && Number.isFinite(reviewed) && reviewed >= started
|
|
134
|
+
? reviewed - started
|
|
135
|
+
: undefined;
|
|
136
|
+
|
|
137
|
+
const watchdogs = manifest?.watchdogs ?? [];
|
|
138
|
+
// Domínios se sobrepõem: conta arquivos únicos, não "slots" por watchdog.
|
|
139
|
+
const files = new Set<string>();
|
|
140
|
+
for (const watchdog of watchdogs) {
|
|
141
|
+
for (const file of watchdog.files ?? []) files.add(file);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
label,
|
|
145
|
+
slug: review.slug,
|
|
146
|
+
branch: review.branch,
|
|
147
|
+
variant: review.variant,
|
|
148
|
+
headSha: review.headSha ?? review.reviewedSha,
|
|
149
|
+
model: hunt?.model ?? manifest?.ai?.model,
|
|
150
|
+
findings,
|
|
151
|
+
bySeverity: countBySeverity(findings),
|
|
152
|
+
coverage: {
|
|
153
|
+
watchdogs: watchdogs.length,
|
|
154
|
+
files: files.size,
|
|
155
|
+
overflow: watchdogs.reduce(
|
|
156
|
+
(total, watchdog) => total + (watchdog.overflow ?? 0),
|
|
157
|
+
0,
|
|
158
|
+
),
|
|
159
|
+
},
|
|
160
|
+
durationMs,
|
|
161
|
+
prepareMs: prepare?.durationMs,
|
|
162
|
+
huntMs: hunt?.durationMs,
|
|
163
|
+
tokens: hunt?.totals?.totalTokens ?? aiUsage?.total?.totalTokens,
|
|
164
|
+
costUsd: hunt?.totals?.costUsd,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Executa a comparação e persiste o relatório na run B. */
|
|
169
|
+
export function runCompare(
|
|
170
|
+
storage: StorageProvider,
|
|
171
|
+
repo: RepoIdentity,
|
|
172
|
+
options: CompareOptions,
|
|
173
|
+
): CompareResult {
|
|
174
|
+
const [a, b] = resolveComparePair(storage, repo, options);
|
|
175
|
+
const comparison = buildComparison(
|
|
176
|
+
snapshotOf(storage, a, "A"),
|
|
177
|
+
snapshotOf(storage, b, "B"),
|
|
178
|
+
);
|
|
179
|
+
const markdown = renderComparison(comparison);
|
|
180
|
+
const json = comparisonToJson(comparison);
|
|
181
|
+
const base = `compare-${a.slug}--${b.slug}`;
|
|
182
|
+
storage.writeArtifact(b.id, {
|
|
183
|
+
name: `${base}.md`,
|
|
184
|
+
kind: "compare",
|
|
185
|
+
content: markdown,
|
|
186
|
+
});
|
|
187
|
+
storage.writeArtifact(b.id, {
|
|
188
|
+
name: `${base}.json`,
|
|
189
|
+
kind: "compare",
|
|
190
|
+
content: json,
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
markdown,
|
|
194
|
+
json,
|
|
195
|
+
a,
|
|
196
|
+
b,
|
|
197
|
+
artifacts: { markdown: `${base}.md`, json: `${base}.json` },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Linha da listagem de variantes da branch. */
|
|
202
|
+
export type CompareListItem = {
|
|
203
|
+
slug: string;
|
|
204
|
+
branch: string;
|
|
205
|
+
variant?: string;
|
|
206
|
+
status: ReviewRecord["status"];
|
|
207
|
+
findings: number;
|
|
208
|
+
bySeverity: Record<Severity, number>;
|
|
209
|
+
updatedAt: string;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/** Lista as runs do repositório (filtra por branch quando informado). */
|
|
213
|
+
export function listRunsForCompare(
|
|
214
|
+
storage: StorageProvider,
|
|
215
|
+
repo: RepoIdentity,
|
|
216
|
+
branch?: string,
|
|
217
|
+
): CompareListItem[] {
|
|
218
|
+
return storage
|
|
219
|
+
.listReviews(repo.key)
|
|
220
|
+
.filter((review) => !branch || review.branch === branch)
|
|
221
|
+
.map((review) => {
|
|
222
|
+
const snapshot = snapshotOf(storage, review, review.variant ?? "principal");
|
|
223
|
+
return {
|
|
224
|
+
slug: review.slug,
|
|
225
|
+
branch: review.branch,
|
|
226
|
+
variant: review.variant,
|
|
227
|
+
status: review.status,
|
|
228
|
+
findings: snapshot.findings.length,
|
|
229
|
+
bySeverity: snapshot.bySeverity,
|
|
230
|
+
updatedAt: review.updatedAt,
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type PrContextRaw,
|
|
13
13
|
renderContextBrief,
|
|
14
14
|
} from "../pr-context";
|
|
15
|
-
import {
|
|
15
|
+
import { reviewSlug } from "../report-builder";
|
|
16
16
|
import type { RepoIdentity, StorageProvider } from "../storage/types";
|
|
17
17
|
import { type ContextPaths, ReviewWorkArea } from "../storage/work-area";
|
|
18
18
|
|
|
@@ -28,6 +28,8 @@ export function runContextCommand(
|
|
|
28
28
|
repo: RepoIdentity,
|
|
29
29
|
opts: {
|
|
30
30
|
branch?: string;
|
|
31
|
+
/** Variante A/B da run (ausente = principal). */
|
|
32
|
+
variant?: string;
|
|
31
33
|
/** Worktree onde a review roda (persistido na review). */
|
|
32
34
|
worktreePath?: string;
|
|
33
35
|
/** PR de origem (persistida na review). */
|
|
@@ -35,10 +37,12 @@ export function runContextCommand(
|
|
|
35
37
|
} = {},
|
|
36
38
|
): ContextRunResult {
|
|
37
39
|
const branch = opts.branch ?? currentBranch();
|
|
40
|
+
const variant = opts.variant?.trim() || undefined;
|
|
38
41
|
const review = storage.getOrCreateReview({
|
|
39
42
|
repo,
|
|
40
43
|
branch,
|
|
41
|
-
|
|
44
|
+
variant,
|
|
45
|
+
slug: reviewSlug(branch, variant),
|
|
42
46
|
worktreePath: opts.worktreePath,
|
|
43
47
|
prNumber: opts.prNumber,
|
|
44
48
|
});
|
|
@@ -14,6 +14,7 @@ import { existsSync } from "node:fs";
|
|
|
14
14
|
import { AiClient } from "../ai/ai-client";
|
|
15
15
|
import { AiVerifier } from "../ai/features/verifier";
|
|
16
16
|
import { type HoundsmanConfig, resolveJevConcurrency } from "../config";
|
|
17
|
+
import { type ReviewContext, reviewContext } from "../engine/review-context";
|
|
17
18
|
import { ENV_NAMES } from "../env";
|
|
18
19
|
import {
|
|
19
20
|
applyFindingUpdate,
|
|
@@ -25,7 +26,6 @@ import {
|
|
|
25
26
|
type FindingsFilter,
|
|
26
27
|
type FindingsStateFile,
|
|
27
28
|
type FindingUpdate,
|
|
28
|
-
fileReader,
|
|
29
29
|
filterFindings,
|
|
30
30
|
reorder,
|
|
31
31
|
resolveFindingId,
|
|
@@ -51,9 +51,9 @@ import { VerifyPresenter } from "../renderers/verify-presenter";
|
|
|
51
51
|
import { pingFindingsStatus } from "../run-status";
|
|
52
52
|
import { systemClock } from "../services/clock";
|
|
53
53
|
import { FixBatchBuilder } from "../services/fix-batch-builder";
|
|
54
|
+
import type { GitRepository } from "../services/git-repository";
|
|
54
55
|
import { Verifier } from "../services/verifier";
|
|
55
|
-
import type { RepoIdentity,
|
|
56
|
-
import { ReviewWorkArea } from "../storage/work-area";
|
|
56
|
+
import type { RepoIdentity, StorageProvider } from "../storage/types";
|
|
57
57
|
import { SEVERITY_ORDER, type Severity } from "../types";
|
|
58
58
|
|
|
59
59
|
export type FindingsCommandResult = {
|
|
@@ -68,6 +68,7 @@ const VALUE_FLAGS = new Set([
|
|
|
68
68
|
"--severity",
|
|
69
69
|
"--file",
|
|
70
70
|
"--branch",
|
|
71
|
+
"--variant",
|
|
71
72
|
"--limit",
|
|
72
73
|
"--comment",
|
|
73
74
|
"--by",
|
|
@@ -108,13 +109,11 @@ function parseArgs(args: string[]): ParsedArgs {
|
|
|
108
109
|
return parsed;
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
/**
|
|
112
|
-
type
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
/** Worktree da review quando existir; senão, a raiz do repositório. */
|
|
117
|
-
root: string;
|
|
112
|
+
/** Opções de resolução do contexto (o CLI passa root/git do engine). */
|
|
113
|
+
export type FindingsContextOptions = {
|
|
114
|
+
/** Raiz operacional (default: cwd de quem chamou). */
|
|
115
|
+
root?: string;
|
|
116
|
+
git?: GitRepository;
|
|
118
117
|
};
|
|
119
118
|
|
|
120
119
|
/** Resolve a review da branch atual (explícita ou via git). */
|
|
@@ -122,22 +121,25 @@ function openContext(
|
|
|
122
121
|
storage: StorageProvider,
|
|
123
122
|
repo: RepoIdentity,
|
|
124
123
|
parsed: ParsedArgs,
|
|
125
|
-
|
|
124
|
+
options: FindingsContextOptions = {},
|
|
125
|
+
): ReviewContext {
|
|
126
126
|
const branch = parsed.values["--branch"] ?? currentBranch();
|
|
127
|
-
const
|
|
127
|
+
const variant = parsed.values["--variant"]?.trim() || undefined;
|
|
128
|
+
const review = storage.findReview(repo.key, branch, variant);
|
|
128
129
|
if (!review) {
|
|
129
130
|
throw new Error(
|
|
130
|
-
`Nenhuma revisão para '${branch}'. Rode \`houndsman --finalize\` primeiro.`,
|
|
131
|
+
`Nenhuma revisão para '${branch}'${variant ? ` (variante '${variant}')` : ""}. Rode \`houndsman --finalize\` primeiro.`,
|
|
131
132
|
);
|
|
132
133
|
}
|
|
133
134
|
const worktree = review.worktreePath;
|
|
134
135
|
// Sem worktree, mantém a raiz onde o comando foi invocado (comportamento histórico).
|
|
135
|
-
const root =
|
|
136
|
-
|
|
136
|
+
const root =
|
|
137
|
+
worktree && existsSync(worktree) ? worktree : (options.root ?? process.cwd());
|
|
138
|
+
return reviewContext(storage, review, { root, git: options.git });
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
/** Lê o estado de triagem; lança se a review ainda não tem findings. */
|
|
140
|
-
function loadState(ctx:
|
|
142
|
+
function loadState(ctx: ReviewContext): FindingsStateFile {
|
|
141
143
|
const state = ctx.storage.readFindings(ctx.review.id);
|
|
142
144
|
if (!state) {
|
|
143
145
|
throw new Error(
|
|
@@ -149,7 +151,7 @@ function loadState(ctx: CommandContext): FindingsStateFile {
|
|
|
149
151
|
|
|
150
152
|
/** Lê-modifica-grava o estado, atualizando `updatedAt` e a ordem. */
|
|
151
153
|
function mutateState(
|
|
152
|
-
ctx:
|
|
154
|
+
ctx: ReviewContext,
|
|
153
155
|
mutator: (state: FindingsStateFile) => void,
|
|
154
156
|
): FindingsStateFile {
|
|
155
157
|
const state = loadState(ctx);
|
|
@@ -221,7 +223,7 @@ function filterFrom(parsed: ParsedArgs): FindingsFilter {
|
|
|
221
223
|
}
|
|
222
224
|
|
|
223
225
|
/** Faz ping no status da review (derivado do estado atual) após uma mutação. */
|
|
224
|
-
function pingStatus(ctx:
|
|
226
|
+
function pingStatus(ctx: ReviewContext, state: FindingsStateFile): void {
|
|
225
227
|
pingFindingsStatus(ctx.storage, ctx.review.id, state);
|
|
226
228
|
}
|
|
227
229
|
|
|
@@ -230,7 +232,7 @@ function pingStatus(ctx: CommandContext, state: FindingsStateFile): void {
|
|
|
230
232
|
* Fail-open (`gh` ausente/sem PR ⇒ só a mensagem). Devolve a linha para o
|
|
231
233
|
* output ou `null` quando não há vínculo remoto pendente.
|
|
232
234
|
*/
|
|
233
|
-
function syncThreadsOutput(ctx:
|
|
235
|
+
function syncThreadsOutput(ctx: ReviewContext, ids: readonly string[]): string | null {
|
|
234
236
|
if (ids.length === 0) return null;
|
|
235
237
|
const state = loadState(ctx);
|
|
236
238
|
const outcome = syncResolvedThreads({ state, ids });
|
|
@@ -246,7 +248,7 @@ function syncThreadsOutput(ctx: CommandContext, ids: readonly string[]): string
|
|
|
246
248
|
|
|
247
249
|
/** Aplica uma atualização a um finding e persiste. */
|
|
248
250
|
function updateOne(
|
|
249
|
-
ctx:
|
|
251
|
+
ctx: ReviewContext,
|
|
250
252
|
input: string,
|
|
251
253
|
update: FindingUpdate,
|
|
252
254
|
): FindingState {
|
|
@@ -261,7 +263,7 @@ function updateOne(
|
|
|
261
263
|
|
|
262
264
|
/** Aplica a mesma atualização a vários ids (done/reset). */
|
|
263
265
|
function updateMany(
|
|
264
|
-
ctx:
|
|
266
|
+
ctx: ReviewContext,
|
|
265
267
|
inputs: string[],
|
|
266
268
|
update: FindingUpdate,
|
|
267
269
|
): FindingState[] {
|
|
@@ -281,10 +283,11 @@ export function runFindingsCommand(
|
|
|
281
283
|
storage: StorageProvider,
|
|
282
284
|
repo: RepoIdentity,
|
|
283
285
|
args: string[],
|
|
286
|
+
options: FindingsContextOptions = {},
|
|
284
287
|
): FindingsCommandResult {
|
|
285
288
|
try {
|
|
286
289
|
const parsed = parseArgs(args);
|
|
287
|
-
return execute(config, storage, repo, parsed);
|
|
290
|
+
return execute(config, storage, repo, parsed, options);
|
|
288
291
|
} catch (error) {
|
|
289
292
|
return {
|
|
290
293
|
output: error instanceof Error ? error.message : String(error),
|
|
@@ -298,8 +301,9 @@ function execute(
|
|
|
298
301
|
storage: StorageProvider,
|
|
299
302
|
repo: RepoIdentity,
|
|
300
303
|
parsed: ParsedArgs,
|
|
304
|
+
options: FindingsContextOptions,
|
|
301
305
|
): FindingsCommandResult {
|
|
302
|
-
const ctx = openContext(storage, repo, parsed);
|
|
306
|
+
const ctx = openContext(storage, repo, parsed, options);
|
|
303
307
|
switch (parsed.sub ?? "list") {
|
|
304
308
|
case "list": {
|
|
305
309
|
const state = loadState(ctx);
|
|
@@ -386,7 +390,7 @@ function execute(
|
|
|
386
390
|
}
|
|
387
391
|
case "sync": {
|
|
388
392
|
const state = loadState(ctx);
|
|
389
|
-
const result = syncState(state,
|
|
393
|
+
const result = syncState(state, ctx.files.reader());
|
|
390
394
|
ctx.storage.writeFindings(ctx.review.id, state);
|
|
391
395
|
pingStatus(ctx, state);
|
|
392
396
|
const output = parsed.bools.has("--json")
|
|
@@ -483,10 +487,11 @@ export async function runVerifyCommand(
|
|
|
483
487
|
storage: StorageProvider,
|
|
484
488
|
repo: RepoIdentity,
|
|
485
489
|
args: string[],
|
|
490
|
+
options: FindingsContextOptions = {},
|
|
486
491
|
): Promise<FindingsCommandResult> {
|
|
487
492
|
try {
|
|
488
493
|
const parsed = parseArgs(args);
|
|
489
|
-
return await executeVerify(config, storage, repo, parsed);
|
|
494
|
+
return await executeVerify(config, storage, repo, parsed, options);
|
|
490
495
|
} catch (error) {
|
|
491
496
|
return {
|
|
492
497
|
output: error instanceof Error ? error.message : String(error),
|
|
@@ -514,8 +519,9 @@ async function executeVerify(
|
|
|
514
519
|
storage: StorageProvider,
|
|
515
520
|
repo: RepoIdentity,
|
|
516
521
|
parsed: ParsedArgs,
|
|
522
|
+
options: FindingsContextOptions,
|
|
517
523
|
): Promise<FindingsCommandResult> {
|
|
518
|
-
const ctx = openContext(storage, repo, parsed);
|
|
524
|
+
const ctx = openContext(storage, repo, parsed, options);
|
|
519
525
|
const state = loadState(ctx);
|
|
520
526
|
|
|
521
527
|
// Lazy: `../jev` (axios) só entra no caminho do verify.
|
|
@@ -523,7 +529,7 @@ async function executeVerify(
|
|
|
523
529
|
const { JevCandidatesStore, JevClient, JevVerifier } = await import("../jev");
|
|
524
530
|
|
|
525
531
|
const findings = verifier.select(state, filterFrom(parsed));
|
|
526
|
-
const entries = verifier.verifyLocal(findings,
|
|
532
|
+
const entries = verifier.verifyLocal(findings, ctx.files.reader());
|
|
527
533
|
|
|
528
534
|
// Tier 1 — Jev só nos ambíguos (fail-open: sem chave/erro mantém o provisório).
|
|
529
535
|
let jevChecked = 0;
|
|
@@ -571,7 +577,7 @@ async function executeVerify(
|
|
|
571
577
|
} else if (!aiVerifier.enabled) {
|
|
572
578
|
aiReason = "feature ai.features.verify desligada";
|
|
573
579
|
} else {
|
|
574
|
-
const readFile =
|
|
580
|
+
const readFile = ctx.files.reader();
|
|
575
581
|
const byId = new Map(findings.map((finding) => [finding.id, finding]));
|
|
576
582
|
const confidence = await aiVerifier.verify({
|
|
577
583
|
findings: stillAmbiguous.map((entry) => ({
|
|
@@ -748,10 +754,11 @@ export async function runSubmitCommand(
|
|
|
748
754
|
storage: StorageProvider,
|
|
749
755
|
repo: RepoIdentity,
|
|
750
756
|
args: string[],
|
|
757
|
+
options: FindingsContextOptions = {},
|
|
751
758
|
): Promise<FindingsCommandResult> {
|
|
752
759
|
try {
|
|
753
760
|
const parsed = parseArgs(args);
|
|
754
|
-
return await executeSubmit(config, storage, repo, parsed);
|
|
761
|
+
return await executeSubmit(config, storage, repo, parsed, options);
|
|
755
762
|
} catch (error) {
|
|
756
763
|
return {
|
|
757
764
|
output: error instanceof Error ? error.message : String(error),
|
|
@@ -765,8 +772,9 @@ async function executeSubmit(
|
|
|
765
772
|
storage: StorageProvider,
|
|
766
773
|
repo: RepoIdentity,
|
|
767
774
|
parsed: ParsedArgs,
|
|
775
|
+
options: FindingsContextOptions,
|
|
768
776
|
): Promise<FindingsCommandResult> {
|
|
769
|
-
const ctx = openContext(storage, repo, parsed);
|
|
777
|
+
const ctx = openContext(storage, repo, parsed, options);
|
|
770
778
|
const state = loadState(ctx);
|
|
771
779
|
const selection = submitSelection(parsed);
|
|
772
780
|
const target = resolvePrTarget(defaultGhRunner, state.branch);
|
|
@@ -22,10 +22,16 @@ export function parseReviewArgs(args: string[]): ReviewOptions {
|
|
|
22
22
|
const branch = parser.value("--branch");
|
|
23
23
|
const pr = parser.value("--pr");
|
|
24
24
|
const worktree = parser.value("--worktree");
|
|
25
|
+
const variant = parser.value("--variant");
|
|
25
26
|
if (base) opts.base = base;
|
|
26
27
|
if (head) opts.head = head;
|
|
27
28
|
if (branch) opts.branch = branch;
|
|
28
29
|
if (worktree) opts.worktree = worktree;
|
|
30
|
+
if (variant !== undefined) {
|
|
31
|
+
const label = variant.trim();
|
|
32
|
+
if (!label) throw new Error("--variant não pode ser vazio.");
|
|
33
|
+
opts.variant = label;
|
|
34
|
+
}
|
|
29
35
|
if (pr) {
|
|
30
36
|
const number = Number.parseInt(pr, 10);
|
|
31
37
|
if (!Number.isFinite(number) || number <= 0) {
|
|
@@ -98,6 +98,15 @@ export const houndsmanConfig = {
|
|
|
98
98
|
watchdogs: WATCHDOG_DEFINITIONS,
|
|
99
99
|
exclusions: EXCLUSION_PATTERNS,
|
|
100
100
|
docMappings: DOC_MAPPINGS,
|
|
101
|
+
// Prioridade por caminho (opcional): "linhas de churn equivalentes".
|
|
102
|
+
// \`bonus\` promove caminho crítico; \`maxRiskFromChurn\` satura o superchurn de
|
|
103
|
+
// caminho de baixa criticidade (risco = min(churn, teto) + bonus).
|
|
104
|
+
// A primeira regra que casar vence; sem regra, o risco é o churn puro.
|
|
105
|
+
// priorities: [
|
|
106
|
+
// { pattern: /^src\\/(db|repositories)\\//, bonus: 300 },
|
|
107
|
+
// { pattern: /^src\\/(services|domain)\\//, bonus: 200 },
|
|
108
|
+
// { pattern: /^src\\/(components|styles)\\//, maxRiskFromChurn: 120 },
|
|
109
|
+
// ],
|
|
101
110
|
} satisfies HoundsmanConfig;
|
|
102
111
|
|
|
103
112
|
export default houndsmanConfig;
|
|
@@ -215,6 +224,7 @@ export type AiProviderOptions = Record<string, Record<string, unknown>>;
|
|
|
215
224
|
/** Features de IA do fluxo normal (todas fail-open). */
|
|
216
225
|
export type HoundsmanAiFeatures = {
|
|
217
226
|
context?: boolean;
|
|
227
|
+
business?: boolean;
|
|
218
228
|
brief?: boolean;
|
|
219
229
|
technical?: boolean;
|
|
220
230
|
classification?: boolean;
|
|
@@ -252,6 +262,18 @@ export type HoundsmanHunt = {
|
|
|
252
262
|
binaryPath?: string;
|
|
253
263
|
};
|
|
254
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Prioridade por caminho.
|
|
267
|
+
*
|
|
268
|
+
* \`bonus\` promove (linhas de churn equivalentes) e \`maxRiskFromChurn\` satura o
|
|
269
|
+
* superchurn de caminhos de baixa criticidade.
|
|
270
|
+
*/
|
|
271
|
+
export type HoundsmanPriority = {
|
|
272
|
+
pattern: RegExp;
|
|
273
|
+
bonus?: number;
|
|
274
|
+
maxRiskFromChurn?: number;
|
|
275
|
+
};
|
|
276
|
+
|
|
255
277
|
/** Contrato de configuração do motor. */
|
|
256
278
|
export type HoundsmanConfig<TId extends string = string> = {
|
|
257
279
|
projectName: string;
|
|
@@ -260,6 +282,8 @@ export type HoundsmanConfig<TId extends string = string> = {
|
|
|
260
282
|
watchdogs: Record<TId, WatchdogDefinition>;
|
|
261
283
|
exclusions: readonly RegExp[];
|
|
262
284
|
docMappings: readonly (readonly [RegExp, readonly string[]])[];
|
|
285
|
+
/** Prioridade por caminho (opcional; a primeira regra que casar vence). */
|
|
286
|
+
priorities?: readonly HoundsmanPriority[];
|
|
263
287
|
alwaysIncludeDocs?: readonly string[];
|
|
264
288
|
budgets?: HoundsmanBudgets;
|
|
265
289
|
concurrency?: HoundsmanConcurrency;
|
|
@@ -365,6 +389,9 @@ import { exampleWatchdog } from "./example";
|
|
|
365
389
|
/**
|
|
366
390
|
* Definições dos watchdogs — as chaves definem o union \`WatchdogId\`.
|
|
367
391
|
* Adicionar um watchdog = criar \`<id>.ts\` e registrá-lo aqui.
|
|
392
|
+
*
|
|
393
|
+
* NÃO declare um watchdog com id \`general\`: ele é reservado pelo motor
|
|
394
|
+
* (revisor de fluxo obrigatório) e é injetado em todo ciclo.
|
|
368
395
|
*/
|
|
369
396
|
export const WATCHDOG_DEFINITIONS = {
|
|
370
397
|
default: defaultWatchdog,
|
|
@@ -33,11 +33,14 @@ function findReview(
|
|
|
33
33
|
storage: StorageProvider,
|
|
34
34
|
repo: RepoIdentity,
|
|
35
35
|
branch?: string,
|
|
36
|
+
variant?: string,
|
|
36
37
|
): ReviewRecord {
|
|
37
38
|
const target = branch ?? currentBranch();
|
|
38
|
-
const review = storage.findReview(repo.key, target);
|
|
39
|
+
const review = storage.findReview(repo.key, target, variant);
|
|
39
40
|
if (!review) {
|
|
40
|
-
throw new Error(
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Nenhuma revisão para '${target}'${variant ? ` (variante '${variant}')` : ""}.`,
|
|
43
|
+
);
|
|
41
44
|
}
|
|
42
45
|
return review;
|
|
43
46
|
}
|
|
@@ -47,8 +50,9 @@ export function readStatus(
|
|
|
47
50
|
storage: StorageProvider,
|
|
48
51
|
repo: RepoIdentity,
|
|
49
52
|
branch?: string,
|
|
53
|
+
variant?: string,
|
|
50
54
|
): StatusResult {
|
|
51
|
-
return toResult(findReview(storage, repo, branch));
|
|
55
|
+
return toResult(findReview(storage, repo, branch, variant));
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/** Renova o heartbeat de uma execução ativa (`reviewing`/`fixing`). */
|
|
@@ -56,8 +60,9 @@ export function pingStatus(
|
|
|
56
60
|
storage: StorageProvider,
|
|
57
61
|
repo: RepoIdentity,
|
|
58
62
|
branch?: string,
|
|
63
|
+
variant?: string,
|
|
59
64
|
now?: string,
|
|
60
65
|
): StatusResult {
|
|
61
|
-
const review = findReview(storage, repo, branch);
|
|
66
|
+
const review = findReview(storage, repo, branch, variant);
|
|
62
67
|
return toResult(touchRunStatus(storage, review.id, now));
|
|
63
68
|
}
|