@payconductor/houndsman 0.3.0 → 0.4.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 +18 -6
- package/package.json +1 -1
- package/src/cli/commands/context-command.ts +11 -9
- package/src/cli/commands/doctor-command.ts +5 -0
- package/src/cli/commands/findings-command.ts +13 -3
- package/src/cli/commands/review-command.ts +1 -2
- package/src/cli/help.ts +7 -3
- package/src/cli/index.ts +2 -2
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +155 -120
- package/src/core/commands/review.ts +7 -8
- package/src/core/commands/scaffold.ts +15 -16
- package/src/core/config.ts +16 -7
- package/src/core/engine/houndsman-engine.ts +15 -1
- package/src/core/engine/review-session.ts +37 -9
- package/src/core/findings-state.ts +32 -71
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +175 -128
- package/src/core/paths.ts +23 -17
- package/src/core/pr-context.ts +19 -5
- package/src/core/project-config.ts +4 -5
- package/src/core/renderers/manifest-writer.ts +29 -18
- package/src/core/run-layout.ts +4 -104
- package/src/core/run-status.ts +28 -53
- package/src/core/services/git-repository.ts +16 -0
- package/src/core/services/project-files.ts +13 -1
- package/src/core/storage/factory.ts +24 -0
- package/src/core/storage/file/file-storage.ts +269 -0
- package/src/core/storage/paths.ts +64 -0
- package/src/core/storage/repo-identity.ts +70 -0
- package/src/core/storage/sqlite/mappers.ts +160 -0
- package/src/core/storage/sqlite/schema.ts +108 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
- package/src/core/storage/types.ts +143 -0
- package/src/core/storage/work-area.ts +191 -0
- package/src/index.ts +27 -6
- package/src/server/artifacts.ts +8 -61
- package/src/server/client/ArtifactsPanel.tsx +137 -23
- package/src/server/client/FindingCard.tsx +37 -6
- package/src/server/client/MarkdownText.tsx +101 -0
- package/src/server/client/SubmitModal.tsx +9 -4
- package/src/server/client/code-highlight.ts +422 -0
- package/src/server/client/constants.ts +130 -24
- package/src/server/client/json-highlight.ts +55 -0
- package/src/server/client/markdown.ts +153 -0
- package/src/server/client/types.ts +23 -0
- package/src/server/index.ts +95 -15
- package/src/server/run-repository.ts +144 -136
- package/src/skill/OUTPUT-FORMAT.md +6 -4
- package/src/skill/README.md +22 -14
- package/src/skill/SETUP.md +4 -4
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +27 -18
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +5 -5
- package/src/tests/__snapshots__/baseline.test.ts.snap +44 -33
- package/src/tests/baseline.test.ts +76 -34
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +1 -1
- package/src/tests/findings-commands.test.ts +153 -131
- package/src/tests/findings-state.test.ts +6 -3
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/infrastructure.test.ts +33 -38
- package/src/tests/markdown.test.ts +89 -0
- package/src/tests/review-scripts.test.ts +8 -5
- package/src/tests/run-status.test.ts +33 -23
- package/src/tests/server.test.ts +108 -75
- package/src/tests/storage.test.ts +386 -0
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subcomando `findings` — triagem dos findings da revisão (biblioteca).
|
|
3
3
|
*
|
|
4
|
-
* Lê/grava o
|
|
5
|
-
* (markdown ou JSON) + exit code; o CLI decide o log/exit. O agente do
|
|
6
|
-
* usa `list`/`set`/`done` (e `batch` para lotes de correção) para
|
|
4
|
+
* Lê/grava o estado de triagem no storage (SQLite por default). Devolve o texto
|
|
5
|
+
* pronto (markdown ou JSON) + exit code; o CLI decide o log/exit. O agente do
|
|
6
|
+
* usuário usa `list`/`set`/`done` (e `batch` para lotes de correção) para
|
|
7
|
+
* interagir sem abrir o banco à mão.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* Ids aceitam prefixo único (exato primeiro) — útil porque o id curto convive
|
|
10
|
+
* com o UUID interno no banco.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import {
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
type HoundsmanConfig,
|
|
16
|
-
resolveArtifactDir,
|
|
17
|
-
resolveJevConcurrency,
|
|
18
|
-
} from "../config";
|
|
13
|
+
import { type HoundsmanConfig, resolveJevConcurrency } from "../config";
|
|
19
14
|
import {
|
|
20
15
|
applyFindingUpdate,
|
|
21
|
-
buildStateFromRunDir,
|
|
22
16
|
FINDING_DECISIONS,
|
|
23
17
|
FINDING_EXEC_STATES,
|
|
24
18
|
type FindingDecision,
|
|
@@ -26,11 +20,11 @@ import {
|
|
|
26
20
|
type FindingState,
|
|
27
21
|
type FindingsFilter,
|
|
28
22
|
type FindingsStateFile,
|
|
29
|
-
FindingsStateStore,
|
|
30
23
|
type FindingUpdate,
|
|
31
24
|
fileReader,
|
|
32
25
|
filterFindings,
|
|
33
|
-
|
|
26
|
+
reorder,
|
|
27
|
+
resolveFindingId,
|
|
34
28
|
summarize,
|
|
35
29
|
syncState,
|
|
36
30
|
} from "../findings-state";
|
|
@@ -43,14 +37,17 @@ import {
|
|
|
43
37
|
type SubmitSelection,
|
|
44
38
|
submitFindings,
|
|
45
39
|
} from "../github-review";
|
|
46
|
-
import {
|
|
40
|
+
import { VERIFY_FILE } from "../paths";
|
|
47
41
|
import { currentBranch } from "../pr-context";
|
|
48
42
|
import { BatchPresenter } from "../renderers/batch-presenter";
|
|
49
43
|
import { FindingsPresenter } from "../renderers/findings-presenter";
|
|
50
44
|
import { VerifyPresenter } from "../renderers/verify-presenter";
|
|
51
45
|
import { pingFindingsStatus } from "../run-status";
|
|
46
|
+
import { systemClock } from "../services/clock";
|
|
52
47
|
import { FixBatchBuilder } from "../services/fix-batch-builder";
|
|
53
48
|
import { Verifier } from "../services/verifier";
|
|
49
|
+
import type { RepoIdentity, ReviewRecord, StorageProvider } from "../storage/types";
|
|
50
|
+
import { ReviewWorkArea } from "../storage/work-area";
|
|
54
51
|
import { SEVERITY_ORDER, type Severity } from "../types";
|
|
55
52
|
|
|
56
53
|
export type FindingsCommandResult = {
|
|
@@ -65,7 +62,6 @@ const VALUE_FLAGS = new Set([
|
|
|
65
62
|
"--severity",
|
|
66
63
|
"--file",
|
|
67
64
|
"--branch",
|
|
68
|
-
"--dir",
|
|
69
65
|
"--limit",
|
|
70
66
|
"--comment",
|
|
71
67
|
"--by",
|
|
@@ -85,6 +81,11 @@ function parseArgs(args: string[]): ParsedArgs {
|
|
|
85
81
|
const parsed: ParsedArgs = { positionals: [], values: {}, bools: new Set() };
|
|
86
82
|
for (let i = 0; i < args.length; i++) {
|
|
87
83
|
const arg = args[i];
|
|
84
|
+
if (arg === "--dir") {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"--dir foi removido; configure `storage` no houndsman.config.ts.",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
88
89
|
if (arg.startsWith("--")) {
|
|
89
90
|
if (VALUE_FLAGS.has(arg)) {
|
|
90
91
|
const value = args[i + 1];
|
|
@@ -101,36 +102,63 @@ function parseArgs(args: string[]): ParsedArgs {
|
|
|
101
102
|
return parsed;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
/** Contexto de uma review (storage + registro + staging). */
|
|
106
|
+
type CommandContext = {
|
|
107
|
+
storage: StorageProvider;
|
|
108
|
+
review: ReviewRecord;
|
|
109
|
+
work: ReviewWorkArea;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Resolve a review da branch atual (explícita ou via git). */
|
|
113
|
+
function openContext(
|
|
114
|
+
storage: StorageProvider,
|
|
115
|
+
repo: RepoIdentity,
|
|
107
116
|
parsed: ParsedArgs,
|
|
108
|
-
):
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
): CommandContext {
|
|
118
|
+
const branch = parsed.values["--branch"] ?? currentBranch();
|
|
119
|
+
const review = storage.findReview(repo.key, branch);
|
|
120
|
+
if (!review) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Nenhuma revisão para '${branch}'. Rode \`houndsman --finalize\` primeiro.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return { storage, review, work: new ReviewWorkArea(review) };
|
|
113
126
|
}
|
|
114
127
|
|
|
115
|
-
/** Lê o estado
|
|
116
|
-
function loadState(
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
/** Lê o estado de triagem; lança se a review ainda não tem findings. */
|
|
129
|
+
function loadState(ctx: CommandContext): FindingsStateFile {
|
|
130
|
+
const state = ctx.storage.readFindings(ctx.review.id);
|
|
131
|
+
if (!state) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Nenhum estado de triagem para '${ctx.review.branch}'. Rode \`houndsman --finalize\` primeiro.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return state;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Lê-modifica-grava o estado, atualizando `updatedAt` e a ordem. */
|
|
140
|
+
function mutateState(
|
|
141
|
+
ctx: CommandContext,
|
|
142
|
+
mutator: (state: FindingsStateFile) => void,
|
|
143
|
+
): FindingsStateFile {
|
|
144
|
+
const state = loadState(ctx);
|
|
145
|
+
mutator(state);
|
|
146
|
+
state.updatedAt = systemClock();
|
|
147
|
+
reorder(state);
|
|
148
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
149
|
+
return state;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Resolve um id (exato ou prefixo único), lançando erro explicativo. */
|
|
153
|
+
function requireFindingId(state: FindingsStateFile, input: string): string {
|
|
154
|
+
const resolution = resolveFindingId(state, input);
|
|
155
|
+
if (resolution.status === "found") return resolution.id;
|
|
156
|
+
if (resolution.status === "ambiguous") {
|
|
128
157
|
throw new Error(
|
|
129
|
-
`
|
|
158
|
+
`Id '${input}' ambíguo — candidatos: ${resolution.matches.join(", ")}.`,
|
|
130
159
|
);
|
|
131
160
|
}
|
|
132
|
-
|
|
133
|
-
return built;
|
|
161
|
+
throw new Error(`Finding '${input}' não encontrado.`);
|
|
134
162
|
}
|
|
135
163
|
|
|
136
164
|
// ---------------------------------------------------------------------------
|
|
@@ -181,64 +209,52 @@ function filterFrom(parsed: ParsedArgs): FindingsFilter {
|
|
|
181
209
|
};
|
|
182
210
|
}
|
|
183
211
|
|
|
184
|
-
/** Faz ping no
|
|
185
|
-
function pingStatus(
|
|
186
|
-
|
|
187
|
-
const state = FindingsStateStore.at(dir, branch).read();
|
|
188
|
-
if (state) pingFindingsStatus(runDir(dir, branch), state);
|
|
212
|
+
/** Faz ping no status da review (derivado do estado atual) após uma mutação. */
|
|
213
|
+
function pingStatus(ctx: CommandContext, state: FindingsStateFile): void {
|
|
214
|
+
pingFindingsStatus(ctx.storage, ctx.review.id, state);
|
|
189
215
|
}
|
|
190
216
|
|
|
191
217
|
/** Aplica uma atualização a um finding e persiste. */
|
|
192
218
|
function updateOne(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
id: string,
|
|
219
|
+
ctx: CommandContext,
|
|
220
|
+
input: string,
|
|
196
221
|
update: FindingUpdate,
|
|
197
222
|
): FindingState {
|
|
198
|
-
const { dir, branch } = resolveDir(config, parsed);
|
|
199
|
-
const store = FindingsStateStore.at(dir, branch);
|
|
200
223
|
let updated: FindingState | undefined;
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
updated = applyFindingUpdate(finding, update);
|
|
224
|
+
mutateState(ctx, (state) => {
|
|
225
|
+
const id = requireFindingId(state, input);
|
|
226
|
+
updated = applyFindingUpdate(state.findings[id], update);
|
|
205
227
|
});
|
|
206
|
-
if (!updated) throw new Error(`Finding '${
|
|
228
|
+
if (!updated) throw new Error(`Finding '${input}' não encontrado.`);
|
|
207
229
|
return updated;
|
|
208
230
|
}
|
|
209
231
|
|
|
210
232
|
/** Aplica a mesma atualização a vários ids (done/reset). */
|
|
211
233
|
function updateMany(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
ids: string[],
|
|
234
|
+
ctx: CommandContext,
|
|
235
|
+
inputs: string[],
|
|
215
236
|
update: FindingUpdate,
|
|
216
237
|
): FindingState[] {
|
|
217
|
-
const { dir, branch } = resolveDir(config, parsed);
|
|
218
|
-
const store = FindingsStateStore.at(dir, branch);
|
|
219
238
|
const updated: FindingState[] = [];
|
|
220
|
-
|
|
239
|
+
mutateState(ctx, (state) => {
|
|
240
|
+
const ids = inputs.map((input) => requireFindingId(state, input));
|
|
221
241
|
for (const id of ids) {
|
|
222
|
-
|
|
223
|
-
if (!finding) continue;
|
|
224
|
-
updated.push(applyFindingUpdate(finding, update));
|
|
242
|
+
updated.push(applyFindingUpdate(state.findings[id], update));
|
|
225
243
|
}
|
|
226
244
|
});
|
|
227
|
-
const missing = ids.filter((id) => !updated.some((f) => f.id === id));
|
|
228
|
-
if (missing.length > 0) {
|
|
229
|
-
throw new Error(`Findings não encontrados: ${missing.join(", ")}.`);
|
|
230
|
-
}
|
|
231
245
|
return updated;
|
|
232
246
|
}
|
|
233
247
|
|
|
234
248
|
/** Executa um subcomando de `findings` e devolve o texto + exit code. */
|
|
235
249
|
export function runFindingsCommand(
|
|
236
250
|
config: HoundsmanConfig,
|
|
251
|
+
storage: StorageProvider,
|
|
252
|
+
repo: RepoIdentity,
|
|
237
253
|
args: string[],
|
|
238
254
|
): FindingsCommandResult {
|
|
239
|
-
const parsed = parseArgs(args);
|
|
240
255
|
try {
|
|
241
|
-
|
|
256
|
+
const parsed = parseArgs(args);
|
|
257
|
+
return execute(config, storage, repo, parsed);
|
|
242
258
|
} catch (error) {
|
|
243
259
|
return {
|
|
244
260
|
output: error instanceof Error ? error.message : String(error),
|
|
@@ -247,10 +263,16 @@ export function runFindingsCommand(
|
|
|
247
263
|
}
|
|
248
264
|
}
|
|
249
265
|
|
|
250
|
-
function execute(
|
|
266
|
+
function execute(
|
|
267
|
+
_config: HoundsmanConfig,
|
|
268
|
+
storage: StorageProvider,
|
|
269
|
+
repo: RepoIdentity,
|
|
270
|
+
parsed: ParsedArgs,
|
|
271
|
+
): FindingsCommandResult {
|
|
272
|
+
const ctx = openContext(storage, repo, parsed);
|
|
251
273
|
switch (parsed.sub ?? "list") {
|
|
252
274
|
case "list": {
|
|
253
|
-
const state = loadState(
|
|
275
|
+
const state = loadState(ctx);
|
|
254
276
|
const filtered = filterFindings(state, filterFrom(parsed));
|
|
255
277
|
const limit = Number(parsed.values["--limit"]);
|
|
256
278
|
const findings =
|
|
@@ -263,11 +285,11 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
263
285
|
};
|
|
264
286
|
}
|
|
265
287
|
case "show": {
|
|
266
|
-
const state = loadState(
|
|
267
|
-
const
|
|
268
|
-
if (!
|
|
288
|
+
const state = loadState(ctx);
|
|
289
|
+
const input = parsed.positionals[0];
|
|
290
|
+
if (!input) throw new Error("Informe o id: houndsman findings show <id>");
|
|
291
|
+
const id = requireFindingId(state, input);
|
|
269
292
|
const finding = state.findings[id];
|
|
270
|
-
if (!finding) throw new Error(`Finding '${id}' não encontrado.`);
|
|
271
293
|
return {
|
|
272
294
|
output: parsed.bools.has("--json")
|
|
273
295
|
? JSON.stringify(finding, null, 2)
|
|
@@ -276,16 +298,17 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
276
298
|
};
|
|
277
299
|
}
|
|
278
300
|
case "set": {
|
|
279
|
-
const
|
|
280
|
-
if (!
|
|
301
|
+
const input = parsed.positionals[0];
|
|
302
|
+
if (!input)
|
|
303
|
+
throw new Error("Informe o id: houndsman findings set <id> [flags]");
|
|
281
304
|
const update = updateFrom(parsed);
|
|
282
305
|
if (!hasUpdate(update)) {
|
|
283
306
|
throw new Error(
|
|
284
307
|
"Nada para atualizar (use --decision, --exec ou --comment).",
|
|
285
308
|
);
|
|
286
309
|
}
|
|
287
|
-
const finding = updateOne(
|
|
288
|
-
pingStatus(
|
|
310
|
+
const finding = updateOne(ctx, input, update);
|
|
311
|
+
pingStatus(ctx, loadState(ctx));
|
|
289
312
|
return {
|
|
290
313
|
output: parsed.bools.has("--json")
|
|
291
314
|
? JSON.stringify(finding, null, 2)
|
|
@@ -295,8 +318,8 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
295
318
|
}
|
|
296
319
|
case "done": {
|
|
297
320
|
const ids = requireIds(parsed, "done");
|
|
298
|
-
const updated = updateMany(
|
|
299
|
-
pingStatus(
|
|
321
|
+
const updated = updateMany(ctx, ids, { execState: "done" });
|
|
322
|
+
pingStatus(ctx, loadState(ctx));
|
|
300
323
|
return {
|
|
301
324
|
output: `Marcados como done: ${updated.map((f) => f.id).join(", ")}.`,
|
|
302
325
|
exitCode: 0,
|
|
@@ -304,28 +327,26 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
304
327
|
}
|
|
305
328
|
case "reset": {
|
|
306
329
|
const ids = requireIds(parsed, "reset");
|
|
307
|
-
const updated = updateMany(
|
|
330
|
+
const updated = updateMany(ctx, ids, {
|
|
308
331
|
decision: "pending",
|
|
309
332
|
execState: "pending",
|
|
310
333
|
comment: "",
|
|
311
334
|
});
|
|
312
|
-
pingStatus(
|
|
335
|
+
pingStatus(ctx, loadState(ctx));
|
|
313
336
|
return {
|
|
314
337
|
output: `Triagem reiniciada: ${updated.map((f) => f.id).join(", ")}.`,
|
|
315
338
|
exitCode: 0,
|
|
316
339
|
};
|
|
317
340
|
}
|
|
318
341
|
case "sync": {
|
|
319
|
-
const
|
|
320
|
-
const store = FindingsStateStore.at(dir, branch);
|
|
321
|
-
const state = loadState(config, parsed);
|
|
342
|
+
const state = loadState(ctx);
|
|
322
343
|
const result = syncState(state, fileReader(process.cwd()));
|
|
323
|
-
|
|
324
|
-
pingStatus(
|
|
344
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
345
|
+
pingStatus(ctx, state);
|
|
325
346
|
const output = parsed.bools.has("--json")
|
|
326
347
|
? JSON.stringify(result, null, 2)
|
|
327
348
|
: [
|
|
328
|
-
`Sync — ${branch}: ${result.ok} ok, ${result.moved.length} movido(s), ${result.stale.length} stale.`,
|
|
349
|
+
`Sync — ${ctx.review.branch}: ${result.ok} ok, ${result.moved.length} movido(s), ${result.stale.length} stale.`,
|
|
329
350
|
result.moved.length > 0
|
|
330
351
|
? `Movidos: ${result.moved.join(", ")}`
|
|
331
352
|
: "",
|
|
@@ -338,7 +359,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
338
359
|
return { output, exitCode: 0 };
|
|
339
360
|
}
|
|
340
361
|
case "summary": {
|
|
341
|
-
const state = loadState(
|
|
362
|
+
const state = loadState(ctx);
|
|
342
363
|
const summary = summarize(state);
|
|
343
364
|
return {
|
|
344
365
|
output: parsed.bools.has("--json")
|
|
@@ -348,12 +369,13 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
348
369
|
};
|
|
349
370
|
}
|
|
350
371
|
case "path": {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
372
|
+
return {
|
|
373
|
+
output: ctx.storage.location(ctx.review.id),
|
|
374
|
+
exitCode: 0,
|
|
375
|
+
};
|
|
354
376
|
}
|
|
355
377
|
case "batch": {
|
|
356
|
-
const state = loadState(
|
|
378
|
+
const state = loadState(ctx);
|
|
357
379
|
const severity = parsed.values["--severity"];
|
|
358
380
|
const offset = Number(parsed.values["--offset"]);
|
|
359
381
|
const result = fixBatchBuilder.build(state, {
|
|
@@ -412,11 +434,13 @@ function requireIds(parsed: ParsedArgs, sub: string): string[] {
|
|
|
412
434
|
*/
|
|
413
435
|
export async function runVerifyCommand(
|
|
414
436
|
config: HoundsmanConfig,
|
|
437
|
+
storage: StorageProvider,
|
|
438
|
+
repo: RepoIdentity,
|
|
415
439
|
args: string[],
|
|
416
440
|
): Promise<FindingsCommandResult> {
|
|
417
|
-
const parsed = parseArgs(args);
|
|
418
441
|
try {
|
|
419
|
-
|
|
442
|
+
const parsed = parseArgs(args);
|
|
443
|
+
return await executeVerify(config, storage, repo, parsed);
|
|
420
444
|
} catch (error) {
|
|
421
445
|
return {
|
|
422
446
|
output: error instanceof Error ? error.message : String(error),
|
|
@@ -427,11 +451,12 @@ export async function runVerifyCommand(
|
|
|
427
451
|
|
|
428
452
|
async function executeVerify(
|
|
429
453
|
config: HoundsmanConfig,
|
|
454
|
+
storage: StorageProvider,
|
|
455
|
+
repo: RepoIdentity,
|
|
430
456
|
parsed: ParsedArgs,
|
|
431
457
|
): Promise<FindingsCommandResult> {
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
const state = loadState(config, parsed);
|
|
458
|
+
const ctx = openContext(storage, repo, parsed);
|
|
459
|
+
const state = loadState(ctx);
|
|
435
460
|
|
|
436
461
|
// Lazy: `../jev` (axios) só entra no caminho do verify.
|
|
437
462
|
const verifier = new Verifier();
|
|
@@ -471,23 +496,31 @@ async function executeVerify(
|
|
|
471
496
|
}
|
|
472
497
|
|
|
473
498
|
const summary = verifier.summarize(entries);
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
499
|
+
const location = ctx.storage.location(ctx.review.id);
|
|
500
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
501
|
+
name: VERIFY_FILE,
|
|
502
|
+
kind: "verify",
|
|
503
|
+
content: `${presenters.verify.render(state, entries, summary, { jevChecked, jevReason })}\n`,
|
|
504
|
+
});
|
|
479
505
|
|
|
480
506
|
let applied: string[] = [];
|
|
481
507
|
if (parsed.bools.has("--apply")) {
|
|
482
|
-
|
|
483
|
-
applied = verifier.applyResolutions(
|
|
508
|
+
const current = mutateState(ctx, (draft) => {
|
|
509
|
+
applied = verifier.applyResolutions(draft, entries);
|
|
484
510
|
});
|
|
485
|
-
pingStatus(
|
|
511
|
+
pingStatus(ctx, current);
|
|
486
512
|
}
|
|
487
513
|
|
|
488
514
|
const output = parsed.bools.has("--json")
|
|
489
515
|
? JSON.stringify(
|
|
490
|
-
{
|
|
516
|
+
{
|
|
517
|
+
branch: ctx.review.branch,
|
|
518
|
+
summary,
|
|
519
|
+
jevChecked,
|
|
520
|
+
jevReason,
|
|
521
|
+
applied,
|
|
522
|
+
entries,
|
|
523
|
+
},
|
|
491
524
|
null,
|
|
492
525
|
2,
|
|
493
526
|
)
|
|
@@ -497,7 +530,7 @@ async function executeVerify(
|
|
|
497
530
|
jevReason,
|
|
498
531
|
}),
|
|
499
532
|
"",
|
|
500
|
-
`Relatório: ${
|
|
533
|
+
`Relatório: ${location} (${VERIFY_FILE})`,
|
|
501
534
|
parsed.bools.has("--apply")
|
|
502
535
|
? `Aplicados (done): ${applied.length > 0 ? applied.join(", ") : "—"}.`
|
|
503
536
|
: "",
|
|
@@ -587,11 +620,13 @@ function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
|
|
|
587
620
|
*/
|
|
588
621
|
export async function runSubmitCommand(
|
|
589
622
|
config: HoundsmanConfig,
|
|
623
|
+
storage: StorageProvider,
|
|
624
|
+
repo: RepoIdentity,
|
|
590
625
|
args: string[],
|
|
591
626
|
): Promise<FindingsCommandResult> {
|
|
592
|
-
const parsed = parseArgs(args);
|
|
593
627
|
try {
|
|
594
|
-
|
|
628
|
+
const parsed = parseArgs(args);
|
|
629
|
+
return await executeSubmit(config, storage, repo, parsed);
|
|
595
630
|
} catch (error) {
|
|
596
631
|
return {
|
|
597
632
|
output: error instanceof Error ? error.message : String(error),
|
|
@@ -601,10 +636,13 @@ export async function runSubmitCommand(
|
|
|
601
636
|
}
|
|
602
637
|
|
|
603
638
|
async function executeSubmit(
|
|
604
|
-
|
|
639
|
+
_config: HoundsmanConfig,
|
|
640
|
+
storage: StorageProvider,
|
|
641
|
+
repo: RepoIdentity,
|
|
605
642
|
parsed: ParsedArgs,
|
|
606
643
|
): Promise<FindingsCommandResult> {
|
|
607
|
-
const
|
|
644
|
+
const ctx = openContext(storage, repo, parsed);
|
|
645
|
+
const state = loadState(ctx);
|
|
608
646
|
const selection = submitSelection(parsed);
|
|
609
647
|
const target = resolvePrTarget(defaultGhRunner, state.branch);
|
|
610
648
|
const preview = buildSubmitPreview(state, target, selection);
|
|
@@ -621,11 +659,8 @@ async function executeSubmit(
|
|
|
621
659
|
runner: defaultGhRunner,
|
|
622
660
|
selection,
|
|
623
661
|
});
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
state.branch,
|
|
627
|
-
).write(state);
|
|
628
|
-
pingStatus(config, parsed);
|
|
662
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
663
|
+
pingStatus(ctx, state);
|
|
629
664
|
|
|
630
665
|
return {
|
|
631
666
|
output: renderSubmitOutcome(outcome, json),
|
|
@@ -6,24 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
import { ArgParser } from "../arg-parser";
|
|
8
8
|
import { defaultOptions, type ReviewOptions } from "../orchestrator";
|
|
9
|
-
import { ARTIFACT_DIR } from "../paths";
|
|
10
9
|
|
|
11
10
|
/** Converte argv (sem o subcomando) nas opções de prepare/finalize. */
|
|
12
|
-
export function parseReviewArgs(
|
|
13
|
-
args: string[],
|
|
14
|
-
artifactDir: string = ARTIFACT_DIR,
|
|
15
|
-
): ReviewOptions {
|
|
11
|
+
export function parseReviewArgs(args: string[]): ReviewOptions {
|
|
16
12
|
const parser = ArgParser.from(args);
|
|
17
|
-
|
|
13
|
+
if (parser.has("--dir")) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"--dir foi removido; configure `storage` no houndsman.config.ts.",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const opts = defaultOptions();
|
|
18
19
|
|
|
19
20
|
const base = parser.value("--base");
|
|
20
21
|
const head = parser.value("--head");
|
|
21
22
|
const branch = parser.value("--branch");
|
|
22
|
-
const dir = parser.value("--dir");
|
|
23
23
|
if (base) opts.base = base;
|
|
24
24
|
if (head) opts.head = head;
|
|
25
25
|
if (branch) opts.branch = branch;
|
|
26
|
-
if (dir) opts.dir = dir;
|
|
27
26
|
|
|
28
27
|
if (parser.has("--finalize") || parser.has("--report-only")) opts.finalize = true;
|
|
29
28
|
if (parser.has("--fail-on-blocking")) opts.failOnBlocking = true;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* **do usuário**:
|
|
6
6
|
*
|
|
7
7
|
* .houndsman/
|
|
8
|
-
* ├── .gitignore AUTO — ignora core/
|
|
8
|
+
* ├── .gitignore AUTO — ignora core/ (gerado)
|
|
9
9
|
* ├── core/types.ts AUTO — snapshot dos tipos + helpers
|
|
10
10
|
* ├── core/skill/*.md AUTO — docs do skill (com {{projectName}} resolvido)
|
|
11
11
|
* ├── watchdogs/*.ts USUÁRIO
|
|
@@ -16,13 +16,7 @@
|
|
|
16
16
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { basename, join } from "node:path";
|
|
18
18
|
import { SKILL_DIR, SKILL_FILE } from "../assets";
|
|
19
|
-
import {
|
|
20
|
-
ARTIFACT_DIR,
|
|
21
|
-
CORE_DIR,
|
|
22
|
-
CORE_SKILL_DIR,
|
|
23
|
-
OUTPUT_FORMAT_PATH,
|
|
24
|
-
PROJECT_DIR,
|
|
25
|
-
} from "../paths";
|
|
19
|
+
import { CORE_DIR, CORE_SKILL_DIR, OUTPUT_FORMAT_PATH, PROJECT_DIR } from "../paths";
|
|
26
20
|
|
|
27
21
|
export type Harness = "agents" | "claude";
|
|
28
22
|
|
|
@@ -87,9 +81,9 @@ import type { HoundsmanConfig } from "./${CORE_DIR}/types";
|
|
|
87
81
|
/**
|
|
88
82
|
* Configuração do houndsman para ${projectName}.
|
|
89
83
|
*
|
|
90
|
-
*
|
|
91
|
-
* output em ${OUTPUT_FORMAT_PATH}. Ajuste
|
|
92
|
-
* docs conforme o projeto.
|
|
84
|
+
* Runs, findings e artefatos vivem no storage global (SQLite por default) —
|
|
85
|
+
* fora do projeto. O schema de output fica em ${OUTPUT_FORMAT_PATH}. Ajuste
|
|
86
|
+
* watchdogs, exclusões e mapeamentos de docs conforme o projeto.
|
|
93
87
|
*/
|
|
94
88
|
export const houndsmanConfig = {
|
|
95
89
|
projectName: "${projectName}",
|
|
@@ -182,10 +176,16 @@ export type HoundsmanConcurrency = {
|
|
|
182
176
|
jev?: number;
|
|
183
177
|
};
|
|
184
178
|
|
|
179
|
+
/** Backend de persistência (reviews, findings e artefatos). */
|
|
180
|
+
export type HoundsmanStorage = {
|
|
181
|
+
provider?: "sqlite" | "file";
|
|
182
|
+
path?: string;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
185
|
/** Contrato de configuração do motor. */
|
|
186
186
|
export type HoundsmanConfig<TId extends string = string> = {
|
|
187
187
|
projectName: string;
|
|
188
|
-
|
|
188
|
+
storage?: HoundsmanStorage;
|
|
189
189
|
watchdogIds?: readonly TId[];
|
|
190
190
|
watchdogs: Record<TId, WatchdogDefinition>;
|
|
191
191
|
exclusions: readonly RegExp[];
|
|
@@ -267,10 +267,9 @@ export function isExcluded(filePath: string): boolean {
|
|
|
267
267
|
const DOC_MAPPINGS_TEMPLATE = `/**
|
|
268
268
|
* Mapeamento de padrões nos paths → documentação relevante.
|
|
269
269
|
* Consumido pelo motor via \`HoundsmanConfig.docMappings\`.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
];
|
|
270
|
+
*/\`core/\` é regenerado por \`houndsman update\`.
|
|
271
|
+
# Runs, findings e artefatos vivem no storage global (fora do projeto).
|
|
272
|
+
core
|
|
274
273
|
`;
|
|
275
274
|
|
|
276
275
|
const PROJECT_GITIGNORE = `# Diretórios gerados pelo houndsman — \`core/\` é regenerado por \`houndsman update\`
|
package/src/core/config.ts
CHANGED
|
@@ -27,6 +27,20 @@ export type HoundsmanConcurrency = {
|
|
|
27
27
|
jev?: number;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
/** Backend de persistência (reviews, findings e artefatos). */
|
|
31
|
+
export type HoundsmanStorage = {
|
|
32
|
+
/**
|
|
33
|
+
* Provider de storage (default: `sqlite`). `file` mantém o formato antigo
|
|
34
|
+
* em arquivos, mas sob o diretório global do houndsman.
|
|
35
|
+
*/
|
|
36
|
+
provider?: "sqlite" | "file";
|
|
37
|
+
/**
|
|
38
|
+
* Caminho do banco (sqlite) ou raiz das runs (file). Default:
|
|
39
|
+
* `<HOUNDSMAN_HOME>/houndsman.db` / `<HOUNDSMAN_HOME>/runs`.
|
|
40
|
+
*/
|
|
41
|
+
path?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
30
44
|
/**
|
|
31
45
|
* Configuração completa do motor de revisão para um projeto.
|
|
32
46
|
*
|
|
@@ -35,8 +49,8 @@ export type HoundsmanConcurrency = {
|
|
|
35
49
|
export type HoundsmanConfig<TId extends string = string> = {
|
|
36
50
|
/** Nome legível do projeto (usado em prompts e docs). */
|
|
37
51
|
projectName: string;
|
|
38
|
-
/**
|
|
39
|
-
|
|
52
|
+
/** Persistência das runs e artefatos (default: SQLite global). */
|
|
53
|
+
storage?: HoundsmanStorage;
|
|
40
54
|
/**
|
|
41
55
|
* Ids dos watchdogs — **derivados** das chaves de `watchdogs` por
|
|
42
56
|
* `defineHoundsmanConfig` (não informar manualmente).
|
|
@@ -72,11 +86,6 @@ export function defineHoundsmanConfig<TId extends string>(
|
|
|
72
86
|
return { ...input, watchdogIds: Object.keys(input.watchdogs) as TId[] };
|
|
73
87
|
}
|
|
74
88
|
|
|
75
|
-
/** Diretório dos artefatos — o valor do projeto ou o default da camada. */
|
|
76
|
-
export function resolveArtifactDir(config: HoundsmanConfig): string {
|
|
77
|
-
return new ProjectConfig(config).artifactDir;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
89
|
/**
|
|
81
90
|
* Teto de arquivos por brief. Valor ausente/não-positivo cai no default —
|
|
82
91
|
* mantém o contexto do subagente limitado em PRs grandes.
|