@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.
Files changed (69) hide show
  1. package/README.md +18 -6
  2. package/package.json +1 -1
  3. package/src/cli/commands/context-command.ts +11 -9
  4. package/src/cli/commands/doctor-command.ts +5 -0
  5. package/src/cli/commands/findings-command.ts +13 -3
  6. package/src/cli/commands/review-command.ts +1 -2
  7. package/src/cli/help.ts +7 -3
  8. package/src/cli/index.ts +2 -2
  9. package/src/core/commands/context.ts +29 -13
  10. package/src/core/commands/findings.ts +155 -120
  11. package/src/core/commands/review.ts +7 -8
  12. package/src/core/commands/scaffold.ts +15 -16
  13. package/src/core/config.ts +16 -7
  14. package/src/core/engine/houndsman-engine.ts +15 -1
  15. package/src/core/engine/review-session.ts +37 -9
  16. package/src/core/findings-state.ts +32 -71
  17. package/src/core/jev/jev-candidates.ts +49 -35
  18. package/src/core/orchestrator.ts +175 -128
  19. package/src/core/paths.ts +23 -17
  20. package/src/core/pr-context.ts +19 -5
  21. package/src/core/project-config.ts +4 -5
  22. package/src/core/renderers/manifest-writer.ts +29 -18
  23. package/src/core/run-layout.ts +4 -104
  24. package/src/core/run-status.ts +28 -53
  25. package/src/core/services/git-repository.ts +16 -0
  26. package/src/core/services/project-files.ts +13 -1
  27. package/src/core/storage/factory.ts +24 -0
  28. package/src/core/storage/file/file-storage.ts +269 -0
  29. package/src/core/storage/paths.ts +64 -0
  30. package/src/core/storage/repo-identity.ts +70 -0
  31. package/src/core/storage/sqlite/mappers.ts +160 -0
  32. package/src/core/storage/sqlite/schema.ts +108 -0
  33. package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
  34. package/src/core/storage/types.ts +143 -0
  35. package/src/core/storage/work-area.ts +191 -0
  36. package/src/index.ts +27 -6
  37. package/src/server/artifacts.ts +8 -61
  38. package/src/server/client/ArtifactsPanel.tsx +137 -23
  39. package/src/server/client/FindingCard.tsx +37 -6
  40. package/src/server/client/MarkdownText.tsx +101 -0
  41. package/src/server/client/SubmitModal.tsx +9 -4
  42. package/src/server/client/code-highlight.ts +422 -0
  43. package/src/server/client/constants.ts +130 -24
  44. package/src/server/client/json-highlight.ts +55 -0
  45. package/src/server/client/markdown.ts +153 -0
  46. package/src/server/client/types.ts +23 -0
  47. package/src/server/index.ts +95 -15
  48. package/src/server/run-repository.ts +144 -136
  49. package/src/skill/OUTPUT-FORMAT.md +6 -4
  50. package/src/skill/README.md +22 -14
  51. package/src/skill/SETUP.md +4 -4
  52. package/src/skill/fix.md +5 -5
  53. package/src/skill/hunt.md +27 -18
  54. package/src/skill/train.md +1 -1
  55. package/src/skill/verify.md +5 -5
  56. package/src/tests/__snapshots__/baseline.test.ts.snap +44 -33
  57. package/src/tests/baseline.test.ts +76 -34
  58. package/src/tests/cli.test.ts +40 -19
  59. package/src/tests/code-highlight.test.ts +123 -0
  60. package/src/tests/domain.test.ts +1 -1
  61. package/src/tests/findings-commands.test.ts +153 -131
  62. package/src/tests/findings-state.test.ts +6 -3
  63. package/src/tests/fixtures/fake-project.ts +0 -1
  64. package/src/tests/infrastructure.test.ts +33 -38
  65. package/src/tests/markdown.test.ts +89 -0
  66. package/src/tests/review-scripts.test.ts +8 -5
  67. package/src/tests/run-status.test.ts +33 -23
  68. package/src/tests/server.test.ts +108 -75
  69. 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 `state.json` da run (`findings-state.ts`). Devolve o texto pronto
5
- * (markdown ou JSON) + exit code; o CLI decide o log/exit. O agente do usuário
6
- * usa `list`/`set`/`done` (e `batch` para lotes de correção) para interagir sem abrir o JSON à mão.
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
- * Se o `state.json` não existir, é re-construído a partir de
9
- * `findings/*.json` + `classification.json` (não exige `--finalize`).
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 { writeFileSync } from "node:fs";
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
- runDir,
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 { relFromRoot, VERIFY_FILE } from "../paths";
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
- /** Diretório da run + branch resolvida. */
105
- function resolveDir(
106
- config: HoundsmanConfig,
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
- ): { dir: string; branch: string } {
109
- return {
110
- dir: parsed.values["--dir"] ?? resolveArtifactDir(config),
111
- branch: parsed.values["--branch"] ?? currentBranch(),
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 da run; reconstrói dos findings se ausente. Lança se impossível. */
116
- function loadState(config: HoundsmanConfig, parsed: ParsedArgs): FindingsStateFile {
117
- const { dir, branch } = resolveDir(config, parsed);
118
- const store = FindingsStateStore.at(dir, branch);
119
- const existing = store.read();
120
- if (existing) return existing;
121
-
122
- const built = buildStateFromRunDir(
123
- runDir(dir, branch),
124
- branch,
125
- fileReader(process.cwd()),
126
- );
127
- if (!built) {
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
- `Nenhum estado de triagem para '${branch}'. Rode \`houndsman --finalize\` primeiro.`,
158
+ `Id '${input}' ambíguo — candidatos: ${resolution.matches.join(", ")}.`,
130
159
  );
131
160
  }
132
- store.write(built);
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 `run.json` (status derivado do estado atual) após uma mutação. */
185
- function pingStatus(config: HoundsmanConfig, parsed: ParsedArgs): void {
186
- const { dir, branch } = resolveDir(config, parsed);
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
- config: HoundsmanConfig,
194
- parsed: ParsedArgs,
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
- store.update((state) => {
202
- const finding = state.findings[id];
203
- if (!finding) return;
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 '${id}' não encontrado.`);
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
- config: HoundsmanConfig,
213
- parsed: ParsedArgs,
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
- store.update((state) => {
239
+ mutateState(ctx, (state) => {
240
+ const ids = inputs.map((input) => requireFindingId(state, input));
221
241
  for (const id of ids) {
222
- const finding = state.findings[id];
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
- return execute(config, parsed);
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(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandResult {
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(config, parsed);
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(config, parsed);
267
- const id = parsed.positionals[0];
268
- if (!id) throw new Error("Informe o id: houndsman findings show <id>");
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 id = parsed.positionals[0];
280
- if (!id) throw new Error("Informe o id: houndsman findings set <id> [flags]");
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(config, parsed, id, update);
288
- pingStatus(config, parsed);
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(config, parsed, ids, { execState: "done" });
299
- pingStatus(config, parsed);
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(config, parsed, ids, {
330
+ const updated = updateMany(ctx, ids, {
308
331
  decision: "pending",
309
332
  execState: "pending",
310
333
  comment: "",
311
334
  });
312
- pingStatus(config, parsed);
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 { dir, branch } = resolveDir(config, parsed);
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
- store.write(state);
324
- pingStatus(config, parsed);
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(config, parsed);
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
- const { dir, branch } = resolveDir(config, parsed);
352
- const path = FindingsStateStore.at(dir, branch).filePath;
353
- return { output: relFromRoot(path), exitCode: 0 };
372
+ return {
373
+ output: ctx.storage.location(ctx.review.id),
374
+ exitCode: 0,
375
+ };
354
376
  }
355
377
  case "batch": {
356
- const state = loadState(config, parsed);
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
- return await executeVerify(config, parsed);
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 { dir, branch } = resolveDir(config, parsed);
433
- const store = FindingsStateStore.at(dir, branch);
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 reportPath = join(runDir(dir, branch), VERIFY_FILE);
475
- writeFileSync(
476
- reportPath,
477
- `${presenters.verify.render(state, entries, summary, { jevChecked, jevReason })}\n`,
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
- store.update((current) => {
483
- applied = verifier.applyResolutions(current, entries);
508
+ const current = mutateState(ctx, (draft) => {
509
+ applied = verifier.applyResolutions(draft, entries);
484
510
  });
485
- pingStatus(config, parsed);
511
+ pingStatus(ctx, current);
486
512
  }
487
513
 
488
514
  const output = parsed.bools.has("--json")
489
515
  ? JSON.stringify(
490
- { branch, summary, jevChecked, jevReason, applied, entries },
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: ${relFromRoot(reportPath)}`,
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
- return await executeSubmit(config, parsed);
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
- config: HoundsmanConfig,
639
+ _config: HoundsmanConfig,
640
+ storage: StorageProvider,
641
+ repo: RepoIdentity,
605
642
  parsed: ParsedArgs,
606
643
  ): Promise<FindingsCommandResult> {
607
- const state = loadState(config, parsed);
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
- FindingsStateStore.at(
625
- parsed.values["--dir"] ?? resolveArtifactDir(config),
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
- const opts = defaultOptions(artifactDir);
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/ e .runs/ (gerados/temporários)
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
- * Caminhos são resolvidos pela camada: artefatos em ${ARTIFACT_DIR} e schema de
91
- * output em ${OUTPUT_FORMAT_PATH}. Ajuste watchdogs, exclusões e mapeamentos de
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
- artifactDir?: string;
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
- export const DOC_MAPPINGS: readonly (readonly [RegExp, readonly string[]])[] = [
272
- [/^src\\//, ["AGENTS.md"]],
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\`
@@ -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
- /** Diretório dos artefatos (default: `ARTIFACT_DIR` da camada). */
39
- artifactDir?: string;
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.