@payconductor/houndsman 0.5.0 → 0.7.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.
- package/README.md +24 -4
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/grammars/web-tree-sitter.wasm +0 -0
- package/package.json +11 -3
- package/src/cli/commands/command.ts +5 -0
- package/src/cli/commands/doctor-command.ts +116 -24
- package/src/cli/commands/hunt-command.ts +25 -0
- package/src/cli/commands/server-command.ts +7 -1
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +10 -6
- package/src/core/ai/ai-client.ts +221 -0
- package/src/core/ai/ai-sdk-model-provider.ts +101 -0
- package/src/core/ai/errors.ts +36 -0
- package/src/core/ai/features/brief-enricher.ts +62 -0
- package/src/core/ai/features/classifier.ts +50 -0
- package/src/core/ai/features/context-summarizer.ts +31 -0
- package/src/core/ai/features/pre-analyzer.ts +51 -0
- package/src/core/ai/features/verifier.ts +45 -0
- package/src/core/ai/index.ts +56 -0
- package/src/core/ai/model-factory.ts +137 -0
- package/src/core/ai/prompts/base.ts +36 -0
- package/src/core/ai/prompts/brief.ts +87 -0
- package/src/core/ai/prompts/classification.ts +36 -0
- package/src/core/ai/prompts/context.ts +29 -0
- package/src/core/ai/prompts/pre-analysis.ts +49 -0
- package/src/core/ai/prompts/verify.ts +40 -0
- package/src/core/ai/schemas.ts +110 -0
- package/src/core/ai/types.ts +76 -0
- package/src/core/ai/usage-budget.ts +75 -0
- package/src/core/ai/usage-recorder.ts +79 -0
- package/src/core/assets.ts +16 -0
- package/src/core/ast/diff.ts +220 -0
- package/src/core/ast/impact.ts +127 -0
- package/src/core/ast/parser.ts +167 -0
- package/src/core/ast/service.ts +233 -0
- package/src/core/ast/symbols.ts +294 -0
- package/src/core/ast/types.ts +125 -0
- package/src/core/brief.ts +138 -0
- package/src/core/commands/findings.ts +65 -2
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/update.ts +5 -3
- package/src/core/config.ts +85 -0
- package/src/core/defaults.ts +27 -0
- package/src/core/domain/watchdog.ts +43 -5
- package/src/core/engine/houndsman-engine.ts +16 -1
- package/src/core/engine/review-scope.ts +52 -6
- package/src/core/engine/review-session.ts +27 -2
- package/src/core/env.ts +101 -0
- package/src/core/file-classifier.ts +25 -4
- package/src/core/git-runner.ts +25 -13
- package/src/core/github-review.ts +10 -7
- package/src/core/jev/jev-client.ts +8 -7
- package/src/core/jev/jev-verify.ts +2 -1
- package/src/core/logging/index.ts +17 -0
- package/src/core/logging/logger.ts +187 -0
- package/src/core/orchestrator.ts +357 -9
- package/src/core/project-config.ts +123 -1
- package/src/core/registry-check.ts +148 -6
- package/src/core/renderers/manifest-writer.ts +33 -0
- package/src/core/rule-text.ts +97 -0
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/gh-client.ts +38 -22
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/types.ts +6 -0
- package/src/core/storage/work-area.ts +12 -0
- package/src/core/types.ts +37 -1
- package/src/core/verify.ts +17 -5
- package/src/harness/README.md +15 -0
- package/src/harness/agent/agent-context.ts +33 -0
- package/src/harness/agent/agent-errors.ts +30 -0
- package/src/harness/agent/agent-factory.ts +45 -0
- package/src/harness/agent/agent-reporter.ts +34 -0
- package/src/harness/agent/agent-result.ts +16 -0
- package/src/harness/agent/agent-runner.ts +32 -0
- package/src/harness/agent/agent-scheduler.ts +37 -0
- package/src/harness/agent/review-agent.ts +391 -0
- package/src/harness/config/harness-config.ts +84 -0
- package/src/harness/hunt/hunt-harness.ts +83 -0
- package/src/harness/hunt/hunt-options.ts +75 -0
- package/src/harness/hunt/hunt-result.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +372 -0
- package/src/harness/hunt/review-planner.ts +98 -0
- package/src/harness/index.ts +36 -0
- package/src/harness/investigation/agent-state.ts +134 -0
- package/src/harness/investigation/evidence-verifier.ts +129 -0
- package/src/harness/investigation/evidence.ts +110 -0
- package/src/harness/investigation/finding-critic.ts +96 -0
- package/src/harness/investigation/investigation-planner.ts +118 -0
- package/src/harness/investigation/investigation-task.ts +10 -0
- package/src/harness/investigation/review-journal.ts +87 -0
- package/src/harness/investigation/rule-analyzer.ts +178 -0
- package/src/harness/investigation/rule-signals.ts +110 -0
- package/src/harness/prompts/agent-schemas.ts +57 -0
- package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
- package/src/harness/prompts/states/conclude-prompt.ts +16 -0
- package/src/harness/prompts/states/investigate-prompt.ts +37 -0
- package/src/harness/prompts/states/orient-prompt.ts +15 -0
- package/src/harness/prompts/states/verify-prompt.ts +22 -0
- package/src/harness/tools/agent-tool.ts +102 -0
- package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
- package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
- package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
- package/src/harness/tools/builtin/find-references-tool.ts +66 -0
- package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
- package/src/harness/tools/builtin/helpers.ts +115 -0
- package/src/harness/tools/builtin/impact-tool.ts +48 -0
- package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
- package/src/harness/tools/builtin/index.ts +59 -0
- package/src/harness/tools/builtin/list-files-tool.ts +85 -0
- package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
- package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
- package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
- package/src/harness/tools/builtin/search-code-tool.ts +65 -0
- package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
- package/src/harness/tools/path-guard.ts +90 -0
- package/src/harness/tools/result-limiter.ts +49 -0
- package/src/harness/tools/secret-redactor.ts +54 -0
- package/src/harness/tools/tool-cache.ts +50 -0
- package/src/harness/tools/tool-call-budget.ts +60 -0
- package/src/harness/tools/tool-context.ts +5 -0
- package/src/harness/tools/tool-middleware.ts +105 -0
- package/src/harness/tools/tool-policy.ts +51 -0
- package/src/harness/tools/tool-registry.ts +112 -0
- package/src/harness/tools/tool-result.ts +15 -0
- package/src/harness/tools/toolbox.ts +36 -0
- package/src/harness/validation/finding-validator.ts +119 -0
- package/src/index.ts +46 -0
- package/src/server/client/App.tsx +38 -12
- package/src/server/client/CommentsPanel.tsx +255 -0
- package/src/server/client/FindingCard.tsx +12 -4
- package/src/server/client/FindingsList.tsx +4 -1
- package/src/server/client/Sidebar.tsx +397 -0
- package/src/server/client/SubmitModal.tsx +61 -5
- package/src/server/client/icons.tsx +19 -0
- package/src/server/client/index.tsx +1 -1
- package/src/server/client/utils/constants.ts +29 -5
- package/src/server/client/utils/format.ts +47 -2
- package/src/server/client/utils/types.ts +88 -0
- package/src/server/editor.ts +129 -0
- package/src/server/index.ts +115 -5
- package/src/server/pr-comments.ts +256 -0
- package/src/server/pr-inbox.ts +202 -0
- package/src/server/run-repository.ts +33 -1
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/hunt.md +14 -2
- package/src/skill/verify.md +1 -1
- package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
- package/src/tests/ai-core.test.ts +120 -0
- package/src/tests/ai-features.test.ts +183 -0
- package/src/tests/ast.test.ts +335 -0
- package/src/tests/cli.test.ts +33 -0
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/fixtures/fake-agent-tools.ts +69 -0
- package/src/tests/fixtures/fake-investigation.ts +64 -0
- package/src/tests/fixtures/fake-model-provider.ts +48 -0
- package/src/tests/github-review.test.ts +9 -0
- package/src/tests/harness-agent.test.ts +122 -0
- package/src/tests/harness-investigation.test.ts +206 -0
- package/src/tests/harness-tools.test.ts +148 -0
- package/src/tests/hunt-command.test.ts +212 -0
- package/src/tests/logger.test.ts +83 -0
- package/src/tests/review-scope.test.ts +11 -11
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/server-editor.test.ts +100 -0
- package/src/tests/server.test.ts +295 -2
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- package/src/server/client/RunList.tsx +0 -75
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { HoundsmanConfig } from "./config";
|
|
9
|
+
import { env } from "./env";
|
|
9
10
|
import { JevQuestionFactory } from "./jev";
|
|
10
11
|
import { renderReport, sanitizeBranch } from "./report-builder";
|
|
12
|
+
import { hasExplicitPolarity } from "./rule-text";
|
|
11
13
|
import type { WatchdogMeta } from "./types";
|
|
12
14
|
|
|
13
15
|
/** `id` da definição bate com a chave; `description` não vazio. */
|
|
@@ -27,17 +29,56 @@ function checkDefinitions(config: HoundsmanConfig, problems: string[]): void {
|
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
/** Todo watchdog precisa declarar ao menos um arquivo em `classify
|
|
32
|
+
/** Todo watchdog precisa declarar ao menos um arquivo em `classify` (fallback isento). */
|
|
31
33
|
function checkClassification(config: HoundsmanConfig, problems: string[]): void {
|
|
32
34
|
for (const id of config.watchdogIds) {
|
|
33
35
|
const def = config.watchdogs[id];
|
|
34
36
|
if (!def) continue;
|
|
37
|
+
if (def.fallback) continue;
|
|
35
38
|
if (!def.classify || def.classify.length === 0) {
|
|
36
39
|
problems.push(`Watchdog '${id}' não declara nenhum padrão em 'classify'`);
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
/** Fallback: no máximo um; `classify` é ignorado; precisa de regras. */
|
|
45
|
+
function checkFallbacks(config: HoundsmanConfig, warnings: string[]): void {
|
|
46
|
+
const fallbacks = config.watchdogIds.filter(
|
|
47
|
+
(id) => config.watchdogs[id]?.fallback === true,
|
|
48
|
+
);
|
|
49
|
+
if (fallbacks.length > 1) {
|
|
50
|
+
warnings.push(
|
|
51
|
+
`Mais de um watchdog fallback (${fallbacks.join(", ")}); o primeiro declarado vence.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
for (const id of fallbacks) {
|
|
55
|
+
const def = config.watchdogs[id];
|
|
56
|
+
if (!def) continue;
|
|
57
|
+
if (def.classify && def.classify.length > 0) {
|
|
58
|
+
warnings.push(`watchdogs['${id}'] é fallback — 'classify' é ignorado.`);
|
|
59
|
+
}
|
|
60
|
+
if (def.rules.length === 0) {
|
|
61
|
+
warnings.push(`Watchdog fallback '${id}' não declara nenhuma regra.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Findings emergentes exigem ao menos uma regra de lógica/domínio como piso. */
|
|
67
|
+
function checkEmergent(config: HoundsmanConfig, warnings: string[]): void {
|
|
68
|
+
for (const id of config.watchdogIds) {
|
|
69
|
+
const def = config.watchdogs[id];
|
|
70
|
+
if (!def?.allowEmergentFindings) continue;
|
|
71
|
+
const hasGeneric = def.rules.some((rule) =>
|
|
72
|
+
/l[oó]gica|dom[ií]nio/i.test(rule.text),
|
|
73
|
+
);
|
|
74
|
+
if (!hasGeneric) {
|
|
75
|
+
warnings.push(
|
|
76
|
+
`watchdogs['${id}'].allowEmergentFindings sem regra de lógica/domínio — o piso fica implícito.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
41
82
|
/** Smoke test dos helpers críticos de renderização. */
|
|
42
83
|
function checkRendering(config: HoundsmanConfig, problems: string[]): void {
|
|
43
84
|
if (sanitizeBranch("feature/x") !== "feature-x") {
|
|
@@ -106,9 +147,6 @@ function checkJevSpecs(config: HoundsmanConfig, problems: string[]): number {
|
|
|
106
147
|
return generated;
|
|
107
148
|
}
|
|
108
149
|
|
|
109
|
-
/** Prefixos de polaridade aceitos no texto da regra (§ Convenções de escrita). */
|
|
110
|
-
const POLARITY_PREFIX = /^(promover|evitar|não|nao|nunca)\b/i;
|
|
111
|
-
|
|
112
150
|
/**
|
|
113
151
|
* Aviso **não bloqueante** para regra sem polaridade explícita: o texto deve
|
|
114
152
|
* começar com `Promover:` (DEVE acontecer) ou `Evitar:`/`Não`/`Nunca` (NÃO DEVE).
|
|
@@ -119,7 +157,7 @@ function checkRulePolarity(config: HoundsmanConfig, warnings: string[]): void {
|
|
|
119
157
|
if (!def) continue;
|
|
120
158
|
def.rules.forEach((rule, index) => {
|
|
121
159
|
const text = rule.text.trim();
|
|
122
|
-
if (!text ||
|
|
160
|
+
if (!text || hasExplicitPolarity(text)) return;
|
|
123
161
|
warnings.push(
|
|
124
162
|
`Regra sem polaridade explícita em ${id} (rule-${index}): ${rule.text}`,
|
|
125
163
|
);
|
|
@@ -169,7 +207,7 @@ function checkMetadataPrecision(config: HoundsmanConfig, warnings: string[]): vo
|
|
|
169
207
|
const items = def[field];
|
|
170
208
|
if (!items) continue;
|
|
171
209
|
items.forEach((item, index) => {
|
|
172
|
-
if (!
|
|
210
|
+
if (!hasExplicitPolarity(item)) return;
|
|
173
211
|
warnings.push(
|
|
174
212
|
`Texto de regra em ${id}.${field}[${index}] — a regra vive só em 'rules': ${item}`,
|
|
175
213
|
);
|
|
@@ -192,6 +230,106 @@ function checkBudgets(config: HoundsmanConfig, warnings: string[]): void {
|
|
|
192
230
|
}
|
|
193
231
|
}
|
|
194
232
|
|
|
233
|
+
/** Valida a configuração do AST (a disponibilidade das grammars é checada no doctor). */
|
|
234
|
+
function checkAstConfig(
|
|
235
|
+
config: HoundsmanConfig,
|
|
236
|
+
problems: string[],
|
|
237
|
+
warnings: string[],
|
|
238
|
+
): void {
|
|
239
|
+
const ast = config.ast;
|
|
240
|
+
if (!ast) return;
|
|
241
|
+
|
|
242
|
+
if (ast.maxFileBytes !== undefined && ast.maxFileBytes <= 0) {
|
|
243
|
+
warnings.push(
|
|
244
|
+
`ast.maxFileBytes = ${ast.maxFileBytes} — não-positivo; usando o default.`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (ast.maxImpactFiles !== undefined && ast.maxImpactFiles < 0) {
|
|
248
|
+
warnings.push(
|
|
249
|
+
`ast.maxImpactFiles = ${ast.maxImpactFiles} — negativo; impacto será desabilitado.`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const valid = new Set(["typescript", "tsx", "javascript"]);
|
|
253
|
+
for (const [extension, language] of Object.entries(ast.languages ?? {})) {
|
|
254
|
+
if (!valid.has(language)) {
|
|
255
|
+
problems.push(
|
|
256
|
+
`ast.languages['${extension}'] = '${language}' (esperado typescript|tsx|javascript)`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const [alias, target] of Object.entries(ast.aliases ?? {})) {
|
|
261
|
+
if (!alias.trim() || !target.trim()) {
|
|
262
|
+
warnings.push(`ast.aliases['${alias}'] vazio — alias ignorado.`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Valida a configuração de IA (sem exigir chave — fail-open). */
|
|
268
|
+
function checkAiConfig(
|
|
269
|
+
config: HoundsmanConfig,
|
|
270
|
+
problems: string[],
|
|
271
|
+
warnings: string[],
|
|
272
|
+
): void {
|
|
273
|
+
const ai = config.ai;
|
|
274
|
+
if (!ai) return;
|
|
275
|
+
|
|
276
|
+
const validProviders = new Set(["deepseek", "openai", "anthropic"]);
|
|
277
|
+
if (ai.provider !== undefined && !validProviders.has(ai.provider)) {
|
|
278
|
+
problems.push(
|
|
279
|
+
`ai.provider = '${ai.provider}' (esperado deepseek|openai|anthropic)`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (ai.enabled !== false && ai.model !== undefined && !ai.model.trim()) {
|
|
283
|
+
warnings.push("ai.model vazio — usando o default.");
|
|
284
|
+
}
|
|
285
|
+
for (const key of [
|
|
286
|
+
"concurrency",
|
|
287
|
+
"maxSteps",
|
|
288
|
+
"maxOutputTokens",
|
|
289
|
+
"timeoutMs",
|
|
290
|
+
] as const) {
|
|
291
|
+
const value = ai[key];
|
|
292
|
+
if (value !== undefined && value <= 0) {
|
|
293
|
+
warnings.push(`ai.${key} = ${value} — não-positivo; usando o default.`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (ai.maxCallsPerReview !== undefined && ai.maxCallsPerReview <= 0) {
|
|
297
|
+
warnings.push(
|
|
298
|
+
`ai.maxCallsPerReview = ${ai.maxCallsPerReview} — não-positivo; usando o default.`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
if (ai.maxTokensPerReview !== undefined && ai.maxTokensPerReview < 0) {
|
|
302
|
+
warnings.push(
|
|
303
|
+
`ai.maxTokensPerReview = ${ai.maxTokensPerReview} — negativo; sem teto.`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
for (const [id, override] of Object.entries(ai.watchdogs ?? {})) {
|
|
307
|
+
if (!config.watchdogIds.includes(id)) {
|
|
308
|
+
warnings.push(`ai.watchdogs['${id}'] não corresponde a nenhum watchdog.`);
|
|
309
|
+
}
|
|
310
|
+
if (override.maxSteps !== undefined && override.maxSteps <= 0) {
|
|
311
|
+
warnings.push(
|
|
312
|
+
`ai.watchdogs['${id}'].maxSteps não-positivo — usando o default.`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const provider = ai.provider ?? "deepseek";
|
|
318
|
+
if (validProviders.has(provider) && ai.enabled !== false) {
|
|
319
|
+
const credential =
|
|
320
|
+
provider === "deepseek"
|
|
321
|
+
? env.deepSeekApiKey
|
|
322
|
+
: provider === "openai"
|
|
323
|
+
? env.openAiApiKey
|
|
324
|
+
: env.anthropicApiKey;
|
|
325
|
+
if (!credential) {
|
|
326
|
+
warnings.push(
|
|
327
|
+
`ai.provider '${provider}' sem credencial — features de IA ficam fail-open.`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
195
333
|
export function checkRegistry(config: HoundsmanConfig): {
|
|
196
334
|
problems: string[];
|
|
197
335
|
warnings: string[];
|
|
@@ -202,11 +340,15 @@ export function checkRegistry(config: HoundsmanConfig): {
|
|
|
202
340
|
|
|
203
341
|
checkDefinitions(config, problems);
|
|
204
342
|
checkClassification(config, problems);
|
|
343
|
+
checkFallbacks(config, warnings);
|
|
344
|
+
checkEmergent(config, warnings);
|
|
205
345
|
checkRendering(config, problems);
|
|
206
346
|
checkRulePolarity(config, warnings);
|
|
207
347
|
checkRuleDuplication(config, warnings);
|
|
208
348
|
checkMetadataPrecision(config, warnings);
|
|
209
349
|
checkBudgets(config, warnings);
|
|
350
|
+
checkAstConfig(config, problems, warnings);
|
|
351
|
+
checkAiConfig(config, problems, warnings);
|
|
210
352
|
const jevQuestions = checkJevSpecs(config, problems);
|
|
211
353
|
|
|
212
354
|
return { problems, warnings, jevQuestions };
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* findings apontam para o staging da review.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { AiUsage } from "../ai/types";
|
|
9
10
|
import { Classification } from "../domain/classification";
|
|
10
11
|
import type { ClassificationResult } from "../file-classifier";
|
|
11
12
|
import type { JevCandidatesFile } from "../jev";
|
|
@@ -22,6 +23,27 @@ export type ManifestOptions = {
|
|
|
22
23
|
head: string;
|
|
23
24
|
};
|
|
24
25
|
|
|
26
|
+
/** Resumo do AST no manifesto. */
|
|
27
|
+
export type ManifestAst = {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
languages: string[];
|
|
30
|
+
files: number;
|
|
31
|
+
impact: {
|
|
32
|
+
affectedFiles: number;
|
|
33
|
+
importers: number;
|
|
34
|
+
exportedChanges: number;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Resumo da IA no manifesto (só quando a IA foi usada). */
|
|
39
|
+
export type ManifestAi = {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
provider: string;
|
|
42
|
+
model: string;
|
|
43
|
+
briefPlans: number;
|
|
44
|
+
usage: AiUsage;
|
|
45
|
+
};
|
|
46
|
+
|
|
25
47
|
/** Classificação enriquecida gravada pelo prepare (modo + incremental). */
|
|
26
48
|
export type ManifestClassification = ClassificationResult & {
|
|
27
49
|
headSha?: string;
|
|
@@ -57,6 +79,8 @@ export class ManifestWriter {
|
|
|
57
79
|
activeIds: string[],
|
|
58
80
|
pending: string[],
|
|
59
81
|
jev: JevCandidatesFile,
|
|
82
|
+
ast?: ManifestAst,
|
|
83
|
+
ai?: ManifestAi,
|
|
60
84
|
): void {
|
|
61
85
|
const view = Classification.from(classification);
|
|
62
86
|
const maxFilesPerBrief = this.project.maxFilesPerBrief;
|
|
@@ -65,9 +89,11 @@ export class ManifestWriter {
|
|
|
65
89
|
const watchdogs = activeIds.map((id) => {
|
|
66
90
|
const selection = view.budgetFor(id, maxFilesPerBrief);
|
|
67
91
|
filesOverBudget += selection.overflow.length;
|
|
92
|
+
const domain = this.project.watchdogs.find(id);
|
|
68
93
|
return {
|
|
69
94
|
id,
|
|
70
95
|
name: this.project.watchdogs.nameOf(id),
|
|
96
|
+
...(domain?.definition.fallback ? { fallback: true } : {}),
|
|
71
97
|
files: selection.selected,
|
|
72
98
|
overflow: selection.overflow.length,
|
|
73
99
|
brief: this.work.briefFile(id),
|
|
@@ -109,6 +135,13 @@ export class ManifestWriter {
|
|
|
109
135
|
skipped: jev.skipped?.length ?? 0,
|
|
110
136
|
}
|
|
111
137
|
: { available: false, reason: jev.reason },
|
|
138
|
+
ast: ast ?? {
|
|
139
|
+
enabled: false,
|
|
140
|
+
languages: [],
|
|
141
|
+
files: 0,
|
|
142
|
+
impact: { affectedFiles: 0, importers: 0, exportedChanges: 0 },
|
|
143
|
+
},
|
|
144
|
+
ai,
|
|
112
145
|
deleted: classification.deleted,
|
|
113
146
|
unclassified: classification.unclassified,
|
|
114
147
|
excludedCount: classification.excludedCount,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Texto de regra (biblioteca) — fonte única de polaridade e prefixos reservados.
|
|
3
|
+
*
|
|
4
|
+
* `RuleAnalyzer`, `registry-check` e o harness usam as mesmas listas: evita
|
|
5
|
+
* divergência entre “Promover/Evitar” no analyzer e as validações anti-drift.
|
|
6
|
+
* Também define o vocabulário de findings emergentes (lógica/domínio).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Intenção de uma regra. */
|
|
10
|
+
export type RuleIntent = "promote" | "avoid";
|
|
11
|
+
|
|
12
|
+
/** Palavras que abrem uma regra de “deve acontecer”. */
|
|
13
|
+
export const PROMOTE_PREFIXES = [
|
|
14
|
+
"promover",
|
|
15
|
+
"deve",
|
|
16
|
+
"dever",
|
|
17
|
+
"use",
|
|
18
|
+
"prefira",
|
|
19
|
+
"sempre",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
/** Palavras que abrem uma regra de “não deve acontecer”. */
|
|
23
|
+
export const AVOID_PREFIXES = [
|
|
24
|
+
"evitar",
|
|
25
|
+
"não",
|
|
26
|
+
"nao",
|
|
27
|
+
"nunca",
|
|
28
|
+
"proibido",
|
|
29
|
+
"jamais",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
/** Conjunto normalizado (usado para remover polaridade das keywords). */
|
|
33
|
+
export const POLARITY_WORDS: ReadonlySet<string> = new Set([
|
|
34
|
+
...PROMOTE_PREFIXES,
|
|
35
|
+
...AVOID_PREFIXES,
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/** Escapa um literal para uso em regex. */
|
|
39
|
+
function escapeWord(word: string): string {
|
|
40
|
+
return word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Regex ancorada derivada das listas (sem repetir as palavras). */
|
|
44
|
+
function patternFor(words: readonly string[]): RegExp {
|
|
45
|
+
return new RegExp(`^(${words.map(escapeWord).join("|")})\\b`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const PROMOTE_PATTERN = patternFor(PROMOTE_PREFIXES);
|
|
49
|
+
const AVOID_PATTERN = patternFor(AVOID_PREFIXES);
|
|
50
|
+
|
|
51
|
+
/** Polaridade explícita; sem prefixo conhecido, assume `avoid`. */
|
|
52
|
+
export function polarityOf(text: string): RuleIntent {
|
|
53
|
+
const normalized = text.trim().toLowerCase();
|
|
54
|
+
if (PROMOTE_PATTERN.test(normalized)) return "promote";
|
|
55
|
+
if (AVOID_PATTERN.test(normalized)) return "avoid";
|
|
56
|
+
return "avoid";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `true` quando a regra declara polaridade explícita. */
|
|
60
|
+
export function hasExplicitPolarity(text: string): boolean {
|
|
61
|
+
const normalized = text.trim().toLowerCase();
|
|
62
|
+
return PROMOTE_PATTERN.test(normalized) || AVOID_PATTERN.test(normalized);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Prefixos reservados para findings emergentes (fora das regras explícitas). */
|
|
66
|
+
export const EMERGENT_PREFIXES = {
|
|
67
|
+
logic: "Lógica:",
|
|
68
|
+
domain: "Domínio:",
|
|
69
|
+
} as const;
|
|
70
|
+
|
|
71
|
+
/** Espécie de finding emergente. */
|
|
72
|
+
export type EmergentKind = keyof typeof EMERGENT_PREFIXES;
|
|
73
|
+
|
|
74
|
+
/** Espécie de um finding emergente (`null` quando não é emergente). */
|
|
75
|
+
export function emergentKind(rule: string): EmergentKind | null {
|
|
76
|
+
const normalized = rule.trim();
|
|
77
|
+
if (normalized.startsWith(EMERGENT_PREFIXES.logic)) return "logic";
|
|
78
|
+
if (normalized.startsWith(EMERGENT_PREFIXES.domain)) return "domain";
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `true` quando a regra usa um prefixo emergente. */
|
|
83
|
+
export function isEmergentRule(rule: string): boolean {
|
|
84
|
+
return emergentKind(rule) !== null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Texto que sobra após o prefixo (vazio é inválido). */
|
|
88
|
+
export function emergentText(rule: string): string {
|
|
89
|
+
const kind = emergentKind(rule);
|
|
90
|
+
if (!kind) return "";
|
|
91
|
+
return rule.trim().slice(EMERGENT_PREFIXES[kind].length).trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Monta o texto de um finding emergente. */
|
|
95
|
+
export function emergentRule(kind: EmergentKind, text: string): string {
|
|
96
|
+
return `${EMERGENT_PREFIXES[kind]} ${text.trim()}`;
|
|
97
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* pública e os testes; a classe devolve `Classification` (domínio).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { AstFacts } from "../ast/types";
|
|
8
9
|
import type { HoundsmanConfig } from "../config";
|
|
9
10
|
import type { DiffSource } from "../diff-source";
|
|
10
11
|
import { Classification } from "../domain/classification";
|
|
@@ -23,12 +24,18 @@ export class FileClassifier {
|
|
|
23
24
|
classify(
|
|
24
25
|
entries: ReturnType<typeof parseEntries>,
|
|
25
26
|
churn: ChurnMap = {},
|
|
27
|
+
astFacts?: ReadonlyMap<string, AstFacts>,
|
|
26
28
|
): Classification {
|
|
27
|
-
return new Classification(classifyEntries(entries, this.config, churn));
|
|
29
|
+
return new Classification(classifyEntries(entries, this.config, churn, astFacts));
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/** Executa o diff e classifica os arquivos entre `base` e `head`. */
|
|
31
|
-
classifyRange(
|
|
32
|
-
|
|
33
|
+
classifyRange(
|
|
34
|
+
base: string,
|
|
35
|
+
head: string,
|
|
36
|
+
diff?: DiffSource,
|
|
37
|
+
astFacts?: ReadonlyMap<string, AstFacts>,
|
|
38
|
+
): Classification {
|
|
39
|
+
return new Classification(classifyRange(base, head, this.config, diff, astFacts));
|
|
33
40
|
}
|
|
34
41
|
}
|
|
@@ -10,29 +10,45 @@ export type GhResult = { ok: boolean; code: number; stdout: string; stderr: stri
|
|
|
10
10
|
/** Executa o `gh` e devolve o resultado cru (sem lançar). */
|
|
11
11
|
export type GhRunner = (args: string[]) => GhResult;
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
stdout: "pipe",
|
|
18
|
-
stderr: "pipe",
|
|
19
|
-
});
|
|
20
|
-
const code = result.exitCode ?? 1;
|
|
21
|
-
return {
|
|
22
|
-
ok: code === 0,
|
|
23
|
-
code,
|
|
24
|
-
stdout: new TextDecoder().decode(result.stdout).trim(),
|
|
25
|
-
stderr: new TextDecoder().decode(result.stderr).trim(),
|
|
26
|
-
};
|
|
27
|
-
} catch (error) {
|
|
28
|
-
return {
|
|
29
|
-
ok: false,
|
|
30
|
-
code: 127,
|
|
31
|
-
stdout: "",
|
|
32
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
33
|
-
};
|
|
13
|
+
/** Mensagem curta para um erro do `gh` (sem expor o stderr inteiro). */
|
|
14
|
+
export function describeGhError(result: GhResult): string {
|
|
15
|
+
if (result.code === 127 || /not found|ENOENT/i.test(result.stderr)) {
|
|
16
|
+
return "gh não instalado.";
|
|
34
17
|
}
|
|
35
|
-
|
|
18
|
+
const last = result.stderr.split("\n").filter(Boolean).pop() ?? "";
|
|
19
|
+
if (/auth|login|logged/i.test(last)) return `gh sem autenticação (${last}).`;
|
|
20
|
+
return last || `gh saiu com código ${result.code}.`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Cria um runner do `gh` com diretório de trabalho fixo. */
|
|
24
|
+
export function createGhRunner(cwd?: string): GhRunner {
|
|
25
|
+
return (args) => {
|
|
26
|
+
try {
|
|
27
|
+
const result = Bun.spawnSync(["gh", ...args], {
|
|
28
|
+
cwd,
|
|
29
|
+
stdout: "pipe",
|
|
30
|
+
stderr: "pipe",
|
|
31
|
+
});
|
|
32
|
+
const code = result.exitCode ?? 1;
|
|
33
|
+
return {
|
|
34
|
+
ok: code === 0,
|
|
35
|
+
code,
|
|
36
|
+
stdout: new TextDecoder().decode(result.stdout).trim(),
|
|
37
|
+
stderr: new TextDecoder().decode(result.stderr).trim(),
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
code: 127,
|
|
43
|
+
stdout: "",
|
|
44
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Runner padrão: `gh` a partir do cwd; ausência do binário → `ok:false`. */
|
|
51
|
+
export const defaultGhRunner: GhRunner = createGhRunner();
|
|
36
52
|
|
|
37
53
|
/** Superfície OO sobre o runner injetável. */
|
|
38
54
|
export class GhClient {
|
|
@@ -12,10 +12,11 @@ import type {
|
|
|
12
12
|
FindingsStateFile,
|
|
13
13
|
} from "../findings-state";
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
applyConfidence,
|
|
16
16
|
applyResolutions,
|
|
17
17
|
selectVerifiable,
|
|
18
18
|
summarizeVerify,
|
|
19
|
+
type VerifyBasis,
|
|
19
20
|
type VerifyEntry,
|
|
20
21
|
type VerifySummary,
|
|
21
22
|
verifyLocal,
|
|
@@ -33,13 +34,23 @@ export class Verifier {
|
|
|
33
34
|
return verifyLocal(findings, readFile);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/** Tier 1 — aplica
|
|
37
|
+
/** Tier 1 — aplica confirmações externas (Jev ou IA) aos casos ambíguos. */
|
|
38
|
+
applyConfidence(
|
|
39
|
+
entries: VerifyEntry[],
|
|
40
|
+
confidence: Map<string, number>,
|
|
41
|
+
threshold: number,
|
|
42
|
+
basis: VerifyBasis = "jev",
|
|
43
|
+
): number {
|
|
44
|
+
return applyConfidence(entries, confidence, threshold, basis);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Compatibilidade: confirmação via Jev. */
|
|
37
48
|
applyJev(
|
|
38
49
|
entries: VerifyEntry[],
|
|
39
50
|
confidence: Map<string, number>,
|
|
40
51
|
threshold: number,
|
|
41
52
|
): number {
|
|
42
|
-
return
|
|
53
|
+
return applyConfidence(entries, confidence, threshold, "jev");
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
/** Contagem por status. */
|
|
@@ -9,17 +9,18 @@
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join, resolve } from "node:path";
|
|
11
11
|
import type { HoundsmanConfig } from "../config";
|
|
12
|
+
import { ENV_NAMES, env } from "../env";
|
|
12
13
|
|
|
13
|
-
export const HOUNDSMAN_HOME_ENV =
|
|
14
|
+
export const HOUNDSMAN_HOME_ENV = ENV_NAMES.home;
|
|
14
15
|
|
|
15
16
|
/** Diretório-base global do houndsman. */
|
|
16
17
|
export function houndsmanHome(): string {
|
|
17
|
-
const override =
|
|
18
|
-
if (override
|
|
18
|
+
const override = env.home;
|
|
19
|
+
if (override) return resolve(override);
|
|
19
20
|
|
|
20
21
|
const home = homedir();
|
|
21
22
|
if (process.platform === "win32") {
|
|
22
|
-
const local =
|
|
23
|
+
const local = env.localAppData;
|
|
23
24
|
return join(
|
|
24
25
|
local && local.length > 0 ? local : join(home, "AppData", "Local"),
|
|
25
26
|
"houndsman",
|
|
@@ -28,7 +29,7 @@ export function houndsmanHome(): string {
|
|
|
28
29
|
if (process.platform === "darwin") {
|
|
29
30
|
return join(home, "Library", "Application Support", "houndsman");
|
|
30
31
|
}
|
|
31
|
-
const xdg =
|
|
32
|
+
const xdg = env.xdgDataHome;
|
|
32
33
|
return join(xdg && xdg.length > 0 ? xdg : join(home, ".local", "share"), "houndsman");
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -106,12 +106,24 @@ export class ReviewWorkArea {
|
|
|
106
106
|
writeFileSync(this.briefFile(id), content);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/** Grava o relatório JSON de um watchdog (staging dos subagentes). */
|
|
110
|
+
writeFinding(id: string, content: string): void {
|
|
111
|
+
mkdirSync(this.findingsDir, { recursive: true });
|
|
112
|
+
writeFileSync(this.findingFile(id), content);
|
|
113
|
+
}
|
|
114
|
+
|
|
109
115
|
writeContext(context: unknown, brief: string): void {
|
|
110
116
|
mkdirSync(this.root, { recursive: true });
|
|
111
117
|
writeFileSync(this.contextPaths.context, `${JSON.stringify(context, null, 2)}\n`);
|
|
112
118
|
writeFileSync(this.contextPaths.brief, `${brief}\n`);
|
|
113
119
|
}
|
|
114
120
|
|
|
121
|
+
/** Grava o resumo do contexto (gerado pelo subagente ou pela IA). */
|
|
122
|
+
writeContextSummary(summary: unknown): void {
|
|
123
|
+
mkdirSync(this.root, { recursive: true });
|
|
124
|
+
writeFileSync(this.contextPaths.summary, `${JSON.stringify(summary, null, 2)}\n`);
|
|
125
|
+
}
|
|
126
|
+
|
|
115
127
|
/** Finds brutos gravados pelos subagentes (um JSON por watchdog). */
|
|
116
128
|
readFindings(activeIds: string[]): FindingsIngest {
|
|
117
129
|
const reports: WatchdogReport[] = [];
|
package/src/core/types.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* `Rule` e `WatchdogMeta` descrevem as regras consumidas pela renderização.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { BriefPlan } from "./ai/schemas";
|
|
10
|
+
import type { AstFileBrief } from "./ast/types";
|
|
9
11
|
import type { JevRuleSpec } from "./jev/jev-types";
|
|
10
12
|
|
|
11
13
|
/** Severidade de um achado. `blocking` bloqueia o merge. */
|
|
@@ -27,6 +29,19 @@ export const SEVERITY_EMOJI: Record<Severity, string> = {
|
|
|
27
29
|
nit: "🔎",
|
|
28
30
|
};
|
|
29
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Sinais opcionais de uma regra: ajudam o `RuleAnalyzer` a transformar texto
|
|
34
|
+
* em checagens concretas (símbolos, imports, decorators, paths e tools).
|
|
35
|
+
*/
|
|
36
|
+
export type RuleHints = {
|
|
37
|
+
symbols?: string[];
|
|
38
|
+
imports?: string[];
|
|
39
|
+
decorators?: string[];
|
|
40
|
+
paths?: string[];
|
|
41
|
+
/** Nomes de tools sugeridos (ex.: `symbol_context`). */
|
|
42
|
+
checks?: string[];
|
|
43
|
+
};
|
|
44
|
+
|
|
30
45
|
/**
|
|
31
46
|
* Regra do watchdog: severidade + texto. O texto declara a polaridade de forma
|
|
32
47
|
* explícita — `Promover:` (algo que DEVE acontecer) ou `Evitar:`/`Não`/`Nunca`
|
|
@@ -39,6 +54,7 @@ export type Rule = {
|
|
|
39
54
|
severity: Severity;
|
|
40
55
|
text: string;
|
|
41
56
|
jev?: JevRuleSpec;
|
|
57
|
+
hints?: RuleHints;
|
|
42
58
|
};
|
|
43
59
|
|
|
44
60
|
/**
|
|
@@ -48,9 +64,15 @@ export type Rule = {
|
|
|
48
64
|
*
|
|
49
65
|
* - `filename` (padrão) → padrão aplicado ao caminho do arquivo.
|
|
50
66
|
* - `content` → padrão aplicado ao conteúdo do arquivo.
|
|
67
|
+
* - `symbol` → padrão aplicado aos nomes de símbolos (AST).
|
|
68
|
+
* - `import` → padrão aplicado às fontes/nomes de imports (AST).
|
|
69
|
+
* - `export` → padrão aplicado aos nomes exportados (AST).
|
|
70
|
+
* - `decorator` → padrão aplicado aos decorators (AST).
|
|
71
|
+
*
|
|
72
|
+
* Regras AST exigem grammar disponível; sem ela, a regra simplesmente não casa.
|
|
51
73
|
*/
|
|
52
74
|
export type ClassifyRule = {
|
|
53
|
-
type?: "filename" | "content";
|
|
75
|
+
type?: "filename" | "content" | "symbol" | "import" | "export" | "decorator";
|
|
54
76
|
value: string;
|
|
55
77
|
};
|
|
56
78
|
|
|
@@ -116,6 +138,10 @@ export type WatchdogRenderArgs = {
|
|
|
116
138
|
docs: readonly string[];
|
|
117
139
|
context?: WatchdogRenderContext | null;
|
|
118
140
|
candidates?: readonly WatchdogRenderCandidate[];
|
|
141
|
+
/** Estrutura/delta dos arquivos selecionados (AST). */
|
|
142
|
+
ast?: readonly AstFileBrief[];
|
|
143
|
+
/** Plano de revisão sugerido por IA (nunca substitui as regras). */
|
|
144
|
+
aiPlan?: BriefPlan;
|
|
119
145
|
};
|
|
120
146
|
|
|
121
147
|
/** Definição completa de um watchdog — 1 por arquivo em `project/watchdogs/`. */
|
|
@@ -126,6 +152,16 @@ export type WatchdogDefinition = WatchdogMeta & {
|
|
|
126
152
|
description: string;
|
|
127
153
|
/** Watchdog deve checar breaking change em arquivos deletados do seu domínio. */
|
|
128
154
|
breakingChange?: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Watchdog de último recurso: só assume arquivos que nenhum outro casou.
|
|
157
|
+
* Útil para padrões gerais/lógica; `classify` é ignorado.
|
|
158
|
+
*/
|
|
159
|
+
fallback?: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Permite findings emergentes (`Lógica:`/`Domínio:`) fora das regras
|
|
162
|
+
* explícitas. Exige evidência e severidade máxima `attention`.
|
|
163
|
+
*/
|
|
164
|
+
allowEmergentFindings?: boolean;
|
|
129
165
|
/** Prioridade de arquivos/paths (ex-`O Que Revisar`). Opcional. */
|
|
130
166
|
reviewFocus?: readonly string[];
|
|
131
167
|
/** O que NUNCA carregar/enviar (ex-`NÃO carregar`). Opcional. */
|