@payconductor/houndsman 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payconductor/houndsman",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Orquestrador de revisão de Pull Requests: classifica o diff, despacha watchdogs especializados por domínio e consolida o relatório.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -127,6 +127,57 @@ function checkRulePolarity(config: HoundsmanConfig, warnings: string[]): void {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** Normaliza o texto da regra para detectar duplicatas (case/espaços). */
|
|
131
|
+
function normalizeRuleText(text: string): string {
|
|
132
|
+
return text.trim().toLowerCase().replace(/\s+/g, " ");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Aviso para regra repetida (texto normalizado) no mesmo watchdog — a regra deve
|
|
137
|
+
* existir uma única vez em `rules` (§ Convenções de escrita, item 10).
|
|
138
|
+
*/
|
|
139
|
+
function checkRuleDuplication(config: HoundsmanConfig, warnings: string[]): void {
|
|
140
|
+
for (const id of config.watchdogIds) {
|
|
141
|
+
const def = config.watchdogs[id];
|
|
142
|
+
if (!def) continue;
|
|
143
|
+
const seen = new Set<string>();
|
|
144
|
+
def.rules.forEach((rule, index) => {
|
|
145
|
+
const key = normalizeRuleText(rule.text);
|
|
146
|
+
if (!key) return;
|
|
147
|
+
if (seen.has(key)) {
|
|
148
|
+
warnings.push(`Regra duplicada em ${id} (rule-${index}): ${rule.text}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
seen.add(key);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Campos de metadados que NÃO devem conter texto de regra. */
|
|
157
|
+
const RULE_BEARING_FIELDS = ["reviewFocus", "contextFiles", "exclude"] as const;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Aviso quando `reviewFocus`/`contextFiles`/`exclude` recebem texto de regra
|
|
161
|
+
* (prefixo de polaridade) — a regra vive só em `rules`; duplicá-la nesses campos
|
|
162
|
+
* deixa o brief do subagente redundante.
|
|
163
|
+
*/
|
|
164
|
+
function checkMetadataPrecision(config: HoundsmanConfig, warnings: string[]): void {
|
|
165
|
+
for (const id of config.watchdogIds) {
|
|
166
|
+
const def = config.watchdogs[id];
|
|
167
|
+
if (!def) continue;
|
|
168
|
+
for (const field of RULE_BEARING_FIELDS) {
|
|
169
|
+
const items = def[field];
|
|
170
|
+
if (!items) continue;
|
|
171
|
+
items.forEach((item, index) => {
|
|
172
|
+
if (!POLARITY_PREFIX.test(item.trim())) return;
|
|
173
|
+
warnings.push(
|
|
174
|
+
`Texto de regra em ${id}.${field}[${index}] — a regra vive só em 'rules': ${item}`,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
130
181
|
/** Aviso para orçamento/concorrência inválidos — o motor cai no default. */
|
|
131
182
|
function checkBudgets(config: HoundsmanConfig, warnings: string[]): void {
|
|
132
183
|
const maxFiles = config.budgets?.maxFilesPerBrief;
|
|
@@ -153,6 +204,8 @@ export function checkRegistry(config: HoundsmanConfig): {
|
|
|
153
204
|
checkClassification(config, problems);
|
|
154
205
|
checkRendering(config, problems);
|
|
155
206
|
checkRulePolarity(config, warnings);
|
|
207
|
+
checkRuleDuplication(config, warnings);
|
|
208
|
+
checkMetadataPrecision(config, warnings);
|
|
156
209
|
checkBudgets(config, warnings);
|
|
157
210
|
const jevQuestions = checkJevSpecs(config, problems);
|
|
158
211
|
|
|
@@ -11,6 +11,10 @@ aplicadas em nenhuma parte do código) devem ser **removidas**, não apenas reba
|
|
|
11
11
|
1. `.houndsman/watchdogs/<id>.ts` — `WATCHDOG_DEFINITIONS[<id>]` é a fonte única de verdade:
|
|
12
12
|
`description`, `scope`, `rules` (severidade + texto), `classify`, `contextFiles`,
|
|
13
13
|
`reviewFocus`, `exclude`.
|
|
14
|
+
|
|
15
|
+
> **Onde cada coisa vive:** a **regra** vive só em `rules`; `reviewFocus` = paths a priorizar;
|
|
16
|
+
> `contextFiles` = docs a consultar; `exclude` = paths a ignorar; `scope`/`description` = prosa.
|
|
17
|
+
> Texto de regra em `reviewFocus`/`contextFiles`/`exclude` é **duplicação** a corrigir.
|
|
14
18
|
2. `src/core/brief.ts` (no motor) — `PROCEDURE` (passos de revisão compartilhados).
|
|
15
19
|
3. Ground truth (padrões reais do projeto):
|
|
16
20
|
- Documentos de convenção do projeto (ex.: `AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md`).
|
|
@@ -41,6 +45,7 @@ classifique sem evidência (caminho de arquivo, linha, ou "0 ocorrências via gr
|
|
|
41
45
|
| `OUTDATED` | Já foi aplicado, mas o código mudou | Remover ou reescrever |
|
|
42
46
|
| `MISCALIBRATED` | Regra válida, mas severidade errada | Ajustar severidade |
|
|
43
47
|
| `DEAD-PATH` | `scope`/`contextFiles`/`classify` apontam para caminho/arquivo inexistente | Corrigir ou remover |
|
|
48
|
+
| `REDUNDANT` | Regra duplicada no mesmo watchdog ou repetida em `reviewFocus`/`contextFiles`/`exclude` | Remover a cópia (a regra fica só em `rules`) |
|
|
44
49
|
|
|
45
50
|
## Formato do relatório
|
|
46
51
|
|
package/src/skill/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Um único skill (`/houndsman`), quatro capabilities (detalhadas em `hunt.md`, `t
|
|
|
52
52
|
| `houndsman setup` | Cria a configuração inicial do projeto |
|
|
53
53
|
| `houndsman update` | Sincroniza os arquivos auto-gerados (`.houndsman/core`) |
|
|
54
54
|
| `houndsman doctor` | Valida a configuração, o ambiente e a credencial Jev (anti-drift) |
|
|
55
|
-
| `houndsman test` |
|
|
55
|
+
| `houndsman test` | **Manutenção da lib** — roda a suíte de testes do pacote (não é uso do consumidor) |
|
|
56
56
|
|
|
57
57
|
### Flags
|
|
58
58
|
|
|
@@ -138,6 +138,8 @@ houndsman.config.ts # HoundsmanConfig (dados; descoberto em runtime)
|
|
|
138
138
|
> prove que o padrão existe no código (regra negativa: prove que o anti-padrão **não** existe).
|
|
139
139
|
> Regras devem ser **gerais/reutilizáveis** no domínio e declarar a **polaridade** no texto
|
|
140
140
|
> (`Promover:` = deve acontecer; `Evitar:`/`Não`/`Nunca` = não deve acontecer).
|
|
141
|
+
> Cada regra vive **só** em `rules` — não a repita em `reviewFocus`/`contextFiles`/`scope`/
|
|
142
|
+
> `description`/`exclude` (isso só duplicaria o brief).
|
|
141
143
|
|
|
142
144
|
1. Novas regras/watchdogs → editar `.houndsman/watchdogs/<id>.ts` (regras + escopo +
|
|
143
145
|
`classify`) ou `doc-mappings.ts`/`exclusions.ts`. Um watchdog novo exige só o módulo em
|
package/src/skill/SETUP.md
CHANGED
|
@@ -60,7 +60,6 @@ bun run houndsman update # reescreve .houndsman/core/** e .houndsman/.gitig
|
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
bun run houndsman doctor # valida config + ambiente + credencial Jev
|
|
63
|
-
bun run houndsman test # suíte da lib (project-agnóstica)
|
|
64
63
|
bun run houndsman --base main # prepara briefs do diff atual
|
|
65
64
|
```
|
|
66
65
|
|
package/src/skill/hunt.md
CHANGED
|
@@ -271,7 +271,8 @@ completo) · `--no-jev` · `--fail-on-blocking` (com `--finalize`).
|
|
|
271
271
|
> **`train`** (`/houndsman train`, doc `train.md`).
|
|
272
272
|
> Regra de ouro: só é regra o que o projeto **aplica hoje** — prove com evidência no código
|
|
273
273
|
> (regra negativa com 0 ocorrências = seguida → **manter**).
|
|
274
|
-
> Após qualquer alteração, rodar `houndsman doctor
|
|
274
|
+
> Após qualquer alteração, rodar `houndsman doctor`. (`houndsman test` e `ci:fix`/`ci` são
|
|
275
|
+
> comandos da **lib**, não do projeto consumidor.)
|
|
275
276
|
|
|
276
277
|
---
|
|
277
278
|
|
package/src/skill/train.md
CHANGED
|
@@ -72,10 +72,29 @@ Toda regra (nova ou alterada) deve passar por **todos** os itens:
|
|
|
72
72
|
direta da afirmação). Aí o prefixo lidera o sentido dominante e o outro entra como oração
|
|
73
73
|
subordinada — nunca como segundo juízo coordenado.
|
|
74
74
|
9. **Escopo coerente** — a regra pertence ao domínio do watchdog.
|
|
75
|
-
10. **Sem duplicata** — nenhuma regra repetida
|
|
75
|
+
10. **Sem duplicata** — nenhuma regra repetida (texto normalizado) no mesmo watchdog.
|
|
76
|
+
11. **Lugar único** — a regra vive **apenas** em `rules`. Nunca repita o texto (nem o anti-padrão)
|
|
77
|
+
em `reviewFocus`, `contextFiles`, `scope`, `description` ou `exclude`.
|
|
76
78
|
|
|
77
79
|
> O `houndsman doctor` emite um **aviso** (não bloqueante) para regra sem polaridade explícita
|
|
78
|
-
> (`Promover:`, `Evitar:`, `Não` ou `Nunca`)
|
|
80
|
+
> (`Promover:`, `Evitar:`, `Não` ou `Nunca`), para regra duplicada e para texto de regra que
|
|
81
|
+
> vazou para `reviewFocus`/`contextFiles`/`exclude`.
|
|
82
|
+
|
|
83
|
+
### Onde cada coisa vive (não duplique a regra)
|
|
84
|
+
|
|
85
|
+
Cada informação tem o seu campo — não espelhe a regra em vários lugares:
|
|
86
|
+
|
|
87
|
+
| O que você quer expressar | Campo | Observação |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| A **regra** (o que DEVE/NÃO DEVE acontecer) | `rules[]` | Único lugar da regra: `{ severity, text, jev? }`. |
|
|
90
|
+
| **Quais arquivos** o watchdog cobre | `classify` | Regex de path (ou de conteúdo). |
|
|
91
|
+
| **Que arquivos** olhar primeiro | `reviewFocus` | **Paths/áreas** do código — não texto de regra. |
|
|
92
|
+
| **Que docs** consultar | `contextFiles` | Caminhos de **documentação** — não regra nem código. |
|
|
93
|
+
| O que **nunca** carregar | `exclude` | Paths a ignorar. |
|
|
94
|
+
| O **território** em prosa | `scope` / `description` | Contexto do domínio, não regra. |
|
|
95
|
+
|
|
96
|
+
> A regra aparece **uma única vez** no arquivo — em `rules`. Repeti-la em
|
|
97
|
+
> `reviewFocus`/`contextFiles`/`scope`/`description`/`exclude` só cria ruído no brief do subagente.
|
|
79
98
|
|
|
80
99
|
---
|
|
81
100
|
|
|
@@ -166,7 +185,9 @@ verificáveis por leitura local. Candidatos não confirmados pelo watchdog **nã
|
|
|
166
185
|
`Evitar:`/`Não`/`Nunca` (NÃO DEVE), conforme as Convenções de escrita.
|
|
167
186
|
- Regra que mistura afirmação e negação (dois sentidos) → dividir em **duas entradas** (*Unipolar*, item 8).
|
|
168
187
|
- `jev` (opcional): `{ kind: "noul" }`, com `instructions` e/ou `criteria` — ver seção acima.
|
|
169
|
-
-
|
|
188
|
+
- **A regra vai só em `rules`** — não a repita em `reviewFocus`/`contextFiles`/`scope`/
|
|
189
|
+
`description`/`exclude`. Só toque nesses campos se o **domínio/paths** mudou (ver
|
|
190
|
+
*Onde cada coisa vive*), nunca para reafirmar a regra.
|
|
170
191
|
- Se a regra muda para outro domínio (paths), ajustar o `classify` do watchdog.
|
|
171
192
|
5. **Watchdog novo?** criar `.houndsman/watchdogs/<id>.ts` e registrar em `.houndsman/watchdogs/index.ts` (o id é derivado das chaves).
|
|
172
193
|
6. **Atualizar `src/skill/hunt.md`/`README.md`** se escopo/regra visível mudou (no motor).
|
|
@@ -226,16 +247,14 @@ todos os itens — em especial: **geral/reutilizável** (não regra de negócio
|
|
|
226
247
|
|
|
227
248
|
```bash
|
|
228
249
|
bun run houndsman doctor # config sem drift + ambiente + credencial Jev
|
|
229
|
-
bun run houndsman test # suíte da lib (project-agnóstica)
|
|
230
|
-
bun run ci:fix # formatação/imports/lint (no repo da lib)
|
|
231
|
-
bun run ci # tipagem (no repo da lib)
|
|
232
250
|
```
|
|
233
251
|
|
|
252
|
+
> `bun run houndsman test`, `bun run ci:fix` e `bun run ci` são comandos para **manter a lib** do
|
|
253
|
+
> houndsman (repo do pacote) — não para quem só usa o houndsman no projeto.
|
|
254
|
+
|
|
234
255
|
Checklist final:
|
|
235
256
|
|
|
236
257
|
- [ ] `bun run houndsman doctor` → sem problemas reportados.
|
|
237
|
-
- [ ] `bun run houndsman test` → **0 fail**.
|
|
238
|
-
- [ ] `ci:fix` + `ci` → exit 0 (warnings pré-existentes não contam).
|
|
239
258
|
- [ ] `WATCHDOG_DEFINITIONS[<id>].id` bate com a chave e `description` não vazio.
|
|
240
259
|
- [ ] `src/skill/hunt.md` continua consistente com os watchdogs do projeto.
|
|
241
260
|
- [ ] Toda regra nova/alterada tem **evidência** registrada no PR/relatório
|
|
@@ -244,6 +263,8 @@ Checklist final:
|
|
|
244
263
|
- [ ] Brief de amostra não contém regra removida (dry-run abaixo).
|
|
245
264
|
- [ ] Toda regra passa nas convenções de escrita (geral/reutilizável, sem jargão abreviado,
|
|
246
265
|
sem qualificador vago, com polaridade explícita e **unipolar**) — conferir via grep.
|
|
266
|
+
- [ ] O texto de cada regra aparece **exatamente uma vez** no arquivo (só em `rules`) — nada em
|
|
267
|
+
`reviewFocus`/`contextFiles`/`scope`/`description`/`exclude`.
|
|
247
268
|
|
|
248
269
|
```bash
|
|
249
270
|
# Dry-run: gerar briefs e conferir ausência de regras removidas
|
|
@@ -139,6 +139,42 @@ describe("registry-check", () => {
|
|
|
139
139
|
const { warnings } = checkRegistry(config as typeof fakeHoundsmanConfig);
|
|
140
140
|
expect(warnings).toEqual([]);
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
test("regra duplicada no mesmo watchdog gera aviso", () => {
|
|
144
|
+
const config = {
|
|
145
|
+
...fakeHoundsmanConfig,
|
|
146
|
+
watchdogs: {
|
|
147
|
+
...fakeHoundsmanConfig.watchdogs,
|
|
148
|
+
alpha: {
|
|
149
|
+
...alphaWatchdog,
|
|
150
|
+
rules: [
|
|
151
|
+
{ severity: "attention" as const, text: "Evitar console.log." },
|
|
152
|
+
{ severity: "blocking" as const, text: "Evitar console.log." },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const { warnings } = checkRegistry(config as typeof fakeHoundsmanConfig);
|
|
158
|
+
expect(warnings.some((w) => w.includes("duplicada"))).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("texto de regra em reviewFocus/contextFiles gera aviso", () => {
|
|
162
|
+
const config = {
|
|
163
|
+
...fakeHoundsmanConfig,
|
|
164
|
+
watchdogs: {
|
|
165
|
+
...fakeHoundsmanConfig.watchdogs,
|
|
166
|
+
alpha: {
|
|
167
|
+
...alphaWatchdog,
|
|
168
|
+
reviewFocus: ["Evitar: console.log."],
|
|
169
|
+
contextFiles: ["Promover: validar a entrada."],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
const { warnings } = checkRegistry(config as typeof fakeHoundsmanConfig);
|
|
174
|
+
expect(
|
|
175
|
+
warnings.filter((w) => w.includes("a regra vive só em 'rules'")),
|
|
176
|
+
).toHaveLength(2);
|
|
177
|
+
});
|
|
142
178
|
});
|
|
143
179
|
|
|
144
180
|
// ---------------------------------------------------------------------------
|