@payconductor/houndsman 0.2.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 (107) hide show
  1. package/README.md +18 -6
  2. package/package.json +1 -1
  3. package/src/cli/arg-parser.ts +8 -0
  4. package/src/cli/commands/command.ts +23 -0
  5. package/src/cli/commands/context-command.ts +40 -0
  6. package/src/cli/commands/doctor-command.ts +60 -0
  7. package/src/cli/commands/findings-command.ts +38 -0
  8. package/src/cli/commands/help-command.ts +13 -0
  9. package/src/cli/commands/registry.ts +34 -0
  10. package/src/cli/commands/review-command.ts +21 -0
  11. package/src/cli/commands/server-command.ts +37 -0
  12. package/src/cli/commands/setup-command.ts +34 -0
  13. package/src/cli/commands/watchdog-command.ts +34 -0
  14. package/src/cli/help.ts +47 -0
  15. package/src/cli/index.ts +60 -268
  16. package/src/core/arg-parser.ts +48 -0
  17. package/src/core/commands/context.ts +29 -13
  18. package/src/core/commands/findings.ts +185 -260
  19. package/src/core/commands/review.ts +21 -39
  20. package/src/core/commands/scaffold.ts +189 -80
  21. package/src/core/commands/setup.ts +16 -11
  22. package/src/core/commands/update.ts +7 -7
  23. package/src/core/commands/watchdog.ts +8 -80
  24. package/src/core/config.ts +24 -25
  25. package/src/core/defaults.ts +20 -0
  26. package/src/core/diff-source.ts +1 -13
  27. package/src/core/doc-discoverer.ts +25 -15
  28. package/src/core/domain/budget.ts +42 -0
  29. package/src/core/domain/classification.ts +94 -0
  30. package/src/core/domain/diff.ts +53 -0
  31. package/src/core/domain/watchdog.ts +96 -0
  32. package/src/core/engine/carryover.ts +67 -0
  33. package/src/core/engine/houndsman-engine.ts +65 -0
  34. package/src/core/engine/review-scope.ts +95 -0
  35. package/src/core/engine/review-session.ts +79 -0
  36. package/src/core/file-classifier.ts +11 -67
  37. package/src/core/findings-state.ts +193 -100
  38. package/src/core/git-revision.ts +14 -40
  39. package/src/core/git-runner.ts +46 -0
  40. package/src/core/github-review.ts +4 -33
  41. package/src/core/jev/jev-candidates.ts +49 -35
  42. package/src/core/orchestrator.ts +247 -421
  43. package/src/core/paths.ts +26 -11
  44. package/src/core/pr-context.ts +39 -51
  45. package/src/core/project-config.ts +56 -0
  46. package/src/core/renderers/batch-presenter.ts +40 -0
  47. package/src/core/renderers/brief-renderer.ts +23 -0
  48. package/src/core/renderers/findings-presenter.ts +74 -0
  49. package/src/core/renderers/manifest-writer.ts +123 -0
  50. package/src/core/renderers/report-renderer.ts +66 -0
  51. package/src/core/renderers/verify-presenter.ts +50 -0
  52. package/src/core/renderers/watchdog-presenter.ts +100 -0
  53. package/src/core/report-builder.ts +12 -7
  54. package/src/core/run-layout.ts +8 -0
  55. package/src/core/run-status.ts +29 -59
  56. package/src/core/services/clock.ts +12 -0
  57. package/src/core/services/file-classifier.ts +34 -0
  58. package/src/core/services/fix-batch-builder.ts +13 -0
  59. package/src/core/services/gh-client.ts +54 -0
  60. package/src/core/services/git-repository.ts +139 -0
  61. package/src/core/services/project-files.ts +91 -0
  62. package/src/core/services/verifier.ts +58 -0
  63. package/src/core/storage/factory.ts +24 -0
  64. package/src/core/storage/file/file-storage.ts +269 -0
  65. package/src/core/storage/paths.ts +64 -0
  66. package/src/core/storage/repo-identity.ts +70 -0
  67. package/src/core/storage/sqlite/mappers.ts +160 -0
  68. package/src/core/storage/sqlite/schema.ts +108 -0
  69. package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
  70. package/src/core/storage/types.ts +143 -0
  71. package/src/core/storage/work-area.ts +191 -0
  72. package/src/core/verify.ts +2 -1
  73. package/src/core/watchdog-registry.ts +58 -16
  74. package/src/index.ts +57 -6
  75. package/src/server/artifacts.ts +8 -61
  76. package/src/server/client/ArtifactsPanel.tsx +137 -23
  77. package/src/server/client/FindingCard.tsx +37 -6
  78. package/src/server/client/MarkdownText.tsx +101 -0
  79. package/src/server/client/SubmitModal.tsx +9 -4
  80. package/src/server/client/code-highlight.ts +422 -0
  81. package/src/server/client/constants.ts +130 -24
  82. package/src/server/client/json-highlight.ts +55 -0
  83. package/src/server/client/markdown.ts +153 -0
  84. package/src/server/client/types.ts +23 -0
  85. package/src/server/index.ts +155 -245
  86. package/src/server/run-repository.ts +299 -0
  87. package/src/skill/OUTPUT-FORMAT.md +6 -4
  88. package/src/skill/README.md +22 -14
  89. package/src/skill/SETUP.md +4 -4
  90. package/src/skill/fix.md +5 -5
  91. package/src/skill/hunt.md +27 -18
  92. package/src/skill/train.md +1 -1
  93. package/src/skill/verify.md +5 -5
  94. package/src/tests/__snapshots__/baseline.test.ts.snap +825 -0
  95. package/src/tests/baseline.test.ts +555 -493
  96. package/src/tests/cli.test.ts +40 -19
  97. package/src/tests/code-highlight.test.ts +123 -0
  98. package/src/tests/domain.test.ts +328 -0
  99. package/src/tests/findings-commands.test.ts +153 -131
  100. package/src/tests/findings-state.test.ts +6 -3
  101. package/src/tests/fixtures/fake-project.ts +0 -1
  102. package/src/tests/infrastructure.test.ts +205 -0
  103. package/src/tests/markdown.test.ts +89 -0
  104. package/src/tests/review-scripts.test.ts +8 -5
  105. package/src/tests/run-status.test.ts +33 -23
  106. package/src/tests/server.test.ts +108 -75
  107. package/src/tests/storage.test.ts +386 -0
@@ -4,49 +4,31 @@
4
4
  * Biblioteca: NÃO usa `process.argv`/`process.exit` (o CLI decide o exit code).
5
5
  */
6
6
 
7
+ import { ArgParser } from "../arg-parser";
7
8
  import { defaultOptions, type ReviewOptions } from "../orchestrator";
8
- import { ARTIFACT_DIR } from "../paths";
9
9
 
10
- const VALUE_FLAGS = new Set(["--base", "--head", "--branch", "--dir"]);
11
- const BOOLEAN_FLAGS = new Set([
12
- "--finalize",
13
- "--report-only",
14
- "--fail-on-blocking",
15
- "--clean",
16
- "--no-jev",
17
- "--full",
18
- ]);
10
+ /** Converte argv (sem o subcomando) nas opções de prepare/finalize. */
11
+ export function parseReviewArgs(args: string[]): ReviewOptions {
12
+ const parser = ArgParser.from(args);
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();
19
19
 
20
- function applyValue(opts: ReviewOptions, flag: string, value: string | undefined): void {
21
- if (!value) return;
22
- if (flag === "--base") opts.base = value;
23
- else if (flag === "--head") opts.head = value;
24
- else if (flag === "--branch") opts.branch = value;
25
- else if (flag === "--dir") opts.dir = value;
26
- }
20
+ const base = parser.value("--base");
21
+ const head = parser.value("--head");
22
+ const branch = parser.value("--branch");
23
+ if (base) opts.base = base;
24
+ if (head) opts.head = head;
25
+ if (branch) opts.branch = branch;
27
26
 
28
- function applyBoolean(opts: ReviewOptions, flag: string): void {
29
- if (flag === "--finalize" || flag === "--report-only") opts.finalize = true;
30
- else if (flag === "--fail-on-blocking") opts.failOnBlocking = true;
31
- else if (flag === "--clean") opts.clean = true;
32
- else if (flag === "--no-jev") opts.noJev = true;
33
- else if (flag === "--full") opts.full = true;
34
- }
27
+ if (parser.has("--finalize") || parser.has("--report-only")) opts.finalize = true;
28
+ if (parser.has("--fail-on-blocking")) opts.failOnBlocking = true;
29
+ if (parser.has("--clean")) opts.clean = true;
30
+ if (parser.has("--no-jev")) opts.noJev = true;
31
+ if (parser.has("--full")) opts.full = true;
35
32
 
36
- /** Converte argv (sem o subcomando) nas opções de prepare/finalize. */
37
- export function parseReviewArgs(
38
- args: string[],
39
- artifactDir: string = ARTIFACT_DIR,
40
- ): ReviewOptions {
41
- const opts = defaultOptions(artifactDir);
42
- for (let i = 0; i < args.length; i++) {
43
- const arg = args[i];
44
- if (VALUE_FLAGS.has(arg)) {
45
- applyValue(opts, arg, args[i + 1]);
46
- i++;
47
- } else if (BOOLEAN_FLAGS.has(arg)) {
48
- applyBoolean(opts, arg);
49
- }
50
- }
51
33
  return opts;
52
34
  }
@@ -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\`
@@ -280,83 +279,193 @@ core/
280
279
  `;
281
280
 
282
281
  // ---------------------------------------------------------------------------
283
- // Escrita
282
+ // Templates (arquivo → conteúdo)
284
283
  // ---------------------------------------------------------------------------
285
284
 
286
- /** Escreve um arquivo do usuário somente se ausente (ou se `force`). */
287
- function writeUserFile(
288
- ctx: ScaffoldContext,
289
- relativePath: string,
290
- content: string,
291
- ): void {
292
- const full = join(ctx.cwd, relativePath);
293
- if (existsSync(full) && !ctx.force) {
294
- ctx.skipped.push(relativePath);
295
- return;
285
+ /** Contexto de render de um template. */
286
+ type TemplateContext = { projectName: string };
287
+
288
+ /** Um arquivo da camada (caminho relativo + conteúdo). */
289
+ interface ScaffoldTemplate {
290
+ readonly path: string;
291
+ render(context: TemplateContext): string;
292
+ }
293
+
294
+ class ConfigTemplate implements ScaffoldTemplate {
295
+ readonly path = "houndsman.config.ts";
296
+
297
+ render(context: TemplateContext): string {
298
+ return CONFIG_TEMPLATE(context.projectName);
296
299
  }
297
- mkdirSync(join(full, ".."), { recursive: true });
298
- writeFileSync(full, content);
299
- ctx.created.push(relativePath);
300
300
  }
301
301
 
302
- /** Escreve/sobrescreve um arquivo auto-gerado. */
303
- function writeGeneratedFile(
304
- ctx: ScaffoldContext,
305
- relativePath: string,
306
- content: string,
307
- ): void {
308
- const full = join(ctx.cwd, relativePath);
309
- mkdirSync(join(full, ".."), { recursive: true });
310
- writeFileSync(full, content);
311
- ctx.generated.push(relativePath);
302
+ class WatchdogExampleTemplate implements ScaffoldTemplate {
303
+ readonly path = join(PROJECT_DIR, "watchdogs", "example.ts");
304
+
305
+ render(): string {
306
+ return WATCHDOG_EXAMPLE;
307
+ }
312
308
  }
313
309
 
314
- // ---------------------------------------------------------------------------
315
- // Camadas
316
- // ---------------------------------------------------------------------------
310
+ class WatchdogsIndexTemplate implements ScaffoldTemplate {
311
+ readonly path = join(PROJECT_DIR, "watchdogs", "index.ts");
317
312
 
318
- /** Arquivos do usuário: config, watchdogs, exclusões e mapeamentos. */
319
- export function writeUserLayer(ctx: ScaffoldContext): void {
320
- writeUserFile(ctx, "houndsman.config.ts", CONFIG_TEMPLATE(ctx.projectName));
321
- writeUserFile(ctx, join(PROJECT_DIR, "watchdogs", "example.ts"), WATCHDOG_EXAMPLE);
322
- writeUserFile(ctx, join(PROJECT_DIR, "watchdogs", "index.ts"), WATCHDOGS_INDEX);
323
- writeUserFile(ctx, join(PROJECT_DIR, "exclusions.ts"), EXCLUSIONS_TEMPLATE);
324
- writeUserFile(ctx, join(PROJECT_DIR, "doc-mappings.ts"), DOC_MAPPINGS_TEMPLATE);
313
+ render(): string {
314
+ return WATCHDOGS_INDEX;
315
+ }
325
316
  }
326
317
 
327
- /**
328
- * Arquivos auto-gerados: tipos, docs do skill, `.gitignore` (ignora `core/` e
329
- * `.runs/`) e o `SKILL.md` de cada harness. Sempre sobrescritos (é o alvo do
330
- * `update`).
331
- */
332
- export function syncGeneratedFiles(ctx: ScaffoldContext): void {
333
- writeGeneratedFile(ctx, join(CORE_DIR, "types.ts"), TYPES_TEMPLATE);
334
- writeGeneratedFile(ctx, join(PROJECT_DIR, ".gitignore"), PROJECT_GITIGNORE);
335
-
336
- // Docs do skill vão para `.houndsman/core/skill/`. O `SKILL.md` é o entry point
337
- // dos harnesses (escrito em `writeHarnessPointers`), não um doc da camada.
338
- for (const name of readdirSync(SKILL_DIR)) {
339
- if (!name.endsWith(".md") || name === SKILL_FILE) continue;
340
- const raw = readFileSync(join(SKILL_DIR, name), "utf8");
341
- writeGeneratedFile(
342
- ctx,
343
- join(CORE_SKILL_DIR, name),
344
- raw.replaceAll("{{projectName}}", ctx.projectName),
318
+ class ExclusionsTemplate implements ScaffoldTemplate {
319
+ readonly path = join(PROJECT_DIR, "exclusions.ts");
320
+
321
+ render(): string {
322
+ return EXCLUSIONS_TEMPLATE;
323
+ }
324
+ }
325
+
326
+ class DocMappingsTemplate implements ScaffoldTemplate {
327
+ readonly path = join(PROJECT_DIR, "doc-mappings.ts");
328
+
329
+ render(): string {
330
+ return DOC_MAPPINGS_TEMPLATE;
331
+ }
332
+ }
333
+
334
+ class TypesTemplate implements ScaffoldTemplate {
335
+ readonly path = join(CORE_DIR, "types.ts");
336
+
337
+ render(): string {
338
+ return TYPES_TEMPLATE;
339
+ }
340
+ }
341
+
342
+ class GitignoreTemplate implements ScaffoldTemplate {
343
+ readonly path = join(PROJECT_DIR, ".gitignore");
344
+
345
+ render(): string {
346
+ return PROJECT_GITIGNORE;
347
+ }
348
+ }
349
+
350
+ class SkillDocTemplate implements ScaffoldTemplate {
351
+ private readonly fileName: string;
352
+
353
+ constructor(fileName: string) {
354
+ this.fileName = fileName;
355
+ }
356
+
357
+ get path(): string {
358
+ return join(CORE_SKILL_DIR, this.fileName);
359
+ }
360
+
361
+ render(context: TemplateContext): string {
362
+ return readFileSync(join(SKILL_DIR, this.fileName), "utf8").replaceAll(
363
+ "{{projectName}}",
364
+ context.projectName,
345
365
  );
346
366
  }
367
+ }
347
368
 
348
- for (const harness of ctx.harnesses) writeHarnessPointers(ctx, harness);
369
+ class HarnessPointerTemplate implements ScaffoldTemplate {
370
+ private readonly harness: Harness;
371
+
372
+ constructor(harness: Harness) {
373
+ this.harness = harness;
374
+ }
375
+
376
+ get path(): string {
377
+ return join(`.${this.harness}`, "skills", "houndsman", SKILL_FILE);
378
+ }
379
+
380
+ render(context: TemplateContext): string {
381
+ return readFileSync(join(SKILL_DIR, SKILL_FILE), "utf8").replaceAll(
382
+ "{{projectName}}",
383
+ context.projectName,
384
+ );
385
+ }
349
386
  }
350
387
 
351
- /**
352
- * Escreve o `SKILL.md` do harness a partir do asset base (`src/skill/SKILL.md`),
353
- * resolvendo `{{projectName}}`. O texto do skill vive no asset — não aqui.
354
- */
355
- function writeHarnessPointers(ctx: ScaffoldContext, harness: Harness): void {
356
- const raw = readFileSync(join(SKILL_DIR, SKILL_FILE), "utf8");
357
- writeGeneratedFile(
358
- ctx,
359
- join(`.${harness}`, "skills", "houndsman", SKILL_FILE),
360
- raw.replaceAll("{{projectName}}", ctx.projectName),
361
- );
388
+ // ---------------------------------------------------------------------------
389
+ // Scaffolder
390
+ // ---------------------------------------------------------------------------
391
+
392
+ /** Escreve as camadas do projeto (`setup`) e sincroniza os auto-gerados (`update`). */
393
+ export class ProjectScaffolder {
394
+ private readonly ctx: ScaffoldContext;
395
+
396
+ constructor(ctx: ScaffoldContext) {
397
+ this.ctx = ctx;
398
+ }
399
+
400
+ /** Arquivos do usuário: config, watchdogs, exclusões e mapeamentos. */
401
+ writeUserLayer(): void {
402
+ const context = this.context();
403
+ const templates: ScaffoldTemplate[] = [
404
+ new ConfigTemplate(),
405
+ new WatchdogExampleTemplate(),
406
+ new WatchdogsIndexTemplate(),
407
+ new ExclusionsTemplate(),
408
+ new DocMappingsTemplate(),
409
+ ];
410
+ for (const template of templates) this.writeUserFile(template, context);
411
+ }
412
+
413
+ /** Arquivos auto-gerados (sempre sobrescritos). */
414
+ syncGenerated(): void {
415
+ const context = this.context();
416
+ this.writeGeneratedFile(new TypesTemplate(), context);
417
+ this.writeGeneratedFile(new GitignoreTemplate(), context);
418
+
419
+ // Docs do skill vão para `.houndsman/core/skill/`. O `SKILL.md` é o entry
420
+ // point dos harnesses, não um doc da camada.
421
+ for (const name of readdirSync(SKILL_DIR)) {
422
+ if (!name.endsWith(".md") || name === SKILL_FILE) continue;
423
+ this.writeGeneratedFile(new SkillDocTemplate(name), context);
424
+ }
425
+
426
+ for (const harness of this.ctx.harnesses) {
427
+ this.writeGeneratedFile(new HarnessPointerTemplate(harness), context);
428
+ }
429
+ }
430
+
431
+ private context(): TemplateContext {
432
+ return { projectName: this.ctx.projectName };
433
+ }
434
+
435
+ /** Usuário: só escreve se ausente (ou `force`). */
436
+ private writeUserFile(template: ScaffoldTemplate, context: TemplateContext): void {
437
+ const full = join(this.ctx.cwd, template.path);
438
+ if (existsSync(full) && !this.ctx.force) {
439
+ this.ctx.skipped.push(template.path);
440
+ return;
441
+ }
442
+ mkdirSync(join(full, ".."), { recursive: true });
443
+ writeFileSync(full, template.render(context));
444
+ this.ctx.created.push(template.path);
445
+ }
446
+
447
+ /** Auto-gerado: sempre sobrescreve. */
448
+ private writeGeneratedFile(
449
+ template: ScaffoldTemplate,
450
+ context: TemplateContext,
451
+ ): void {
452
+ const full = join(this.ctx.cwd, template.path);
453
+ mkdirSync(join(full, ".."), { recursive: true });
454
+ writeFileSync(full, template.render(context));
455
+ this.ctx.generated.push(template.path);
456
+ }
457
+ }
458
+
459
+ // ---------------------------------------------------------------------------
460
+ // Fachada (compatibilidade)
461
+ // ---------------------------------------------------------------------------
462
+
463
+ /** Arquivos do usuário (setup). */
464
+ export function writeUserLayer(ctx: ScaffoldContext): void {
465
+ new ProjectScaffolder(ctx).writeUserLayer();
466
+ }
467
+
468
+ /** Arquivos auto-gerados (setup/update). */
469
+ export function syncGeneratedFiles(ctx: ScaffoldContext): void {
470
+ new ProjectScaffolder(ctx).syncGenerated();
362
471
  }
@@ -49,27 +49,32 @@ function ensurePackageScript(ctx: ScaffoldContext): void {
49
49
  ctx.created.push("package.json#scripts.houndsman");
50
50
  }
51
51
 
52
- /** Cria a configuração inicial do projeto. */
53
- export async function runSetupCommand(options: SetupOptions): Promise<void> {
52
+ /** Cria a configuração inicial do projeto. Devolve as mensagens do CLI. */
53
+ export async function runSetupCommand(options: SetupOptions): Promise<string[]> {
54
54
  const ctx = createScaffoldContext(
55
55
  options.cwd,
56
56
  options.args,
57
57
  options.args.includes("--force"),
58
58
  );
59
59
 
60
- console.log(`→ Configurando houndsman em ${ctx.cwd} (projeto: ${ctx.projectName})`);
60
+ const messages = [
61
+ `→ Configurando houndsman em ${ctx.cwd} (projeto: ${ctx.projectName})`,
62
+ ];
61
63
 
62
64
  writeUserLayer(ctx);
63
65
  syncGeneratedFiles(ctx);
64
66
  ensurePackageScript(ctx);
65
67
 
66
- for (const path of ctx.created) console.log(` + ${path}`);
67
- for (const path of ctx.skipped) console.log(` = ${path} (mantido)`);
68
- for (const path of ctx.generated) console.log(` ~ ${path} (auto)`);
68
+ for (const path of ctx.created) messages.push(` + ${path}`);
69
+ for (const path of ctx.skipped) messages.push(` = ${path} (mantido)`);
70
+ for (const path of ctx.generated) messages.push(` ~ ${path} (auto)`);
69
71
 
70
- console.log("\nPróximos passos:");
71
- console.log(" 1. Ajuste `houndsman.config.ts` e `.houndsman/watchdogs/`.");
72
- console.log(" 2. Valide: bun run houndsman doctor");
73
- console.log(" 3. Prepare a revisão: bun run houndsman");
74
- console.log(" · Sincronizar arquivos auto-gerados: bun run houndsman update");
72
+ messages.push(
73
+ "\nPróximos passos:",
74
+ " 1. Ajuste `houndsman.config.ts` e `.houndsman/watchdogs/`.",
75
+ " 2. Valide: bun run houndsman doctor",
76
+ " 3. Prepare a revisão: bun run houndsman",
77
+ " · Sincronizar arquivos auto-gerados: bun run houndsman update",
78
+ );
79
+ return messages;
75
80
  }
@@ -13,18 +13,18 @@ export type UpdateOptions = {
13
13
  args: string[];
14
14
  };
15
15
 
16
- /** Sincroniza os arquivos auto-gerados da camada de projeto. */
17
- export async function runUpdateCommand(options: UpdateOptions): Promise<void> {
16
+ /** Sincroniza os arquivos auto-gerados da camada de projeto. Devolve mensagens. */
17
+ export async function runUpdateCommand(options: UpdateOptions): Promise<string[]> {
18
18
  const ctx = createScaffoldContext(options.cwd, options.args, false);
19
-
20
- console.log(`→ Sincronizando arquivos auto-gerados em ${ctx.cwd}`);
19
+ const messages = [`→ Sincronizando arquivos auto-gerados em ${ctx.cwd}`];
21
20
 
22
21
  syncGeneratedFiles(ctx);
23
22
 
24
- for (const path of ctx.generated) console.log(` ~ ${path} (auto)`);
23
+ for (const path of ctx.generated) messages.push(` ~ ${path} (auto)`);
25
24
 
26
- console.log("\nArquivos do usuário não foram tocados:");
27
- console.log(
25
+ messages.push(
26
+ "\nArquivos do usuário não foram tocados:",
28
27
  " .houndsman/watchdogs/ · exclusions.ts · doc-mappings.ts · houndsman.config.ts",
29
28
  );
29
+ return messages;
30
30
  }
@@ -6,12 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { HoundsmanConfig } from "../config";
9
- import {
10
- type Rule,
11
- SEVERITY_EMOJI,
12
- severityRank,
13
- type WatchdogDefinition,
14
- } from "../types";
9
+ import { WatchdogPresenter } from "../renderers/watchdog-presenter";
15
10
  import { findWatchdog, getWatchdogContext } from "../watchdog-registry";
16
11
 
17
12
  export type WatchdogContextOptions = {
@@ -21,7 +16,7 @@ export type WatchdogContextOptions = {
21
16
  json?: boolean;
22
17
  };
23
18
 
24
- type WatchdogContext = ReturnType<typeof getWatchdogContext>;
19
+ const presenter = new WatchdogPresenter();
25
20
 
26
21
  /** Renderiza o contexto de um watchdog (markdown ou JSON). `null` se o id não existe. */
27
22
  export function renderWatchdogContext(
@@ -29,77 +24,10 @@ export function renderWatchdogContext(
29
24
  id: string,
30
25
  opts: WatchdogContextOptions = {},
31
26
  ): string | null {
32
- const def = findWatchdog(config, id);
33
- if (!def) return null;
34
- const ctx = getWatchdogContext(config, def.id);
35
- const withRules = opts.withRules ?? false;
36
- return opts.json
37
- ? serializeWatchdogContext(ctx, def, withRules)
38
- : renderWatchdogMarkdown(ctx, def, withRules);
39
- }
40
-
41
- /** Payload serializável (regras incluídas apenas com `withRules`). */
42
- function serializeWatchdogContext(
43
- ctx: WatchdogContext,
44
- def: WatchdogDefinition,
45
- withRules: boolean,
46
- ): string {
47
- const payload = withRules ? { ...ctx, rules: def.rules } : { ...ctx };
48
- return JSON.stringify(payload, null, 2);
49
- }
50
-
51
- /** Lista de itens em bloco `## <título>` (vazio se não houver itens). */
52
- function section(title: string, items: readonly string[] | undefined): string[] {
53
- if (!items || items.length === 0) return [];
54
- return [`## ${title}`, "", ...items.map((item) => `- ${item}`), ""];
55
- }
56
-
57
- /** Regras com emoji de severidade em bloco `## <título>`. */
58
- function ruleSection(title: string, rules: readonly Rule[]): string[] {
59
- return [
60
- `## ${title}`,
61
- "",
62
- ...rules.map(
63
- (rule) =>
64
- `- ${SEVERITY_EMOJI[rule.severity]} **${rule.severity}** — ${rule.text}`,
65
- ),
66
- "",
67
- ];
68
- }
69
-
70
- /** Render markdown legível do contexto do watchdog. */
71
- function renderWatchdogMarkdown(
72
- ctx: WatchdogContext,
73
- def: WatchdogDefinition,
74
- withRules: boolean,
75
- ): string {
76
- const lines: string[] = [
77
- `# ${ctx.name} (\`${ctx.id}\`)`,
78
- "",
79
- `**Descrição:** ${ctx.description}`,
80
- `**Escopo:** ${ctx.scope}`,
81
- "",
82
- ];
83
-
84
- lines.push(...section("Prioridade de leitura", ctx.reviewFocus));
85
- lines.push(...section("Não carregar", ctx.exclude));
86
- lines.push(
87
- ...section(
88
- "Docs de contexto",
89
- ctx.contextFiles.map((doc) => `\`${doc}\``),
90
- ),
91
- );
92
-
93
- if (withRules) {
94
- lines.push(
95
- ...ruleSection(
96
- "Regras",
97
- [...def.rules].sort(
98
- (a, b) => severityRank(a.severity) - severityRank(b.severity),
99
- ),
100
- ),
101
- );
102
- }
103
-
104
- return lines.join("\n").trimEnd();
27
+ const definition = findWatchdog(config, id);
28
+ if (!definition) return null;
29
+ return presenter.render(getWatchdogContext(config, definition.id), definition, {
30
+ withRules: opts.withRules ?? false,
31
+ json: opts.json ?? false,
32
+ });
105
33
  }
@@ -6,22 +6,14 @@
6
6
  * (camada `houndsman/`) — único ponto de troca ao portar para outro repositório.
7
7
  */
8
8
 
9
- import { ARTIFACT_DIR } from "./paths";
9
+ import { ProjectConfig } from "./project-config";
10
10
  import type { WatchdogDefinition } from "./types";
11
11
 
12
- /** Documentos sempre incluídos nos briefs (default). */
13
- export const DEFAULT_ALWAYS_INCLUDE_DOCS: readonly string[] = ["AGENTS.md"];
14
-
15
- /**
16
- * Teto default de arquivos por brief. Um PR grande (milhares de linhas) pode
17
- * concentrar centenas de arquivos em um único domínio; o teto mantém o contexto
18
- * do subagente limitado — o excedente vira "fora do orçamento" (cobertura
19
- * honesta), nunca some silenciosamente.
20
- */
21
- export const DEFAULT_MAX_FILES_PER_BRIEF = 40;
22
-
23
- /** Concorrência default das chamadas simultâneas ao Jev. */
24
- export const DEFAULT_JEV_CONCURRENCY = 5;
12
+ export {
13
+ DEFAULT_ALWAYS_INCLUDE_DOCS,
14
+ DEFAULT_JEV_CONCURRENCY,
15
+ DEFAULT_MAX_FILES_PER_BRIEF,
16
+ } from "./defaults";
25
17
 
26
18
  /** Limites de triage para PRs grandes. */
27
19
  export type HoundsmanBudgets = {
@@ -35,6 +27,20 @@ export type HoundsmanConcurrency = {
35
27
  jev?: number;
36
28
  };
37
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
+
38
44
  /**
39
45
  * Configuração completa do motor de revisão para um projeto.
40
46
  *
@@ -43,8 +49,8 @@ export type HoundsmanConcurrency = {
43
49
  export type HoundsmanConfig<TId extends string = string> = {
44
50
  /** Nome legível do projeto (usado em prompts e docs). */
45
51
  projectName: string;
46
- /** Diretório dos artefatos (default: `ARTIFACT_DIR` da camada). */
47
- artifactDir?: string;
52
+ /** Persistência das runs e artefatos (default: SQLite global). */
53
+ storage?: HoundsmanStorage;
48
54
  /**
49
55
  * Ids dos watchdogs — **derivados** das chaves de `watchdogs` por
50
56
  * `defineHoundsmanConfig` (não informar manualmente).
@@ -80,22 +86,15 @@ export function defineHoundsmanConfig<TId extends string>(
80
86
  return { ...input, watchdogIds: Object.keys(input.watchdogs) as TId[] };
81
87
  }
82
88
 
83
- /** Diretório dos artefatos — o valor do projeto ou o default da camada. */
84
- export function resolveArtifactDir(config: HoundsmanConfig): string {
85
- return config.artifactDir ?? ARTIFACT_DIR;
86
- }
87
-
88
89
  /**
89
90
  * Teto de arquivos por brief. Valor ausente/não-positivo cai no default —
90
91
  * mantém o contexto do subagente limitado em PRs grandes.
91
92
  */
92
93
  export function resolveMaxFilesPerBrief(config: HoundsmanConfig): number {
93
- const value = config.budgets?.maxFilesPerBrief;
94
- return value !== undefined && value > 0 ? value : DEFAULT_MAX_FILES_PER_BRIEF;
94
+ return new ProjectConfig(config).maxFilesPerBrief;
95
95
  }
96
96
 
97
97
  /** Concorrência das chamadas Jev (valor ausente/não-positivo cai no default). */
98
98
  export function resolveJevConcurrency(config: HoundsmanConfig): number {
99
- const value = config.concurrency?.jev;
100
- return value !== undefined && value > 0 ? value : DEFAULT_JEV_CONCURRENCY;
99
+ return new ProjectConfig(config).jevConcurrency;
101
100
  }