@payconductor/houndsman 0.12.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 +23 -2
- 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/doctor-command.ts +22 -0
- package/src/cli/commands/findings-command.ts +11 -1
- package/src/cli/commands/hunt-command.ts +479 -0
- package/src/cli/commands/review-command.ts +58 -2
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +20 -3
- package/src/cli/index.ts +20 -3
- package/src/cli/progress.ts +50 -0
- package/src/core/ai/ai-client.ts +38 -2
- package/src/core/ai/ai-sdk-model-provider.ts +39 -0
- 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 +14 -1
- 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/types.ts +2 -0
- 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 +38 -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 +57 -1
- package/src/core/defaults.ts +63 -2
- 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 +106 -26
- package/src/core/engine/review-session.ts +18 -16
- package/src/core/engine/review-workspace.ts +14 -0
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/env.ts +14 -0
- package/src/core/findings-state.ts +13 -1
- package/src/core/github-review.ts +5 -2
- package/src/core/hunt/hunt-runner.ts +418 -0
- package/src/core/hunt/index.ts +61 -0
- package/src/core/hunt/maki-install.ts +331 -0
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-model.ts +101 -0
- package/src/core/hunt/maki-prompt.ts +83 -0
- package/src/core/hunt/maki-release.ts +104 -0
- package/src/core/hunt/maki-runner.ts +276 -0
- package/src/core/hunt/pool.ts +8 -0
- package/src/core/logging/index.ts +7 -0
- package/src/core/logging/logger.ts +19 -0
- package/src/core/logging/progress.ts +366 -0
- package/src/core/orchestrator.ts +33 -1180
- 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 +46 -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 +17 -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 +78 -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 +30 -14
- package/src/skill/SETUP.md +22 -1
- package/src/skill/SKILL.md +17 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +181 -58
- package/src/tests/__snapshots__/baseline.test.ts.snap +206 -178
- package/src/tests/ai-core.test.ts +84 -1
- 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/cli.test.ts +1 -0
- package/src/tests/compare.test.ts +233 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/hunt-cli.test.ts +185 -0
- package/src/tests/hunt-install.test.ts +153 -0
- package/src/tests/hunt-maki.test.ts +525 -0
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +145 -0
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +90 -19
- 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
|
@@ -22,6 +22,7 @@ export type ReviewRow = {
|
|
|
22
22
|
repo_root: string;
|
|
23
23
|
repo_remote: string | null;
|
|
24
24
|
branch: string;
|
|
25
|
+
variant: string;
|
|
25
26
|
slug: string;
|
|
26
27
|
base: string | null;
|
|
27
28
|
head: string | null;
|
|
@@ -50,6 +51,10 @@ export type FindingRow = {
|
|
|
50
51
|
message: string;
|
|
51
52
|
action: string;
|
|
52
53
|
evidence: string | null;
|
|
54
|
+
scenario: string | null;
|
|
55
|
+
impact: string | null;
|
|
56
|
+
confidence: number | null;
|
|
57
|
+
basis: string | null;
|
|
53
58
|
anchor: string | null;
|
|
54
59
|
found_at_sha: string | null;
|
|
55
60
|
count: number;
|
|
@@ -91,6 +96,7 @@ export function reviewFromRow(row: ReviewRow): ReviewRecord {
|
|
|
91
96
|
repoRoot: row.repo_root,
|
|
92
97
|
repoRemote: row.repo_remote ?? undefined,
|
|
93
98
|
branch: row.branch,
|
|
99
|
+
variant: row.variant || undefined,
|
|
94
100
|
slug: row.slug,
|
|
95
101
|
base: row.base ?? undefined,
|
|
96
102
|
head: row.head ?? undefined,
|
|
@@ -129,6 +135,10 @@ export function findingFromRow(row: FindingRow, history: HistoryRow[]): FindingS
|
|
|
129
135
|
message: row.message,
|
|
130
136
|
action: row.action,
|
|
131
137
|
evidence: row.evidence ?? undefined,
|
|
138
|
+
scenario: row.scenario ?? undefined,
|
|
139
|
+
impact: row.impact ?? undefined,
|
|
140
|
+
confidence: row.confidence === null ? undefined : Number(row.confidence),
|
|
141
|
+
basis: (row.basis as FindingState["basis"]) ?? undefined,
|
|
132
142
|
anchor: parseJson<FindingAnchor>(row.anchor) ?? null,
|
|
133
143
|
foundAtSha: row.found_at_sha ?? undefined,
|
|
134
144
|
count: Number(row.count),
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { Database } from "bun:sqlite";
|
|
9
9
|
|
|
10
|
-
export const SCHEMA_VERSION =
|
|
10
|
+
export const SCHEMA_VERSION = 5;
|
|
11
11
|
|
|
12
12
|
const DDL = `
|
|
13
13
|
CREATE TABLE IF NOT EXISTS reviews (
|
|
@@ -16,6 +16,7 @@ CREATE TABLE IF NOT EXISTS reviews (
|
|
|
16
16
|
repo_root TEXT NOT NULL,
|
|
17
17
|
repo_remote TEXT,
|
|
18
18
|
branch TEXT NOT NULL,
|
|
19
|
+
variant TEXT NOT NULL DEFAULT '',
|
|
19
20
|
slug TEXT NOT NULL,
|
|
20
21
|
base TEXT,
|
|
21
22
|
head TEXT,
|
|
@@ -29,7 +30,7 @@ CREATE TABLE IF NOT EXISTS reviews (
|
|
|
29
30
|
pr_number INTEGER,
|
|
30
31
|
created_at TEXT NOT NULL,
|
|
31
32
|
updated_at TEXT NOT NULL,
|
|
32
|
-
UNIQUE(repo_key, branch)
|
|
33
|
+
UNIQUE(repo_key, branch, variant)
|
|
33
34
|
);
|
|
34
35
|
|
|
35
36
|
CREATE INDEX IF NOT EXISTS idx_reviews_repo_updated
|
|
@@ -48,6 +49,10 @@ CREATE TABLE IF NOT EXISTS findings (
|
|
|
48
49
|
message TEXT NOT NULL,
|
|
49
50
|
action TEXT NOT NULL,
|
|
50
51
|
evidence TEXT,
|
|
52
|
+
scenario TEXT,
|
|
53
|
+
impact TEXT,
|
|
54
|
+
confidence REAL,
|
|
55
|
+
basis TEXT,
|
|
51
56
|
anchor TEXT,
|
|
52
57
|
found_at_sha TEXT,
|
|
53
58
|
count INTEGER NOT NULL DEFAULT 1,
|
|
@@ -93,7 +98,6 @@ CREATE INDEX IF NOT EXISTS idx_artifacts_review ON artifacts(review_id);
|
|
|
93
98
|
|
|
94
99
|
/** Aplica pragmas e cria o schema quando necessário. */
|
|
95
100
|
export function migrate(db: Database, path: string): void {
|
|
96
|
-
db.exec("PRAGMA foreign_keys = ON;");
|
|
97
101
|
db.exec("PRAGMA busy_timeout = 5000;");
|
|
98
102
|
db.exec("PRAGMA synchronous = NORMAL;");
|
|
99
103
|
if (path !== ":memory:") db.exec("PRAGMA journal_mode = WAL;");
|
|
@@ -102,25 +106,102 @@ export function migrate(db: Database, path: string): void {
|
|
|
102
106
|
user_version?: number;
|
|
103
107
|
} | null;
|
|
104
108
|
const version = Number(row?.user_version ?? 0);
|
|
105
|
-
if (version >= SCHEMA_VERSION)
|
|
109
|
+
if (version >= SCHEMA_VERSION) {
|
|
110
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
106
113
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
[
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
// O rebuild de `reviews` (v5) troca a chave única e precisa de FKs desligadas:
|
|
115
|
+
// com elas ligadas, `DROP TABLE reviews` cascatearia e apagaria os filhos.
|
|
116
|
+
db.exec("PRAGMA foreign_keys = OFF;");
|
|
117
|
+
try {
|
|
118
|
+
db.transaction(() => {
|
|
119
|
+
db.exec(DDL);
|
|
120
|
+
// v2: heartbeat explícito da execução; v3: worktree/pr da revisão;
|
|
121
|
+
// v4: cenário/impacto/confiança/origem do finding.
|
|
122
|
+
const addMissing = (table: string, columns: [string, string][]): void => {
|
|
123
|
+
const info = db.query(`PRAGMA table_info(${table})`).all() as {
|
|
124
|
+
name: string;
|
|
125
|
+
}[];
|
|
126
|
+
for (const [name, type] of columns) {
|
|
127
|
+
if (!info.some((column) => column.name === name)) {
|
|
128
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
addMissing("reviews", [
|
|
133
|
+
["heartbeat_at", "TEXT"],
|
|
134
|
+
["worktree_path", "TEXT"],
|
|
135
|
+
["pr_number", "INTEGER"],
|
|
136
|
+
]);
|
|
137
|
+
addMissing("findings", [
|
|
138
|
+
["scenario", "TEXT"],
|
|
139
|
+
["impact", "TEXT"],
|
|
140
|
+
["confidence", "REAL"],
|
|
141
|
+
["basis", "TEXT"],
|
|
142
|
+
]);
|
|
143
|
+
// v5: variante A/B na identidade da review.
|
|
144
|
+
rebuildReviewsWithVariant(db);
|
|
145
|
+
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
146
|
+
})();
|
|
147
|
+
} finally {
|
|
148
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
149
|
+
}
|
|
150
|
+
const violations = db.query("PRAGMA foreign_key_check").all();
|
|
151
|
+
if (violations.length > 0) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Migração do banco deixou ${violations.length} referência(s) inválida(s).`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* v5 — adiciona `variant` e troca `UNIQUE(repo_key, branch)` por
|
|
160
|
+
* `UNIQUE(repo_key, branch, variant)`.
|
|
161
|
+
*
|
|
162
|
+
* SQLite não remove constraint inline: recria a tabela preservando os ids (as
|
|
163
|
+
* FKs dos filhos continuam válidas) e recria o índice.
|
|
164
|
+
*/
|
|
165
|
+
function rebuildReviewsWithVariant(db: Database): void {
|
|
166
|
+
const columns = db.query("PRAGMA table_info(reviews)").all() as {
|
|
167
|
+
name: string;
|
|
168
|
+
}[];
|
|
169
|
+
if (columns.some((column) => column.name === "variant")) return;
|
|
170
|
+
|
|
171
|
+
db.exec(`
|
|
172
|
+
CREATE TABLE reviews_v5 (
|
|
173
|
+
id TEXT PRIMARY KEY,
|
|
174
|
+
repo_key TEXT NOT NULL,
|
|
175
|
+
repo_root TEXT NOT NULL,
|
|
176
|
+
repo_remote TEXT,
|
|
177
|
+
branch TEXT NOT NULL,
|
|
178
|
+
variant TEXT NOT NULL DEFAULT '',
|
|
179
|
+
slug TEXT NOT NULL,
|
|
180
|
+
base TEXT,
|
|
181
|
+
head TEXT,
|
|
182
|
+
head_sha TEXT,
|
|
183
|
+
reviewed_sha TEXT,
|
|
184
|
+
reviewed_at TEXT,
|
|
185
|
+
status TEXT,
|
|
186
|
+
started_at TEXT,
|
|
187
|
+
heartbeat_at TEXT,
|
|
188
|
+
worktree_path TEXT,
|
|
189
|
+
pr_number INTEGER,
|
|
190
|
+
created_at TEXT NOT NULL,
|
|
191
|
+
updated_at TEXT NOT NULL,
|
|
192
|
+
UNIQUE(repo_key, branch, variant)
|
|
193
|
+
);
|
|
194
|
+
INSERT INTO reviews_v5
|
|
195
|
+
(id, repo_key, repo_root, repo_remote, branch, variant, slug, base, head,
|
|
196
|
+
head_sha, reviewed_sha, reviewed_at, status, started_at, heartbeat_at,
|
|
197
|
+
worktree_path, pr_number, created_at, updated_at)
|
|
198
|
+
SELECT id, repo_key, repo_root, repo_remote, branch, '', slug, base, head,
|
|
199
|
+
head_sha, reviewed_sha, reviewed_at, status, started_at, heartbeat_at,
|
|
200
|
+
worktree_path, pr_number, created_at, updated_at
|
|
201
|
+
FROM reviews;
|
|
202
|
+
DROP TABLE reviews;
|
|
203
|
+
ALTER TABLE reviews_v5 RENAME TO reviews;
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_repo_updated
|
|
205
|
+
ON reviews(repo_key, updated_at DESC);
|
|
206
|
+
`);
|
|
126
207
|
}
|
|
@@ -69,7 +69,7 @@ export class SQLiteStorageProvider implements StorageProvider {
|
|
|
69
69
|
// -----------------------------------------------------------------------
|
|
70
70
|
|
|
71
71
|
getOrCreateReview(input: CreateReviewInput): ReviewRecord {
|
|
72
|
-
const existing = this.findReview(input.repo.key, input.branch);
|
|
72
|
+
const existing = this.findReview(input.repo.key, input.branch, input.variant);
|
|
73
73
|
if (existing) {
|
|
74
74
|
// O repo pode ter sido movido; a identidade continua a mesma.
|
|
75
75
|
const moved =
|
|
@@ -102,6 +102,7 @@ export class SQLiteStorageProvider implements StorageProvider {
|
|
|
102
102
|
repoRoot: input.repo.root,
|
|
103
103
|
repoRemote: input.repo.remote,
|
|
104
104
|
branch: input.branch,
|
|
105
|
+
variant: input.variant?.trim() || undefined,
|
|
105
106
|
slug: input.slug,
|
|
106
107
|
base: input.base,
|
|
107
108
|
head: input.head,
|
|
@@ -113,15 +114,16 @@ export class SQLiteStorageProvider implements StorageProvider {
|
|
|
113
114
|
};
|
|
114
115
|
this.db.run(
|
|
115
116
|
`INSERT INTO reviews
|
|
116
|
-
(id, repo_key, repo_root, repo_remote, branch, slug, base, head,
|
|
117
|
+
(id, repo_key, repo_root, repo_remote, branch, variant, slug, base, head,
|
|
117
118
|
worktree_path, pr_number, created_at, updated_at)
|
|
118
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
119
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
119
120
|
[
|
|
120
121
|
record.id,
|
|
121
122
|
record.repoKey,
|
|
122
123
|
record.repoRoot,
|
|
123
124
|
record.repoRemote ?? null,
|
|
124
125
|
record.branch,
|
|
126
|
+
record.variant ?? "",
|
|
125
127
|
record.slug,
|
|
126
128
|
record.base ?? null,
|
|
127
129
|
record.head ?? null,
|
|
@@ -134,10 +136,12 @@ export class SQLiteStorageProvider implements StorageProvider {
|
|
|
134
136
|
return record;
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
findReview(repoKey: string, branch: string): ReviewRecord | null {
|
|
139
|
+
findReview(repoKey: string, branch: string, variant?: string): ReviewRecord | null {
|
|
138
140
|
const row = this.db
|
|
139
|
-
.query(
|
|
140
|
-
|
|
141
|
+
.query(
|
|
142
|
+
"SELECT * FROM reviews WHERE repo_key = ? AND branch = ? AND variant = ?",
|
|
143
|
+
)
|
|
144
|
+
.get(repoKey, branch, variant?.trim() ?? "") as ReviewRow | null;
|
|
141
145
|
return row ? reviewFromRow(row) : null;
|
|
142
146
|
}
|
|
143
147
|
|
|
@@ -253,9 +257,10 @@ export class SQLiteStorageProvider implements StorageProvider {
|
|
|
253
257
|
const upsert = this.db.query(
|
|
254
258
|
`INSERT INTO findings
|
|
255
259
|
(id, review_id, short_id, fingerprint, watchdog_ids, rule, severity,
|
|
256
|
-
file, line, message, action, evidence,
|
|
257
|
-
present, decision, comment, exec_state, github,
|
|
258
|
-
|
|
260
|
+
file, line, message, action, evidence, scenario, impact, confidence, basis,
|
|
261
|
+
anchor, found_at_sha, count, present, decision, comment, exec_state, github,
|
|
262
|
+
created_at, updated_at)
|
|
263
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
259
264
|
ON CONFLICT(id) DO UPDATE SET
|
|
260
265
|
short_id = excluded.short_id,
|
|
261
266
|
fingerprint = excluded.fingerprint,
|
|
@@ -267,6 +272,10 @@ export class SQLiteStorageProvider implements StorageProvider {
|
|
|
267
272
|
message = excluded.message,
|
|
268
273
|
action = excluded.action,
|
|
269
274
|
evidence = excluded.evidence,
|
|
275
|
+
scenario = excluded.scenario,
|
|
276
|
+
impact = excluded.impact,
|
|
277
|
+
confidence = excluded.confidence,
|
|
278
|
+
basis = excluded.basis,
|
|
270
279
|
anchor = excluded.anchor,
|
|
271
280
|
found_at_sha = excluded.found_at_sha,
|
|
272
281
|
count = excluded.count,
|
|
@@ -325,6 +334,10 @@ export class SQLiteStorageProvider implements StorageProvider {
|
|
|
325
334
|
finding.message,
|
|
326
335
|
finding.action,
|
|
327
336
|
finding.evidence ?? null,
|
|
337
|
+
finding.scenario ?? null,
|
|
338
|
+
finding.impact ?? null,
|
|
339
|
+
finding.confidence ?? null,
|
|
340
|
+
finding.basis ?? null,
|
|
328
341
|
finding.anchor ? JSON.stringify(finding.anchor) : null,
|
|
329
342
|
finding.foundAtSha ?? null,
|
|
330
343
|
finding.count,
|
|
@@ -28,6 +28,13 @@ export type ReviewRecord = {
|
|
|
28
28
|
repoRoot: string;
|
|
29
29
|
repoRemote?: string;
|
|
30
30
|
branch: string;
|
|
31
|
+
/**
|
|
32
|
+
* Rótulo da variante A/B (ausente = run principal da branch).
|
|
33
|
+
*
|
|
34
|
+
* Faz parte da identidade junto com `(repoKey, branch)`: permite rodar a
|
|
35
|
+
* mesma PR com dois experimentos sem apagar a revisão anterior.
|
|
36
|
+
*/
|
|
37
|
+
variant?: string;
|
|
31
38
|
slug: string;
|
|
32
39
|
base?: string;
|
|
33
40
|
head?: string;
|
|
@@ -81,6 +88,8 @@ export type CreateReviewInput = {
|
|
|
81
88
|
repo: RepoIdentity;
|
|
82
89
|
branch: string;
|
|
83
90
|
slug: string;
|
|
91
|
+
/** Variante A/B (ausente = run principal da branch). */
|
|
92
|
+
variant?: string;
|
|
84
93
|
base?: string;
|
|
85
94
|
head?: string;
|
|
86
95
|
/** Worktree onde a revisão roda (ausente = clone principal). */
|
|
@@ -99,17 +108,20 @@ export type ArtifactKind =
|
|
|
99
108
|
| "context"
|
|
100
109
|
| "context-brief"
|
|
101
110
|
| "context-summary"
|
|
111
|
+
| "business"
|
|
112
|
+
| "business-brief"
|
|
102
113
|
| "finding-report"
|
|
103
|
-
| "review"
|
|
104
114
|
| "resume"
|
|
105
115
|
| "technical"
|
|
106
|
-
| "domain-review"
|
|
107
116
|
| "verify"
|
|
108
117
|
| "ast"
|
|
109
118
|
| "ast-impact"
|
|
110
119
|
| "ai"
|
|
111
120
|
| "ai-usage"
|
|
112
121
|
| "ai-brief"
|
|
122
|
+
| "hunt"
|
|
123
|
+
| "prepare"
|
|
124
|
+
| "compare"
|
|
113
125
|
| "other";
|
|
114
126
|
|
|
115
127
|
/** Artefato persistido de uma review. */
|
|
@@ -141,9 +153,10 @@ export type WriteArtifactInput = {
|
|
|
141
153
|
export interface StorageProvider {
|
|
142
154
|
readonly kind: "sqlite" | "file";
|
|
143
155
|
|
|
144
|
-
/**
|
|
156
|
+
/** Busca por identidade `(repo, branch, variante)`; variante ausente = principal. */
|
|
157
|
+
findReview(repoKey: string, branch: string, variant?: string): ReviewRecord | null;
|
|
158
|
+
/** Run da branch + variante (cria quando não existe). */
|
|
145
159
|
getOrCreateReview(input: CreateReviewInput): ReviewRecord;
|
|
146
|
-
findReview(repoKey: string, branch: string): ReviewRecord | null;
|
|
147
160
|
getReview(id: string): ReviewRecord | null;
|
|
148
161
|
/** Reviews do repositório (mais recentes primeiro); sem chave = todas. */
|
|
149
162
|
listReviews(repoKey?: string): ReviewRecord[];
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
+
import { type BusinessSummary, parseBusinessSummary } from "../business-summary";
|
|
12
13
|
import {
|
|
14
|
+
BUSINESS_BRIEF_FILE,
|
|
15
|
+
BUSINESS_SUMMARY_FILE,
|
|
13
16
|
CONTEXT_BRIEF_FILE,
|
|
14
17
|
CONTEXT_FILE,
|
|
15
18
|
CONTEXT_SUMMARY_FILE,
|
|
@@ -97,7 +100,6 @@ export class ReviewWorkArea {
|
|
|
97
100
|
brief: this.file(CONTEXT_BRIEF_FILE),
|
|
98
101
|
};
|
|
99
102
|
}
|
|
100
|
-
|
|
101
103
|
/** Garante os diretórios; `clean` recria a área do zero. */
|
|
102
104
|
ensure(clean = false): void {
|
|
103
105
|
if (clean) this.clean();
|
|
@@ -143,6 +145,62 @@ export class ReviewWorkArea {
|
|
|
143
145
|
writeFileSync(this.contextPaths.summary, `${JSON.stringify(summary, null, 2)}\n`);
|
|
144
146
|
}
|
|
145
147
|
|
|
148
|
+
/** Caminhos do modelo de intenção (JSON + brief do passo). */
|
|
149
|
+
get businessPaths(): { summary: string; brief: string } {
|
|
150
|
+
return {
|
|
151
|
+
summary: this.file(BUSINESS_SUMMARY_FILE),
|
|
152
|
+
brief: this.file(BUSINESS_BRIEF_FILE),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Grava o modelo de intenção (determinístico, IA ou revisor). */
|
|
157
|
+
writeBusinessSummary(summary: unknown): void {
|
|
158
|
+
mkdirSync(this.root, { recursive: true });
|
|
159
|
+
writeFileSync(
|
|
160
|
+
this.businessPaths.summary,
|
|
161
|
+
`${JSON.stringify(summary, null, 2)}\n`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Grava o brief do passo de intenção. */
|
|
166
|
+
writeBusinessBrief(content: string): void {
|
|
167
|
+
mkdirSync(this.root, { recursive: true });
|
|
168
|
+
writeFileSync(this.businessPaths.brief, `${content}\n`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Modelo de intenção gravado (subagente/IA) — null se ausente/inválido. */
|
|
172
|
+
readBusinessSummary(): BusinessSummary | null {
|
|
173
|
+
const raw = this.readBusinessSummaryRaw();
|
|
174
|
+
if (raw === null) return null;
|
|
175
|
+
try {
|
|
176
|
+
return parseBusinessSummary(JSON.parse(raw), "reviewer");
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Conteúdo cru do modelo de intenção (para persistir como artefato). */
|
|
183
|
+
readBusinessSummaryRaw(): string | null {
|
|
184
|
+
const path = this.businessPaths.summary;
|
|
185
|
+
if (!existsSync(path)) return null;
|
|
186
|
+
try {
|
|
187
|
+
return readFileSync(path, "utf8");
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Conteúdo cru do brief de intenção (para persistir como artefato). */
|
|
194
|
+
readBusinessBriefRaw(): string | null {
|
|
195
|
+
const path = this.businessPaths.brief;
|
|
196
|
+
if (!existsSync(path)) return null;
|
|
197
|
+
try {
|
|
198
|
+
return readFileSync(path, "utf8");
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
146
204
|
/** Finds brutos gravados pelos subagentes (um JSON por watchdog). */
|
|
147
205
|
readFindings(activeIds: string[]): FindingsIngest {
|
|
148
206
|
const reports: WatchdogReport[] = [];
|
package/src/core/types.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { BriefPlan } from "./ai/schemas";
|
|
10
10
|
import type { AstFileBrief } from "./ast/types";
|
|
11
|
+
import type { BusinessSummary } from "./business-summary";
|
|
11
12
|
import type { ChangedScope } from "./changed-scope";
|
|
12
13
|
import type { JevRuleSpec } from "./jev/jev-types";
|
|
13
14
|
|
|
@@ -131,8 +132,38 @@ export type WatchdogRenderArgs = {
|
|
|
131
132
|
ast?: readonly AstFileBrief[];
|
|
132
133
|
/** Plano de revisão sugerido por IA (nunca substitui as regras). */
|
|
133
134
|
aiPlan?: BriefPlan;
|
|
135
|
+
/** Modelo de intenção da PR (negócio) — insumo do revisor `general`. */
|
|
136
|
+
business?: BusinessSummary;
|
|
137
|
+
/** Caminho do `business-summary.json` no staging (versão canônica). */
|
|
138
|
+
businessSummaryPath?: string;
|
|
139
|
+
/** Trechos alterados (`diff` por arquivo) — só para o revisor de fluxo. */
|
|
140
|
+
snippets?: readonly { file: string; snippet: string }[];
|
|
141
|
+
/** `true` no revisor de fluxo: exige devolver/gravar o modelo de intenção. */
|
|
142
|
+
flowReview?: boolean;
|
|
134
143
|
/** Escopo do diff por arquivo (símbolos alterados; hunks como fallback). */
|
|
135
144
|
changedScope?: ChangedScope;
|
|
145
|
+
/** Micro-alterações iguais em muitos arquivos (propagação mecânica). */
|
|
146
|
+
propagation?: PropagationCluster | null;
|
|
147
|
+
/** Contratos alterados cujos importadores não mudaram (possível lacuna). */
|
|
148
|
+
propagationGaps?: readonly PropagationGap[];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** Cluster de propagação mecânica (ex.: novo parâmetro em 100 call sites). */
|
|
152
|
+
export type PropagationCluster = {
|
|
153
|
+
/** Linhas adicionadas normalizadas (o padrão dominante). */
|
|
154
|
+
lines: string[];
|
|
155
|
+
/** Arquivos que seguem o padrão. */
|
|
156
|
+
files: string[];
|
|
157
|
+
/** Arquivos com micro-alteração que NÃO seguem o padrão. */
|
|
158
|
+
outliers: { file: string; lines: string[] }[];
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** Contrato que mudou e tem importadores fora do diff. */
|
|
162
|
+
export type PropagationGap = {
|
|
163
|
+
/** Arquivo do contrato (o que mudou). */
|
|
164
|
+
file: string;
|
|
165
|
+
/** Importadores que não aparecem no diff. */
|
|
166
|
+
importers: string[];
|
|
136
167
|
};
|
|
137
168
|
|
|
138
169
|
/** Definição completa de um watchdog — 1 por arquivo em `project/watchdogs/`. */
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* adaptadores para a API pública.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { builtinWatchdog, GENERAL_WATCHDOG_ID, mergeGeneralWatchdog } from "./builtins";
|
|
9
10
|
import type { HoundsmanConfig } from "./config";
|
|
10
11
|
import { Watchdog, type WatchdogContext } from "./domain/watchdog";
|
|
11
12
|
import type { WatchdogDefinition } from "./types";
|
|
@@ -23,17 +24,34 @@ export class WatchdogRegistry<TId extends string = string> {
|
|
|
23
24
|
return this.config.watchdogIds;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
/** Definição de um watchdog (
|
|
27
|
+
/** Definição de um watchdog (config vence; senão, built-in). */
|
|
27
28
|
get(watchdogId: string): WatchdogDefinition {
|
|
28
|
-
|
|
29
|
+
const definition = this.definitionOf(watchdogId);
|
|
30
|
+
if (!definition) {
|
|
31
|
+
throw new Error(`Watchdog '${watchdogId}' não encontrado.`);
|
|
32
|
+
}
|
|
33
|
+
return definition;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
/** Busca null-safe por id. */
|
|
32
37
|
find(watchdogId: string): Watchdog | null {
|
|
33
|
-
const definition = this.
|
|
38
|
+
const definition = this.definitionOf(watchdogId);
|
|
34
39
|
return definition ? new Watchdog(definition) : null;
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a definição: config primeiro, built-in depois. O `general` é
|
|
44
|
+
* reservado e sempre inclui o piso de lógica do motor (mesmo se o consumidor
|
|
45
|
+
* declarar um watchdog com esse id).
|
|
46
|
+
*/
|
|
47
|
+
private definitionOf(watchdogId: string): WatchdogDefinition | null {
|
|
48
|
+
const configured = this.config.watchdogs[watchdogId as TId];
|
|
49
|
+
if (watchdogId === GENERAL_WATCHDOG_ID) {
|
|
50
|
+
return mergeGeneralWatchdog(configured);
|
|
51
|
+
}
|
|
52
|
+
return configured ?? builtinWatchdog(watchdogId);
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
/** Nome legível (default: o próprio id). */
|
|
38
56
|
nameOf(watchdogId: string): string {
|
|
39
57
|
return this.find(watchdogId)?.name ?? watchdogId;
|
package/src/index.ts
CHANGED
|
@@ -50,6 +50,29 @@ export type {
|
|
|
50
50
|
AstSymbolKind,
|
|
51
51
|
} from "./core/ast/types";
|
|
52
52
|
export { defaultRenderBrief, PROCEDURE, renderBrief } from "./core/brief";
|
|
53
|
+
export {
|
|
54
|
+
BUILTIN_WATCHDOGS,
|
|
55
|
+
builtinWatchdog,
|
|
56
|
+
GENERAL_WATCHDOG_ID,
|
|
57
|
+
generalWatchdog,
|
|
58
|
+
mergeGeneralWatchdog,
|
|
59
|
+
} from "./core/builtins";
|
|
60
|
+
export {
|
|
61
|
+
BUSINESS_SUMMARY_SCHEMA,
|
|
62
|
+
type BusinessBriefInput,
|
|
63
|
+
type BusinessContract,
|
|
64
|
+
type BusinessFlow,
|
|
65
|
+
type BusinessInput,
|
|
66
|
+
type BusinessState,
|
|
67
|
+
type BusinessSummary,
|
|
68
|
+
type BusinessSummarySource,
|
|
69
|
+
buildBusinessBrief,
|
|
70
|
+
buildBusinessPromptPayload,
|
|
71
|
+
buildBusinessSummary,
|
|
72
|
+
parseBusinessSummary,
|
|
73
|
+
renderBusinessLines,
|
|
74
|
+
summarizeBusiness,
|
|
75
|
+
} from "./core/business-summary";
|
|
53
76
|
export type {
|
|
54
77
|
BuildChangedScopeInput,
|
|
55
78
|
ChangedScope,
|
|
@@ -67,7 +90,10 @@ export {
|
|
|
67
90
|
mergeRanges,
|
|
68
91
|
summarizeScope,
|
|
69
92
|
} from "./core/changed-scope";
|
|
70
|
-
export type {
|
|
93
|
+
export type {
|
|
94
|
+
FindingsCommandResult,
|
|
95
|
+
FindingsContextOptions,
|
|
96
|
+
} from "./core/commands/findings";
|
|
71
97
|
export { runFindingsCommand, runVerifyCommand } from "./core/commands/findings";
|
|
72
98
|
export { ProjectScaffolder } from "./core/commands/scaffold";
|
|
73
99
|
export type {
|
|
@@ -80,13 +106,19 @@ export type {
|
|
|
80
106
|
HoundsmanConcurrency,
|
|
81
107
|
HoundsmanConfig,
|
|
82
108
|
HoundsmanConfigInput,
|
|
109
|
+
HoundsmanHunt,
|
|
110
|
+
HoundsmanPriority,
|
|
83
111
|
HoundsmanStorage,
|
|
84
112
|
} from "./core/config";
|
|
85
113
|
export {
|
|
86
114
|
DEFAULT_ALWAYS_INCLUDE_DOCS,
|
|
87
115
|
DEFAULT_AST_MAX_FILE_BYTES,
|
|
88
116
|
DEFAULT_AST_MAX_IMPACT_FILES,
|
|
117
|
+
DEFAULT_HUNT_CONCURRENCY,
|
|
118
|
+
DEFAULT_HUNT_RETRIES,
|
|
119
|
+
DEFAULT_HUNT_TIMEOUT_MS,
|
|
89
120
|
DEFAULT_JEV_CONCURRENCY,
|
|
121
|
+
DEFAULT_MAKI_VERSION,
|
|
90
122
|
DEFAULT_MAX_FILES_PER_BRIEF,
|
|
91
123
|
defineHoundsmanConfig,
|
|
92
124
|
resolveJevConcurrency,
|
|
@@ -181,6 +213,50 @@ export {
|
|
|
181
213
|
syncState,
|
|
182
214
|
} from "./core/findings-state";
|
|
183
215
|
export type { GitResult, GitRunner } from "./core/git-runner";
|
|
216
|
+
export type {
|
|
217
|
+
HuntEvent,
|
|
218
|
+
HuntReport,
|
|
219
|
+
HuntRunnerOptions,
|
|
220
|
+
HuntTotals,
|
|
221
|
+
HuntWatchdogResult,
|
|
222
|
+
InstallMakiOptions,
|
|
223
|
+
InstallMakiResult,
|
|
224
|
+
MakiBinary,
|
|
225
|
+
MakiBinarySource,
|
|
226
|
+
MakiInstallManifest,
|
|
227
|
+
MakiModel,
|
|
228
|
+
MakiProcessRunner,
|
|
229
|
+
MakiRunMetrics,
|
|
230
|
+
MakiRunRequest,
|
|
231
|
+
MakiRunResult,
|
|
232
|
+
MakiUsage,
|
|
233
|
+
} from "./core/hunt";
|
|
234
|
+
export {
|
|
235
|
+
extractJsonObject,
|
|
236
|
+
HuntRunner,
|
|
237
|
+
houndsmanKeyEnvFor,
|
|
238
|
+
installMaki,
|
|
239
|
+
MAKI_API_KEY_ENV,
|
|
240
|
+
MAKI_DISALLOWED_TOOLS,
|
|
241
|
+
makiArgs,
|
|
242
|
+
makiCurrentFile,
|
|
243
|
+
makiInstallDir,
|
|
244
|
+
makiSourceLabel,
|
|
245
|
+
makiToolsRoot,
|
|
246
|
+
mapPool,
|
|
247
|
+
metricsFrom,
|
|
248
|
+
parseMakiOutput,
|
|
249
|
+
probeMakiVersion,
|
|
250
|
+
readMakiInstall,
|
|
251
|
+
renderHuntPrompt,
|
|
252
|
+
renderRetrySuffix,
|
|
253
|
+
requireMakiBinary,
|
|
254
|
+
requireMakiModel,
|
|
255
|
+
resolveMakiBinary,
|
|
256
|
+
resolveMakiModel,
|
|
257
|
+
SpawnMakiRunner,
|
|
258
|
+
uninstallMaki,
|
|
259
|
+
} from "./core/hunt";
|
|
184
260
|
export {
|
|
185
261
|
CORE_DIR,
|
|
186
262
|
CORE_SKILL_DIR,
|
|
@@ -193,9 +269,7 @@ export { ProjectConfig } from "./core/project-config";
|
|
|
193
269
|
export { checkRegistry } from "./core/registry-check";
|
|
194
270
|
export { BriefRenderer } from "./core/renderers/brief-renderer";
|
|
195
271
|
export { ManifestWriter } from "./core/renderers/manifest-writer";
|
|
196
|
-
export {
|
|
197
|
-
export type { DomainReport } from "./core/report-builder";
|
|
198
|
-
export { renderDomainReport } from "./core/report-builder";
|
|
272
|
+
export { RESUME_MAX_CHARS, renderReviewResume } from "./core/renderers/resume-renderer";
|
|
199
273
|
export type { Clock } from "./core/services/clock";
|
|
200
274
|
export { systemClock } from "./core/services/clock";
|
|
201
275
|
export { FileClassifier } from "./core/services/file-classifier";
|
|
@@ -19,9 +19,11 @@ import {
|
|
|
19
19
|
import { formatJson, highlightJson } from "./utils/json-highlight";
|
|
20
20
|
import type { ArtifactMeta } from "./utils/types";
|
|
21
21
|
|
|
22
|
-
/** Prefere o
|
|
22
|
+
/** Prefere o resumo final; senão, o primeiro artefato disponível. */
|
|
23
23
|
function pickDefault(items: ArtifactMeta[]): string {
|
|
24
|
-
return (
|
|
24
|
+
return (
|
|
25
|
+
(items.find((item) => item.name === "review-resume.md") ?? items[0])?.name ?? ""
|
|
26
|
+
);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** JSON com destaque de sintaxe (tokens em spans, sem lib externa). */
|