@payconductor/houndsman 0.0.0-stage → 0.1.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -3
  3. package/assets/houndsman.png +0 -0
  4. package/package.json +63 -5
  5. package/src/cli/index.ts +210 -0
  6. package/src/core/assets.ts +20 -0
  7. package/src/core/brief.ts +162 -0
  8. package/src/core/commands/context.ts +39 -0
  9. package/src/core/commands/review.ts +50 -0
  10. package/src/core/commands/scaffold.ts +373 -0
  11. package/src/core/commands/setup.ts +75 -0
  12. package/src/core/commands/test.ts +20 -0
  13. package/src/core/commands/update.ts +30 -0
  14. package/src/core/commands/watchdog.ts +105 -0
  15. package/src/core/config-loader.ts +87 -0
  16. package/src/core/config.ts +59 -0
  17. package/src/core/doc-discoverer.ts +33 -0
  18. package/src/core/file-classifier.ts +247 -0
  19. package/src/core/jev/index.ts +12 -0
  20. package/src/core/jev/jev-candidates.ts +105 -0
  21. package/src/core/jev/jev-client.ts +99 -0
  22. package/src/core/jev/jev-detector.ts +203 -0
  23. package/src/core/jev/jev-questions.ts +92 -0
  24. package/src/core/jev/jev-types.ts +91 -0
  25. package/src/core/orchestrator.ts +444 -0
  26. package/src/core/paths.ts +30 -0
  27. package/src/core/pr-context.ts +271 -0
  28. package/src/core/registry-check.ts +145 -0
  29. package/src/core/report-builder.ts +357 -0
  30. package/src/core/types.ts +137 -0
  31. package/src/core/watchdog-registry.ts +43 -0
  32. package/src/index.ts +56 -0
  33. package/src/skill/AUDIT-PROTOCOL.md +96 -0
  34. package/src/skill/OUTPUT-FORMAT.md +70 -0
  35. package/src/skill/README.md +116 -0
  36. package/src/skill/SETUP.md +99 -0
  37. package/src/skill/example-review.md +58 -0
  38. package/src/skill/houndsman-update.md +251 -0
  39. package/src/skill/houndsman.md +239 -0
  40. package/src/tests/fixtures/fake-project.ts +64 -0
  41. package/src/tests/review-scripts.test.ts +851 -0
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Consolidação dos relatórios JSON dos watchdogs em um markdown (biblioteca).
3
+ *
4
+ * Entrypoint: `cli/review.ts` (via `core/orchestrator.ts`). Deduplica achados
5
+ * por (file, line, rule), agrupa por severidade e renderiza a tabela de cobertura.
6
+ *
7
+ * Schema dos watchdogs: asset `OUTPUT-FORMAT.md` (embarcado em `src/skill/`).
8
+ */
9
+
10
+ import type { JevCandidate } from "./jev";
11
+ import { SEVERITY_EMOJI, SEVERITY_ORDER, type Severity, severityRank } from "./types";
12
+
13
+ /** Resolve o nome legível de um watchdog (default: identidade). */
14
+ export type WatchdogNameResolver = (watchdogId: string) => string;
15
+
16
+ const identityName: WatchdogNameResolver = (id) => id;
17
+
18
+ export type Finding = {
19
+ rule: string;
20
+ severity: Severity;
21
+ file: string;
22
+ line?: number;
23
+ message: string;
24
+ action: string;
25
+ evidence?: string;
26
+ };
27
+
28
+ export type WatchdogReport = {
29
+ watchdogId: string;
30
+ reviewedFiles: string[];
31
+ skipped: string[];
32
+ findings: Finding[];
33
+ };
34
+
35
+ /** Achado deduplicado, com todos os watchdogs que o apontaram. */
36
+ export type MergedFinding = Finding & {
37
+ watchdogs: string[];
38
+ };
39
+
40
+ /** Resumo do contexto da PR (objetivo/motivação/resumo das alterações), gravado pelo subagente. */
41
+ export type ContextSummary = {
42
+ available: boolean;
43
+ source?: {
44
+ number?: number;
45
+ title?: string;
46
+ url?: string;
47
+ base?: string;
48
+ head?: string;
49
+ };
50
+ objective?: string;
51
+ motivation?: string;
52
+ changeSummary?: string[];
53
+ };
54
+
55
+ const VALID_SEVERITIES = new Set<string>(SEVERITY_ORDER);
56
+
57
+ /**
58
+ * Limites do resumo da PR: mantém o relatório e os briefs enxutos — evita
59
+ * confundir o watchdog humano e estourar o contexto da IA watchdog.
60
+ */
61
+ const CONTEXT_LIMITS = {
62
+ objective: 200,
63
+ motivation: 240,
64
+ changeSummaryItem: 160,
65
+ changeSummaryItems: 4,
66
+ } as const;
67
+
68
+ /** Corta `value` em `max` caracteres, sinalizando com reticências. */
69
+ function truncate(value: string, max: number): string {
70
+ return value.length <= max ? value : `${value.slice(0, max - 1).trimEnd()}…`;
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Helpers exportados (testáveis)
75
+ // ---------------------------------------------------------------------------
76
+
77
+ /** `feature/add-webhook` → `feature-add-webhook`. */
78
+ export function sanitizeBranch(branch: string): string {
79
+ return branch
80
+ .trim()
81
+ .replace(/[/\\]+/g, "-")
82
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
83
+ .replace(/^-+|-+$/g, "")
84
+ .toLowerCase();
85
+ }
86
+
87
+ function isFinding(value: unknown): value is Finding {
88
+ if (typeof value !== "object" || value === null) return false;
89
+ const f = value as Record<string, unknown>;
90
+ return (
91
+ typeof f.rule === "string" &&
92
+ typeof f.severity === "string" &&
93
+ VALID_SEVERITIES.has(f.severity) &&
94
+ typeof f.file === "string" &&
95
+ typeof f.message === "string" &&
96
+ typeof f.action === "string" &&
97
+ (f.line === undefined || typeof f.line === "number")
98
+ );
99
+ }
100
+
101
+ /** Valida e normaliza um relatório. Retorna null se inválido. */
102
+ export function parseWatchdogReport(raw: unknown, source: string): WatchdogReport | null {
103
+ if (typeof raw !== "object" || raw === null) {
104
+ console.error(`[report-builder] ${source}: não é um objeto JSON`);
105
+ return null;
106
+ }
107
+ const obj = raw as Record<string, unknown>;
108
+ if (typeof obj.watchdogId !== "string") {
109
+ console.error(`[report-builder] ${source}: campo 'watchdogId' ausente`);
110
+ return null;
111
+ }
112
+ if (!Array.isArray(obj.findings)) {
113
+ console.error(`[report-builder] ${source}: campo 'findings' ausente`);
114
+ return null;
115
+ }
116
+
117
+ const findings = obj.findings.filter((f) => {
118
+ if (!isFinding(f)) {
119
+ console.error(`[report-builder] ${source}: finding inválido ignorado`);
120
+ return false;
121
+ }
122
+ return true;
123
+ });
124
+
125
+ return {
126
+ watchdogId: obj.watchdogId,
127
+ reviewedFiles: Array.isArray(obj.reviewedFiles)
128
+ ? (obj.reviewedFiles as string[])
129
+ : [],
130
+ skipped: Array.isArray(obj.skipped) ? (obj.skipped as string[]) : [],
131
+ findings,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Valida/normaliza o `context-summary.json` do subagente de contexto.
137
+ * Retorna null se indisponível ou sem conteúdo útil — a seção é omitida.
138
+ * Aplica limites de tamanho: o resumo precisa ser curto para não confundir o
139
+ * watchdog humano nem estourar o contexto da IA watchdog (briefs + relatório).
140
+ */
141
+ export function parseContextSummary(raw: unknown): ContextSummary | null {
142
+ if (typeof raw !== "object" || raw === null) return null;
143
+ const obj = raw as Record<string, unknown>;
144
+ if (obj.available !== true) return null;
145
+
146
+ const text = (value: unknown, max = Number.POSITIVE_INFINITY): string | undefined => {
147
+ const trimmed = typeof value === "string" ? value.trim() : "";
148
+ return trimmed.length > 0 ? truncate(trimmed, max) : undefined;
149
+ };
150
+ const objective = text(obj.objective, CONTEXT_LIMITS.objective);
151
+ const motivation = text(obj.motivation, CONTEXT_LIMITS.motivation);
152
+ const changeSummary = (Array.isArray(obj.changeSummary) ? obj.changeSummary : [])
153
+ .map((item) => text(item, CONTEXT_LIMITS.changeSummaryItem))
154
+ .filter((item): item is string => item !== undefined)
155
+ .slice(0, CONTEXT_LIMITS.changeSummaryItems);
156
+
157
+ if (!objective && !motivation && changeSummary.length === 0) return null;
158
+
159
+ const src = (obj.source ?? {}) as Record<string, unknown>;
160
+ const source = {
161
+ number: typeof src.number === "number" ? src.number : undefined,
162
+ title: text(src.title),
163
+ url: text(src.url),
164
+ base: text(src.base),
165
+ head: text(src.head),
166
+ };
167
+
168
+ return { available: true, source, objective, motivation, changeSummary };
169
+ }
170
+
171
+ /** Deduplica achados por (file, line, rule), mesclando atribuição de watchdogs. */
172
+ export function dedupeFindings(reports: WatchdogReport[]): MergedFinding[] {
173
+ const map = new Map<string, MergedFinding>();
174
+ for (const report of reports) {
175
+ for (const finding of report.findings) {
176
+ const key = `${finding.file}|${finding.line ?? ""}|${finding.rule}`;
177
+ const existing = map.get(key);
178
+ if (existing) {
179
+ if (!existing.watchdogs.includes(report.watchdogId)) {
180
+ existing.watchdogs.push(report.watchdogId);
181
+ }
182
+ // Mantém a versão de maior severidade (severidade + detalhes)
183
+ if (severityRank(finding.severity) < severityRank(existing.severity)) {
184
+ existing.severity = finding.severity;
185
+ existing.message = finding.message;
186
+ existing.action = finding.action;
187
+ existing.evidence = finding.evidence ?? existing.evidence;
188
+ }
189
+ continue;
190
+ }
191
+ map.set(key, { ...finding, watchdogs: [report.watchdogId] });
192
+ }
193
+ }
194
+ return [...map.values()].sort((a, b) => {
195
+ const bySeverity = severityRank(a.severity) - severityRank(b.severity);
196
+ if (bySeverity !== 0) return bySeverity;
197
+ if (a.file !== b.file) return a.file.localeCompare(b.file);
198
+ return (a.line ?? 0) - (b.line ?? 0);
199
+ });
200
+ }
201
+
202
+ function watchdogName(nameOf: WatchdogNameResolver, id: string): string {
203
+ return nameOf(id);
204
+ }
205
+
206
+ function renderFinding(finding: MergedFinding, nameOf: WatchdogNameResolver): string[] {
207
+ const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
208
+ const watchdogs = finding.watchdogs.map((r) => watchdogName(nameOf, r)).join(", ");
209
+ const lines = [
210
+ `- **${finding.rule}** — \`${location}\` · _${watchdogs}_`,
211
+ ` ${finding.message}`,
212
+ ` **Ação:** ${finding.action}`,
213
+ ];
214
+ if (finding.evidence) {
215
+ lines.push(` **Evidência:** \`${finding.evidence}\``);
216
+ }
217
+ lines.push("");
218
+ return lines;
219
+ }
220
+
221
+ function renderCoverage(
222
+ reports: WatchdogReport[],
223
+ coverage: { classified?: Record<string, string[]> },
224
+ nameOf: WatchdogNameResolver,
225
+ ): string[] {
226
+ const lines = [
227
+ "## Cobertura",
228
+ "",
229
+ "| Watchdog | Arquivos revisados | Findings |",
230
+ "|---|---|---|",
231
+ ];
232
+ for (const report of reports) {
233
+ const expected = coverage.classified?.[report.watchdogId];
234
+ const reviewed = new Set(report.reviewedFiles);
235
+ const pending = expected ? expected.filter((f) => !reviewed.has(f)) : [];
236
+ const reviewedLabel =
237
+ expected && pending.length > 0
238
+ ? `${report.reviewedFiles.length}/${expected.length} (faltam ${pending.length})`
239
+ : `${report.reviewedFiles.length}`;
240
+ lines.push(
241
+ `| ${watchdogName(nameOf, report.watchdogId)} | ${reviewedLabel} | ${report.findings.length} |`,
242
+ );
243
+ }
244
+ lines.push("");
245
+ return lines;
246
+ }
247
+
248
+ /** Candidatos Jev não confirmados por nenhum watchdog — revisão manual, não bloqueiam. */
249
+ function renderUnconfirmedCandidates(
250
+ candidates: JevCandidate[],
251
+ merged: MergedFinding[],
252
+ ): string[] {
253
+ const confirmed = new Set(merged.map((f) => `${f.file}|${f.rule}`));
254
+ const pending = candidates.filter((c) => !confirmed.has(`${c.file}|${c.rule}`));
255
+ if (pending.length === 0) return [];
256
+ const lines = [
257
+ "## 🔎 Candidatos não verificados (Jev)",
258
+ "",
259
+ "> Indícios automáticos que nenhum watchdog confirmou. Revise manualmente — não bloqueiam.",
260
+ "",
261
+ ];
262
+ for (const c of pending) {
263
+ lines.push(
264
+ `- \`${c.file}\` · **${c.severity}** · confiança ${c.confidence.toFixed(2)} · _${c.watchdogId}_`,
265
+ ` ${c.rule}`,
266
+ "",
267
+ );
268
+ }
269
+ return lines;
270
+ }
271
+
272
+ function renderConclusion(counts: Record<Severity, number>): string[] {
273
+ const status =
274
+ counts.blocking > 0
275
+ ? "❌ Mudanças Necessárias"
276
+ : counts.attention > 0
277
+ ? "⚠️ Aprovado com Ressalvas"
278
+ : "✅ Aprovado";
279
+ return [
280
+ "## Conclusão",
281
+ "",
282
+ `**Status:** ${status}`,
283
+ `**Resumo:** ${counts.blocking} violações bloqueantes, ${counts.attention} pontos de atenção, ${counts.suggestion} sugestões e ${counts.nit} nits.`,
284
+ ];
285
+ }
286
+
287
+ /** Seção de contexto da PR (Objetivo / Motivação / Resumo das alterações). */
288
+ function renderContext(context: ContextSummary): string[] {
289
+ const lines = ["## Contexto da PR", ""];
290
+ const src = context.source;
291
+
292
+ const ref = [src?.number ? `#${src.number}` : "", src?.title ?? ""]
293
+ .filter(Boolean)
294
+ .join(" ");
295
+ if (ref) lines.push(`**PR:** ${ref}${src?.url ? ` — ${src.url}` : ""}`);
296
+ if (src?.head && src?.base)
297
+ lines.push(`**Branch:** \`${src.head}\` → \`${src.base}\``);
298
+ if (context.objective) lines.push(`**Objetivo:** ${context.objective}`);
299
+ if (context.motivation) lines.push(`**Motivação:** ${context.motivation}`);
300
+ if (context.changeSummary && context.changeSummary.length > 0) {
301
+ lines.push("", "**Resumo das alterações:**", "");
302
+ for (const item of context.changeSummary) lines.push(`- ${item}`);
303
+ }
304
+ lines.push("");
305
+ return lines;
306
+ }
307
+
308
+ export function renderReport(
309
+ branch: string,
310
+ reports: WatchdogReport[],
311
+ merged: MergedFinding[],
312
+ coverage: { classified?: Record<string, string[]> },
313
+ context?: ContextSummary | null,
314
+ candidates?: JevCandidate[],
315
+ nameOf: WatchdogNameResolver = identityName,
316
+ ): string {
317
+ const now = new Date().toISOString().slice(0, 10);
318
+ const counts = {
319
+ blocking: merged.filter((f) => f.severity === "blocking").length,
320
+ attention: merged.filter((f) => f.severity === "attention").length,
321
+ suggestion: merged.filter((f) => f.severity === "suggestion").length,
322
+ nit: merged.filter((f) => f.severity === "nit").length,
323
+ };
324
+
325
+ const lines: string[] = [
326
+ `# PR Review — ${branch}`,
327
+ "",
328
+ `**Data:** ${now}`,
329
+ `**Watchdogs ativos:** ${reports.map((r) => r.watchdogId).join(", ") || "—"}`,
330
+ `**Achados:** ${counts.blocking} bloqueantes · ${counts.attention} atenção · ${counts.suggestion} sugestões · ${counts.nit} nits`,
331
+ "",
332
+ ];
333
+
334
+ if (context) lines.push(...renderContext(context));
335
+
336
+ for (const severity of SEVERITY_ORDER) {
337
+ const items = merged.filter((f) => f.severity === severity);
338
+ if (items.length === 0) continue;
339
+ lines.push(`## ${SEVERITY_EMOJI[severity]} ${severity}`, "");
340
+ for (const finding of items) {
341
+ lines.push(...renderFinding(finding, nameOf));
342
+ }
343
+ }
344
+
345
+ if (merged.length === 0) {
346
+ lines.push("Nenhum achado. ✅", "");
347
+ }
348
+
349
+ if (candidates && candidates.length > 0) {
350
+ lines.push(...renderUnconfirmedCandidates(candidates, merged));
351
+ }
352
+
353
+ lines.push(...renderCoverage(reports, coverage, nameOf));
354
+ lines.push(...renderConclusion(counts));
355
+
356
+ return lines.join("\n");
357
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Tipos e constantes genéricas do motor de revisão (houndsman).
3
+ *
4
+ * Sem dependência do projeto: os ids de watchdog são derivados do
5
+ * `HoundsmanConfig` injetado pelo consumidor. `Severity` é a escala de achados;
6
+ * `Rule` e `WatchdogMeta` descrevem as regras consumidas pela renderização.
7
+ */
8
+
9
+ import type { JevRuleSpec } from "./jev/jev-types";
10
+
11
+ /** Severidade de um achado. `blocking` bloqueia o merge. */
12
+ export type Severity = "blocking" | "attention" | "suggestion" | "nit";
13
+
14
+ /** Ordem canônica de severidade (mais grave primeiro). */
15
+ export const SEVERITY_ORDER: readonly Severity[] = [
16
+ "blocking",
17
+ "attention",
18
+ "suggestion",
19
+ "nit",
20
+ ];
21
+
22
+ /** Emoji associado a cada severidade. */
23
+ export const SEVERITY_EMOJI: Record<Severity, string> = {
24
+ blocking: "❌",
25
+ attention: "⚠️",
26
+ suggestion: "💡",
27
+ nit: "🔎",
28
+ };
29
+
30
+ /**
31
+ * Regra do watchdog: severidade + texto. O texto declara a polaridade de forma
32
+ * explícita — `Promover:` (algo que DEVE acontecer) ou `Evitar:`/`Não`/`Nunca`
33
+ * (algo que NÃO DEVE acontecer).
34
+ *
35
+ * `jev` (opcional) é a spec de pré-análise: quando presente, a regra vira uma
36
+ * pergunta tipada enviada ao modelo no Passo 1.5.
37
+ */
38
+ export type Rule = {
39
+ severity: Severity;
40
+ text: string;
41
+ jev?: JevRuleSpec;
42
+ };
43
+
44
+ /**
45
+ * Entrada de classificação: como o watchdog casa um arquivo alterado.
46
+ *
47
+ * `value` é sempre um padrão (regex, estilo grep — fonte sem barras).
48
+ *
49
+ * - `filename` (padrão) → padrão aplicado ao caminho do arquivo.
50
+ * - `content` → padrão aplicado ao conteúdo do arquivo.
51
+ */
52
+ export type ClassifyRule = {
53
+ type?: "filename" | "content";
54
+ value: string;
55
+ };
56
+
57
+ export type WatchdogMeta = {
58
+ /** Nome legível do watchdog */
59
+ name: string;
60
+ /** Escopo de atuação (quais arquivos/pastas revisa) */
61
+ scope: string;
62
+ /** Arquivos que este watchdog cobre (classificação). */
63
+ classify?: readonly ClassifyRule[];
64
+ /** Regras do watchdog (texto com polaridade explícita: `Promover:`/`Evitar:`). */
65
+ rules: readonly Rule[];
66
+ /** Arquivos de documentação de contexto (caminhos relativos à raiz) */
67
+ contextFiles: string[];
68
+ };
69
+
70
+ /** Status de um arquivo alterado (git `--name-status`). */
71
+ export type FileStatus =
72
+ | "added"
73
+ | "modified"
74
+ | "deleted"
75
+ | "renamed"
76
+ | "copied"
77
+ | "unknown";
78
+
79
+ /** Status por arquivo, conforme o `classification.json`. */
80
+ export type FileStatusMap = Record<string, { status: FileStatus; oldPath?: string }>;
81
+
82
+ /** Contexto da PR injetado no brief (estrutural — sem dependência de runtime). */
83
+ export type WatchdogRenderContext = {
84
+ objective?: string;
85
+ motivation?: string;
86
+ changeSummary?: string[];
87
+ };
88
+
89
+ /** Candidato pré-analisado (Jev) exibido no brief. */
90
+ export type WatchdogRenderCandidate = {
91
+ file: string;
92
+ severity: Severity;
93
+ confidence: number;
94
+ rule: string;
95
+ };
96
+
97
+ /** Dados que o renderer do brief recebe (sem dependência de runtime do jev/cli). */
98
+ export type WatchdogRenderArgs = {
99
+ watchdogId: string;
100
+ files: readonly string[];
101
+ statuses: FileStatusMap;
102
+ deleted: readonly string[];
103
+ branch: string;
104
+ outputPath: string;
105
+ outputFormatPath: string;
106
+ docs: readonly string[];
107
+ context?: WatchdogRenderContext | null;
108
+ candidates?: readonly WatchdogRenderCandidate[];
109
+ };
110
+
111
+ /** Definição completa de um watchdog — 1 por arquivo em `project/watchdogs/`. */
112
+ export type WatchdogDefinition = WatchdogMeta & {
113
+ /** Id do watchdog (deve casar com a chave em `WATCHDOG_DEFINITIONS`). */
114
+ id: string;
115
+ /** Uma frase sobre o propósito (ex-`Objetivo` do .md). */
116
+ description: string;
117
+ /** Watchdog deve checar breaking change em arquivos deletados do seu domínio. */
118
+ breakingChange?: boolean;
119
+ /** Prioridade de arquivos/paths (ex-`O Que Revisar`). Opcional. */
120
+ reviewFocus?: readonly string[];
121
+ /** O que NUNCA carregar/enviar (ex-`NÃO carregar`). Opcional. */
122
+ exclude?: readonly string[];
123
+ /** Render customizado do brief; se omitido usa `defaultRenderBrief`.
124
+ * Recebe a própria definição (`def`) para reutilizar `defaultRenderBrief`
125
+ * sem criar dependência de runtime `registry → core`. */
126
+ renderBrief?: (args: WatchdogRenderArgs, def: WatchdogDefinition) => string[];
127
+ };
128
+
129
+ /** Helper de autoria: tipa o objeto do watchdog (id contra `WatchdogId`). */
130
+ export function defineWatchdog(def: WatchdogDefinition): WatchdogDefinition {
131
+ return def;
132
+ }
133
+
134
+ /** Índice numérico da severidade (menor = mais grave). */
135
+ export function severityRank(severity: Severity): number {
136
+ return SEVERITY_ORDER.indexOf(severity);
137
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Acesso tipado aos watchdogs de um `HoundsmanConfig` (biblioteca).
3
+ *
4
+ * Substitui o antigo barrel `registry/`. O motor não conhece ids concretos —
5
+ * tudo chega via `config`.
6
+ */
7
+
8
+ import type { HoundsmanConfig } from "./config";
9
+ import type { WatchdogDefinition } from "./types";
10
+
11
+ /** Acesso tipado a um watchdog do projeto (id string em runtime). */
12
+ export function getWatchdog<TId extends string>(
13
+ config: HoundsmanConfig<TId>,
14
+ watchdogId: string,
15
+ ): WatchdogDefinition {
16
+ return config.watchdogs[watchdogId as TId];
17
+ }
18
+
19
+ /** Busca null-safe por id string (runtime). */
20
+ export function findWatchdog<TId extends string>(
21
+ config: HoundsmanConfig<TId>,
22
+ watchdogId: string,
23
+ ): WatchdogDefinition | null {
24
+ return config.watchdogs[watchdogId as TId] ?? null;
25
+ }
26
+
27
+ /** Contexto "essencial" do watchdog — SEM regras (docs/UI/inspeção). */
28
+ export function getWatchdogContext<TId extends string>(
29
+ config: HoundsmanConfig<TId>,
30
+ watchdogId: string,
31
+ ) {
32
+ const { id, name, description, scope, reviewFocus, exclude, contextFiles } =
33
+ getWatchdog(config, watchdogId);
34
+ return {
35
+ id,
36
+ name,
37
+ description,
38
+ scope,
39
+ reviewFocus,
40
+ exclude,
41
+ contextFiles,
42
+ };
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * API pública do `houndsman`.
3
+ *
4
+ * O consumidor importa daqui para definir a configuração do projeto
5
+ * (`houndsman.config.ts`): watchdogs, tipos e helpers de autoria.
6
+ */
7
+
8
+ export { assetPath, OUTPUT_FORMAT_FILE } from "./core/assets";
9
+ export { defaultRenderBrief, PROCEDURE, renderBrief } from "./core/brief";
10
+ export type { HoundsmanConfig, HoundsmanConfigInput } from "./core/config";
11
+ export { DEFAULT_ALWAYS_INCLUDE_DOCS, defineHoundsmanConfig } from "./core/config";
12
+ export type { LoadedConfig } from "./core/config-loader";
13
+ export {
14
+ CONFIG_FILE_CANDIDATES,
15
+ loadConfig,
16
+ resolveConfigPath,
17
+ } from "./core/config-loader";
18
+ export {
19
+ classifyEntries,
20
+ classifyRange,
21
+ isExcludedPath,
22
+ matchWatchdogs,
23
+ parseEntries,
24
+ } from "./core/file-classifier";
25
+ export {
26
+ ARTIFACT_DIR,
27
+ CORE_DIR,
28
+ CORE_SKILL_DIR,
29
+ OUTPUT_FORMAT_PATH,
30
+ PROJECT_DIR,
31
+ relFromRoot,
32
+ } from "./core/paths";
33
+ export { checkRegistry } from "./core/registry-check";
34
+ export type {
35
+ ClassifyRule,
36
+ FileStatus,
37
+ FileStatusMap,
38
+ Rule,
39
+ Severity,
40
+ WatchdogDefinition,
41
+ WatchdogMeta,
42
+ WatchdogRenderArgs,
43
+ WatchdogRenderCandidate,
44
+ WatchdogRenderContext,
45
+ } from "./core/types";
46
+ export {
47
+ defineWatchdog,
48
+ SEVERITY_EMOJI,
49
+ SEVERITY_ORDER,
50
+ severityRank,
51
+ } from "./core/types";
52
+ export {
53
+ findWatchdog,
54
+ getWatchdog,
55
+ getWatchdogContext,
56
+ } from "./core/watchdog-registry";
@@ -0,0 +1,96 @@
1
+ # Protocolo de Auditoria dos Watchdogs — houndsman
2
+
3
+ Referência do Modo B do skill `houndsman-update`. Objetivo: garantir que as regras dos
4
+ watchdogs validam **apenas padrões que o projeto aplica hoje**. Regras "utópicas" (não
5
+ aplicadas em nenhuma parte do código) devem ser **removidas**, não apenas rebaixadas.
6
+
7
+ ---
8
+
9
+ ## Entradas
10
+
11
+ 1. `.houndsman/watchdogs/<id>.ts` — `WATCHDOG_DEFINITIONS[<id>]` é a fonte única de verdade:
12
+ `description`, `scope`, `rules` (severidade + texto), `classify`, `contextFiles`,
13
+ `reviewFocus`, `exclude`.
14
+ 2. `src/core/brief.ts` (no motor) — `PROCEDURE` (passos de revisão compartilhados).
15
+ 3. Ground truth (padrões reais do projeto):
16
+ - Documentos de convenção do projeto (ex.: `AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md`).
17
+ - Documentação de arquitetura/domínio do projeto.
18
+ - Código-fonte real do domínio (fonte final da verdade).
19
+
20
+ ## Regra de ouro
21
+
22
+ > Só é regra o que o projeto **aplica hoje** — mas como provar depende da **polaridade**:
23
+ >
24
+ > - Regra **afirmativa** (`Promover:`): a **ausência** do padrão no código prova que ele **não é
25
+ > aplicado** → `UTOPIAN` (remover/rebaixar). Não inferir intenção aspiracional.
26
+ > - Regra **negativa** (`Evitar:`, `Não`, `Nunca`): a **ausência** do anti-padrão prova que a regra
27
+ > **é seguida** → `ALIGNED` (**manter**). Nunca remover uma regra negativa por "0 ocorrências".
28
+
29
+ Use regra **geral e reutilizável** (padrão do domínio), nunca regra de negócio pontual amarrada a
30
+ uma classe/use-case — isso é papel de teste automatizado.
31
+
32
+ Use `grep_search`/`file_search`/`read_file` para **provar** cada veredito. Nunca
33
+ classifique sem evidência (caminho de arquivo, linha, ou "0 ocorrências via grep X").
34
+
35
+ ## Taxonomia de veredito
36
+
37
+ | Veredito | Significado | Ação proposta |
38
+ |---|---|---|
39
+ | `ALIGNED` | Padrão documentado E aplicado no código (regra negativa: anti-padrão **ausente**) | Manter |
40
+ | `UTOPIAN` | Regra **afirmativa** cujo padrão não existe em lugar nenhum hoje | **Remover** |
41
+ | `OUTDATED` | Já foi aplicado, mas o código mudou | Remover ou reescrever |
42
+ | `MISCALIBRATED` | Regra válida, mas severidade errada | Ajustar severidade |
43
+ | `DEAD-PATH` | `scope`/`contextFiles`/`classify` apontam para caminho/arquivo inexistente | Corrigir ou remover |
44
+
45
+ ## Formato do relatório
46
+
47
+ Gravar `.houndsman-audit/<watchdog-id>.md`:
48
+
49
+ ```markdown
50
+ # Auditoria — <Nome do watchdog> (`<watchdog-id>`)
51
+
52
+ ## Ground truth do domínio
53
+ - Padrões reais observados (com caminhos de arquivo).
54
+ - Caminhos reais do domínio (confirmar existência).
55
+
56
+ ## Veredito por regra
57
+
58
+ | # | Severidade atual | Polaridade | Regra | Veredito | Evidência | Ação proposta |
59
+ |---|---|---|---|---|---|---|
60
+ | 1 | blocking | afirmativa | <texto da regra> | UTOPIAN | 0 matches em `src/...` | Remover |
61
+ | 2 | attention | negativa | <texto da regra> | ALIGNED | 0 ocorrências do anti-padrão | Manter |
62
+ | ... |
63
+
64
+ ## Correções de metadados
65
+ - `scope`: ...
66
+ - `contextFiles`: ...
67
+
68
+ ## Protocolo (procedimento)
69
+ - Ajustes no `PROCEDURE` compartilhado (`src/core/brief.ts`) ou no `reviewFocus`/`exclude` do watchdog.
70
+
71
+ ## Resumo
72
+ - Total de regras: N · ALIGNED: N · UTOPIAN: N · OUTDATED: N · MISCALIBRATED: N · DEAD-PATH: N
73
+ - Regras a remover: ...
74
+ - Regras a corrigir: ...
75
+ ```
76
+
77
+ ## Consolidação
78
+
79
+ - Um relatório consolidado em `.houndsman-audit/audit-summary.md`.
80
+ - Agrupar **temas transversais** (ex.: a mesma regra aparecia em 2 watchdogs).
81
+ - Resolver conflitos: uma regra duplicada entre watchdogs deve viver no watchdog dono do domínio.
82
+
83
+ ## Disparo dos subagentes
84
+
85
+ - 1 subagente por watchdog; despachar em ondas paralelas (ex.: 8 + 7).
86
+ - Prompt mínimo:
87
+
88
+ > Você é um auditor do skill `houndsman` do {{projectName}}. Leia e siga
89
+ > `.houndsman/core/skill/AUDIT-PROTOCOL.md`. Watchdog alvo: `<watchdog-id>`. Audite CADA regra de
90
+ > `WATCHDOG_DEFINITIONS["<watchdog-id>"]` contra o código/docs reais, provando cada veredito.
91
+ > Grave `.houndsman-audit/<watchdog-id>.md`. Retorne: contagem por veredito,
92
+ > regras a REMOVER e a CORRIGIR. Não altere outros arquivos.
93
+
94
+ ## Idioma
95
+
96
+ Relatórios em português. Código e identificadores em inglês. Seja sucinto.