@payconductor/houndsman 0.0.0-stage → 0.1.1

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 (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -3
  3. package/assets/houndsman.png +0 -0
  4. package/assets/logo-legacy.png +0 -0
  5. package/assets/logo.png +0 -0
  6. package/package.json +64 -5
  7. package/src/cli/index.ts +290 -0
  8. package/src/core/assets.ts +28 -0
  9. package/src/core/brief.ts +185 -0
  10. package/src/core/commands/context.ts +39 -0
  11. package/src/core/commands/findings.ts +390 -0
  12. package/src/core/commands/review.ts +50 -0
  13. package/src/core/commands/scaffold.ts +393 -0
  14. package/src/core/commands/setup.ts +75 -0
  15. package/src/core/commands/test.ts +20 -0
  16. package/src/core/commands/update.ts +30 -0
  17. package/src/core/commands/watchdog.ts +105 -0
  18. package/src/core/config-loader.ts +87 -0
  19. package/src/core/config.ts +101 -0
  20. package/src/core/diff-source.ts +140 -0
  21. package/src/core/doc-discoverer.ts +33 -0
  22. package/src/core/file-classifier.ts +295 -0
  23. package/src/core/finding-anchor.ts +147 -0
  24. package/src/core/findings-state.ts +538 -0
  25. package/src/core/jev/index.ts +12 -0
  26. package/src/core/jev/jev-candidates.ts +105 -0
  27. package/src/core/jev/jev-client.ts +160 -0
  28. package/src/core/jev/jev-detector.ts +205 -0
  29. package/src/core/jev/jev-questions.ts +92 -0
  30. package/src/core/jev/jev-types.ts +91 -0
  31. package/src/core/orchestrator.ts +596 -0
  32. package/src/core/paths.ts +33 -0
  33. package/src/core/pr-context.ts +271 -0
  34. package/src/core/registry-check.ts +160 -0
  35. package/src/core/report-builder.ts +461 -0
  36. package/src/core/types.ts +147 -0
  37. package/src/core/watchdog-registry.ts +43 -0
  38. package/src/index.ts +132 -0
  39. package/src/server/artifacts.ts +66 -0
  40. package/src/server/client/App.tsx +158 -0
  41. package/src/server/client/ArtifactsPanel.tsx +48 -0
  42. package/src/server/client/Filters.tsx +83 -0
  43. package/src/server/client/FindingCard.tsx +122 -0
  44. package/src/server/client/FindingsList.tsx +54 -0
  45. package/src/server/client/RunList.tsx +43 -0
  46. package/src/server/client/SummaryBar.tsx +21 -0
  47. package/src/server/client/api.ts +37 -0
  48. package/src/server/client/constants.ts +63 -0
  49. package/src/server/client/index.tsx +12 -0
  50. package/src/server/client/types.ts +48 -0
  51. package/src/server/client-build.ts +47 -0
  52. package/src/server/index.ts +365 -0
  53. package/src/skill/AUDIT-PROTOCOL.md +96 -0
  54. package/src/skill/OUTPUT-FORMAT.md +70 -0
  55. package/src/skill/README.md +124 -0
  56. package/src/skill/SETUP.md +101 -0
  57. package/src/skill/example-review.md +58 -0
  58. package/src/skill/houndsman-update.md +251 -0
  59. package/src/skill/houndsman.md +316 -0
  60. package/src/tests/findings-anchor.test.ts +120 -0
  61. package/src/tests/findings-commands.test.ts +226 -0
  62. package/src/tests/findings-state.test.ts +274 -0
  63. package/src/tests/fixtures/fake-project.ts +64 -0
  64. package/src/tests/review-scripts.test.ts +1150 -0
  65. package/src/tests/server.test.ts +212 -0
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Scaffold da camada de projeto (`.houndsman/`) — templates e helpers.
3
+ *
4
+ * Separa o que é **auto-gerado** (sincronizável com `houndsman update`) do que é
5
+ * **do usuário**:
6
+ *
7
+ * .houndsman/
8
+ * ├── .gitignore AUTO — ignora core/ e .runs/ (gerados/temporários)
9
+ * ├── core/types.ts AUTO — snapshot dos tipos + helpers
10
+ * ├── core/skill/*.md AUTO — docs do skill (com {{projectName}} resolvido)
11
+ * ├── watchdogs/*.ts USUÁRIO
12
+ * ├── doc-mappings.ts USUÁRIO
13
+ * └── exclusions.ts USUÁRIO
14
+ */
15
+
16
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { basename, join } from "node:path";
18
+ import { SKILL_DIR } from "../assets";
19
+ import {
20
+ ARTIFACT_DIR,
21
+ CORE_DIR,
22
+ CORE_SKILL_DIR,
23
+ OUTPUT_FORMAT_PATH,
24
+ PROJECT_DIR,
25
+ } from "../paths";
26
+
27
+ export type Harness = "agents" | "claude";
28
+
29
+ export type ScaffoldContext = {
30
+ cwd: string;
31
+ projectName: string;
32
+ harnesses: Harness[];
33
+ /** Sobrescreve também os arquivos do usuário (só o `setup --force`). */
34
+ force: boolean;
35
+ /** Arquivos do usuário criados/sobrescritos. */
36
+ created: string[];
37
+ /** Arquivos auto-gerados (re)escritos. */
38
+ generated: string[];
39
+ /** Arquivos do usuário mantidos (já existiam). */
40
+ skipped: string[];
41
+ };
42
+
43
+ function readFlag(args: string[], flag: string): string | undefined {
44
+ const index = args.indexOf(flag);
45
+ return index >= 0 ? args[index + 1] : undefined;
46
+ }
47
+
48
+ /** Resolve os harnesses a configurar a partir de `--harness`. */
49
+ export function resolveHarnesses(args: string[]): Harness[] {
50
+ const raw = readFlag(args, "--harness");
51
+ if (!raw || raw === "both") return ["agents", "claude"];
52
+ if (raw === "none") return [];
53
+ return raw
54
+ .split(",")
55
+ .map((value) => value.trim())
56
+ .filter((value): value is Harness => value === "agents" || value === "claude");
57
+ }
58
+
59
+ /** Monta o contexto do scaffold (nome do projeto, harnesses, flags). */
60
+ export function createScaffoldContext(
61
+ cwd: string,
62
+ args: string[],
63
+ force: boolean,
64
+ ): ScaffoldContext {
65
+ return {
66
+ cwd,
67
+ projectName: readFlag(args, "--name") ?? basename(cwd),
68
+ harnesses: resolveHarnesses(args),
69
+ force,
70
+ created: [],
71
+ generated: [],
72
+ skipped: [],
73
+ };
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Templates
78
+ // ---------------------------------------------------------------------------
79
+
80
+ const CONFIG_TEMPLATE = (
81
+ projectName: string,
82
+ ): string => `import { DOC_MAPPINGS } from "./${PROJECT_DIR}/doc-mappings";
83
+ import { EXCLUSION_PATTERNS } from "./${PROJECT_DIR}/exclusions";
84
+ import { WATCHDOG_DEFINITIONS } from "./${PROJECT_DIR}/watchdogs";
85
+ import type { HoundsmanConfig } from "./${CORE_DIR}/types";
86
+
87
+ /**
88
+ * Configuração do houndsman para ${projectName}.
89
+ *
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.
93
+ */
94
+ export const houndsmanConfig = {
95
+ projectName: "${projectName}",
96
+ watchdogs: WATCHDOG_DEFINITIONS,
97
+ exclusions: EXCLUSION_PATTERNS,
98
+ docMappings: DOC_MAPPINGS,
99
+ } satisfies HoundsmanConfig;
100
+
101
+ export default houndsmanConfig;
102
+ `;
103
+
104
+ const TYPES_TEMPLATE = `/**
105
+ * Tipos e helpers locais do houndsman (GERADO — não edite).
106
+ *
107
+ * Snapshot do contrato público, para tipar a camada de projeto sem depender de um
108
+ * install local do pacote. Sincronize com \`houndsman update\`.
109
+ */
110
+
111
+ export type Severity = "blocking" | "attention" | "suggestion" | "nit";
112
+
113
+ /** Tipo de pergunta aceito pela API Jev. */
114
+ export type JevQuestionType = "noul" | "choice" | "score";
115
+
116
+ /**
117
+ * Spec de pré-análise Jev (opcional) — sempre um objeto; \`kind\` é obrigatório.
118
+ * \`criteria\`: objeto para \`choice\`, lista para \`score\`.
119
+ */
120
+ export type JevRuleSpec = {
121
+ kind: JevQuestionType;
122
+ instructions?: string;
123
+ criteria?: string[] | Record<string, string>;
124
+ };
125
+
126
+ /** Regra do watchdog: severidade + texto — o texto declara a polaridade
127
+ * (\`Promover:\` = deve acontecer; \`Evitar:\`/\`Não\`/\`Nunca\` = não deve acontecer). */
128
+ export type Rule = {
129
+ severity: Severity;
130
+ text: string;
131
+ jev?: JevRuleSpec;
132
+ };
133
+
134
+ /** Como o watchdog casa um arquivo alterado. \`value\` é sempre regex. */
135
+ export type ClassifyRule = {
136
+ type?: "filename" | "content";
137
+ value: string;
138
+ };
139
+
140
+ export type WatchdogMeta = {
141
+ name: string;
142
+ scope: string;
143
+ classify?: readonly ClassifyRule[];
144
+ rules: readonly Rule[];
145
+ contextFiles: string[];
146
+ };
147
+
148
+ /** Definição completa de um watchdog — 1 por arquivo em \`${PROJECT_DIR}/watchdogs/\`. */
149
+ export type WatchdogDefinition = WatchdogMeta & {
150
+ id: string;
151
+ description: string;
152
+ breakingChange?: boolean;
153
+ reviewFocus?: readonly string[];
154
+ exclude?: readonly string[];
155
+ };
156
+
157
+ export type FileStatus =
158
+ | "added"
159
+ | "modified"
160
+ | "deleted"
161
+ | "renamed"
162
+ | "copied"
163
+ | "unknown";
164
+
165
+ export type FileStatusMap = Record<string, { status: FileStatus; oldPath?: string }>;
166
+
167
+ /** Linhas adicionadas/removidas de um arquivo no diff (churn). */
168
+ export type FileChurn = { additions: number; deletions: number };
169
+
170
+ /** Churn por caminho de arquivo. */
171
+ export type ChurnMap = Record<string, FileChurn>;
172
+
173
+ /** Limites de triage para PRs grandes. */
174
+ export type HoundsmanBudgets = {
175
+ /** Máx. de arquivos por brief (default: 40). Excedente vai para "fora do orçamento". */
176
+ maxFilesPerBrief?: number;
177
+ };
178
+
179
+ /** Concorrência das fases paralelas internas. */
180
+ export type HoundsmanConcurrency = {
181
+ /** Chamadas Jev simultâneas (default: 5). */
182
+ jev?: number;
183
+ };
184
+
185
+ /** Contrato de configuração do motor. */
186
+ export type HoundsmanConfig<TId extends string = string> = {
187
+ projectName: string;
188
+ artifactDir?: string;
189
+ watchdogIds?: readonly TId[];
190
+ watchdogs: Record<TId, WatchdogDefinition>;
191
+ exclusions: readonly RegExp[];
192
+ docMappings: readonly (readonly [RegExp, readonly string[]])[];
193
+ alwaysIncludeDocs?: readonly string[];
194
+ budgets?: HoundsmanBudgets;
195
+ concurrency?: HoundsmanConcurrency;
196
+ };
197
+
198
+ /** Helper de autoria (identidade) — apenas tipagem. */
199
+ export function defineWatchdog(definition: WatchdogDefinition): WatchdogDefinition {
200
+ return definition;
201
+ }
202
+
203
+ /** Helper de autoria — deriva \`watchdogIds\` das chaves de \`watchdogs\`. */
204
+ export function defineHoundsmanConfig<TId extends string>(
205
+ input: Omit<HoundsmanConfig<TId>, "watchdogIds">,
206
+ ): HoundsmanConfig<TId> {
207
+ return { ...input, watchdogIds: Object.keys(input.watchdogs) as TId[] };
208
+ }
209
+ `;
210
+
211
+ const WATCHDOG_EXAMPLE = `import { defineWatchdog } from "../core/types";
212
+
213
+ /**
214
+ * Watchdog de exemplo — substitua pelas regras reais do projeto.
215
+ * Um watchdog declara seus arquivos via \`classify\` (regex de path ou conteúdo).
216
+ * O texto de cada regra começa com a polaridade: \`Promover:\` (DEVE acontecer)
217
+ * ou \`Evitar:\`/\`Não\`/\`Nunca\` (NÃO DEVE acontecer).
218
+ */
219
+ export const exampleWatchdog = defineWatchdog({
220
+ id: "example",
221
+ name: "Exemplo",
222
+ description: "Watchdog de exemplo — descreva o propósito em uma frase.",
223
+ scope: "Todo o repositório (ajuste o escopo)",
224
+ classify: [{ value: "^src/" }],
225
+ rules: [
226
+ {
227
+ severity: "attention",
228
+ text: "Promover: descreva aqui um padrão do projeto (algo que DEVE acontecer).",
229
+ jev: { kind: "noul" },
230
+ },
231
+ {
232
+ severity: "nit",
233
+ text: "Evitar: descreva aqui um anti-padrão (algo que NÃO DEVE acontecer).",
234
+ },
235
+ ],
236
+ contextFiles: ["AGENTS.md"],
237
+ });
238
+ `;
239
+
240
+ const WATCHDOGS_INDEX = `import { exampleWatchdog } from "./example";
241
+
242
+ /**
243
+ * Definições dos watchdogs — as chaves definem o union \`WatchdogId\`.
244
+ * Adicionar um watchdog = criar \`<id>.ts\` e registrá-lo aqui.
245
+ */
246
+ export const WATCHDOG_DEFINITIONS = {
247
+ example: exampleWatchdog,
248
+ };
249
+
250
+ export type WatchdogId = keyof typeof WATCHDOG_DEFINITIONS;
251
+ `;
252
+
253
+ const EXCLUSIONS_TEMPLATE = `/** Arquivos que nunca entram na revisão — gerados, locks, builds. */
254
+ export const EXCLUSION_PATTERNS: readonly RegExp[] = [
255
+ /bun\\.lockb?$/,
256
+ /^package-lock\\.json$/,
257
+ /^dist\\//,
258
+ /^build\\//,
259
+ ];
260
+
261
+ /** \`true\` se o caminho é excluído da revisão. */
262
+ export function isExcluded(filePath: string): boolean {
263
+ return EXCLUSION_PATTERNS.some((pattern) => pattern.test(filePath));
264
+ }
265
+ `;
266
+
267
+ const DOC_MAPPINGS_TEMPLATE = `/**
268
+ * Mapeamento de padrões nos paths → documentação relevante.
269
+ * Consumido pelo motor via \`HoundsmanConfig.docMappings\`.
270
+ */
271
+ export const DOC_MAPPINGS: readonly (readonly [RegExp, readonly string[]])[] = [
272
+ [/^src\\//, ["AGENTS.md"]],
273
+ ];
274
+ `;
275
+
276
+ const PROJECT_GITIGNORE = `# Diretórios gerados pelo houndsman — \`core/\` é regenerado por \`houndsman update\`
277
+ # e \`.runs/\` guarda os artefatos de revisão.
278
+ core/
279
+ .runs/
280
+ `;
281
+
282
+ const SKILL_POINTER = (
283
+ name: string,
284
+ projectName: string,
285
+ canonical: string,
286
+ description: string,
287
+ argumentHint: string,
288
+ ): string => `---
289
+ name: ${name}
290
+ description: '${description}'
291
+ argument-hint: '${argumentHint}'
292
+ ---
293
+
294
+ # ${name} — ${projectName}
295
+
296
+ > **Ponteiro.** O conteúdo canônico está em \`${canonical}\`.
297
+
298
+ Leia e siga \`${canonical}\`.
299
+ `;
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // Escrita
303
+ // ---------------------------------------------------------------------------
304
+
305
+ /** Escreve um arquivo do usuário somente se ausente (ou se `force`). */
306
+ function writeUserFile(
307
+ ctx: ScaffoldContext,
308
+ relativePath: string,
309
+ content: string,
310
+ ): void {
311
+ const full = join(ctx.cwd, relativePath);
312
+ if (existsSync(full) && !ctx.force) {
313
+ ctx.skipped.push(relativePath);
314
+ return;
315
+ }
316
+ mkdirSync(join(full, ".."), { recursive: true });
317
+ writeFileSync(full, content);
318
+ ctx.created.push(relativePath);
319
+ }
320
+
321
+ /** Escreve/sobrescreve um arquivo auto-gerado. */
322
+ function writeGeneratedFile(
323
+ ctx: ScaffoldContext,
324
+ relativePath: string,
325
+ content: string,
326
+ ): void {
327
+ const full = join(ctx.cwd, relativePath);
328
+ mkdirSync(join(full, ".."), { recursive: true });
329
+ writeFileSync(full, content);
330
+ ctx.generated.push(relativePath);
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Camadas
335
+ // ---------------------------------------------------------------------------
336
+
337
+ /** Arquivos do usuário: config, watchdogs, exclusões e mapeamentos. */
338
+ export function writeUserLayer(ctx: ScaffoldContext): void {
339
+ writeUserFile(ctx, "houndsman.config.ts", CONFIG_TEMPLATE(ctx.projectName));
340
+ writeUserFile(ctx, join(PROJECT_DIR, "watchdogs", "example.ts"), WATCHDOG_EXAMPLE);
341
+ writeUserFile(ctx, join(PROJECT_DIR, "watchdogs", "index.ts"), WATCHDOGS_INDEX);
342
+ writeUserFile(ctx, join(PROJECT_DIR, "exclusions.ts"), EXCLUSIONS_TEMPLATE);
343
+ writeUserFile(ctx, join(PROJECT_DIR, "doc-mappings.ts"), DOC_MAPPINGS_TEMPLATE);
344
+ }
345
+
346
+ /**
347
+ * Arquivos auto-gerados: tipos, docs do skill, `.gitignore` (ignora `core/` e
348
+ * `.runs/`) e os ponteiros de skill dos harnesses. Sempre sobrescritos (é o
349
+ * alvo do `update`).
350
+ */
351
+ export function syncGeneratedFiles(ctx: ScaffoldContext): void {
352
+ writeGeneratedFile(ctx, join(CORE_DIR, "types.ts"), TYPES_TEMPLATE);
353
+ writeGeneratedFile(ctx, join(PROJECT_DIR, ".gitignore"), PROJECT_GITIGNORE);
354
+
355
+ for (const name of readdirSync(SKILL_DIR)) {
356
+ if (!name.endsWith(".md")) continue;
357
+ const raw = readFileSync(join(SKILL_DIR, name), "utf8");
358
+ writeGeneratedFile(
359
+ ctx,
360
+ join(CORE_SKILL_DIR, name),
361
+ raw.replaceAll("{{projectName}}", ctx.projectName),
362
+ );
363
+ }
364
+
365
+ for (const harness of ctx.harnesses) writeHarnessPointers(ctx, harness);
366
+ }
367
+
368
+ /** Escreve os ponteiros de skill de um harness. */
369
+ function writeHarnessPointers(ctx: ScaffoldContext, harness: Harness): void {
370
+ const pointerDir = `.${harness}/skills`;
371
+ writeGeneratedFile(
372
+ ctx,
373
+ join(pointerDir, "houndsman", "SKILL.md"),
374
+ SKILL_POINTER(
375
+ "houndsman",
376
+ ctx.projectName,
377
+ `${CORE_SKILL_DIR}/houndsman.md`,
378
+ "Revisa Pull Requests com watchdogs especializados por domínio. Orquestra `bun run houndsman context` → `bun run houndsman` → subagentes → `bun run houndsman --finalize`. Config em `houndsman.config.ts`; regras em `.houndsman/`; relatório em `.houndsman/.runs/<branch-slug>/review.md`.",
379
+ "Nome da branch (opcional, detecta automaticamente se omitido)",
380
+ ),
381
+ );
382
+ writeGeneratedFile(
383
+ ctx,
384
+ join(pointerDir, "houndsman-update", "SKILL.md"),
385
+ SKILL_POINTER(
386
+ "houndsman-update",
387
+ ctx.projectName,
388
+ `${CORE_SKILL_DIR}/houndsman-update.md`,
389
+ "Atualiza as regras e protocolos do skill houndsman (watchdogs, exclusões, mapeamentos de docs).",
390
+ "Watchdog alvo (opcional)",
391
+ ),
392
+ );
393
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Subcomando `setup` — cria a configuração inicial do houndsman no projeto.
3
+ *
4
+ * Gera a camada `.houndsman/`: arquivos do usuário (config, watchdogs, exclusões,
5
+ * mapeamentos) + arquivos auto-gerados (tipos, skill, `.houndsman/.gitignore`,
6
+ * ponteiros de harness). O `houndsman update` sincroniza só os auto-gerados.
7
+ *
8
+ * A camada é **autocontida** — não importa o pacote `@payconductor/houndsman` em
9
+ * runtime, o que permite instalar o CLI **globalmente** e usar `bun run houndsman`
10
+ * sem dependência local no projeto.
11
+ *
12
+ * Idempotente: arquivos do usuário só são sobrescritos com `--force`.
13
+ */
14
+
15
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import {
18
+ createScaffoldContext,
19
+ type ScaffoldContext,
20
+ syncGeneratedFiles,
21
+ writeUserLayer,
22
+ } from "./scaffold";
23
+
24
+ export type SetupOptions = {
25
+ cwd: string;
26
+ args: string[];
27
+ };
28
+
29
+ /**
30
+ * Garante o script `houndsman` no `package.json` do projeto (para
31
+ * `bun run houndsman [...]`), sem reformatar o restante do arquivo.
32
+ */
33
+ function ensurePackageScript(ctx: ScaffoldContext): void {
34
+ const pkgPath = join(ctx.cwd, "package.json");
35
+ if (!existsSync(pkgPath)) return;
36
+
37
+ const raw = readFileSync(pkgPath, "utf8");
38
+ if (raw.includes('"houndsman"')) {
39
+ ctx.skipped.push("package.json#scripts.houndsman");
40
+ return;
41
+ }
42
+ const scriptsBlock = /"scripts"\s*:\s*\{/;
43
+ if (!scriptsBlock.test(raw)) return;
44
+
45
+ writeFileSync(
46
+ pkgPath,
47
+ raw.replace(scriptsBlock, (match) => `${match}\n\t\t"houndsman": "houndsman",`),
48
+ );
49
+ ctx.created.push("package.json#scripts.houndsman");
50
+ }
51
+
52
+ /** Cria a configuração inicial do projeto. */
53
+ export async function runSetupCommand(options: SetupOptions): Promise<void> {
54
+ const ctx = createScaffoldContext(
55
+ options.cwd,
56
+ options.args,
57
+ options.args.includes("--force"),
58
+ );
59
+
60
+ console.log(`→ Configurando houndsman em ${ctx.cwd} (projeto: ${ctx.projectName})`);
61
+
62
+ writeUserLayer(ctx);
63
+ syncGeneratedFiles(ctx);
64
+ ensurePackageScript(ctx);
65
+
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)`);
69
+
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");
75
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Subcomando `test` — roda a suíte de testes da lib (project-agnóstica).
3
+ *
4
+ * A suíte vive em `src/tests/` e usa um "project fake"; o adotante
5
+ * da lib não precisa escrever testes.
6
+ */
7
+
8
+ import { join } from "node:path";
9
+
10
+ /** Diretório da suíte de testes da lib (independente do projeto). */
11
+ export const LIB_TESTS_DIR = join(import.meta.dir, "..", "..", "tests");
12
+
13
+ /** Roda a suíte de testes da lib. Devolve o exit code. */
14
+ export function runLibTests(): number {
15
+ const result = Bun.spawnSync(["bun", "test", LIB_TESTS_DIR], {
16
+ stdout: "inherit",
17
+ stderr: "inherit",
18
+ });
19
+ return result.exitCode ?? 1;
20
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Subcomando `update` — sincroniza os arquivos **auto-gerados** da camada `.houndsman/`.
3
+ *
4
+ * Reescreve `core/types.ts`, `core/skill/*.md`, `.houndsman/.gitignore` e os
5
+ * ponteiros de skill dos harnesses. Nunca toca nos arquivos do usuário
6
+ * (`watchdogs/`, `doc-mappings.ts`, `exclusions.ts`, `houndsman.config.ts`).
7
+ */
8
+
9
+ import { createScaffoldContext, syncGeneratedFiles } from "./scaffold";
10
+
11
+ export type UpdateOptions = {
12
+ cwd: string;
13
+ args: string[];
14
+ };
15
+
16
+ /** Sincroniza os arquivos auto-gerados da camada de projeto. */
17
+ export async function runUpdateCommand(options: UpdateOptions): Promise<void> {
18
+ const ctx = createScaffoldContext(options.cwd, options.args, false);
19
+
20
+ console.log(`→ Sincronizando arquivos auto-gerados em ${ctx.cwd}`);
21
+
22
+ syncGeneratedFiles(ctx);
23
+
24
+ for (const path of ctx.generated) console.log(` ~ ${path} (auto)`);
25
+
26
+ console.log("\nArquivos do usuário não foram tocados:");
27
+ console.log(
28
+ " .houndsman/watchdogs/ · exclusions.ts · doc-mappings.ts · houndsman.config.ts",
29
+ );
30
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Subcomando `watchdog` — contexto de um watchdog (biblioteca).
3
+ *
4
+ * Devolve o texto pronto (markdown ou JSON); `null` se o id não existe.
5
+ * O CLI decide o log/exit.
6
+ */
7
+
8
+ import type { HoundsmanConfig } from "../config";
9
+ import {
10
+ type Rule,
11
+ SEVERITY_EMOJI,
12
+ severityRank,
13
+ type WatchdogDefinition,
14
+ } from "../types";
15
+ import { findWatchdog, getWatchdogContext } from "../watchdog-registry";
16
+
17
+ export type WatchdogContextOptions = {
18
+ /** Inclui `criticalRules` + `antiPatterns`. */
19
+ withRules?: boolean;
20
+ /** Saída JSON (default: markdown legível). */
21
+ json?: boolean;
22
+ };
23
+
24
+ type WatchdogContext = ReturnType<typeof getWatchdogContext>;
25
+
26
+ /** Renderiza o contexto de um watchdog (markdown ou JSON). `null` se o id não existe. */
27
+ export function renderWatchdogContext(
28
+ config: HoundsmanConfig,
29
+ id: string,
30
+ opts: WatchdogContextOptions = {},
31
+ ): 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();
105
+ }