@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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planejador determinístico da investigação.
|
|
3
|
+
*
|
|
4
|
+
* Converte `RuleAnalysis[]` em tarefas priorizadas (severidade + confiança +
|
|
5
|
+
* impacto). Uma estratégia diferente pode ser injetada sem tocar no agente.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { severityRank } from "../../core/types";
|
|
9
|
+
import type { ToolCapability } from "../tools/agent-tool";
|
|
10
|
+
import type {
|
|
11
|
+
InvestigationPlan,
|
|
12
|
+
InvestigationStrategy,
|
|
13
|
+
InvestigationTask,
|
|
14
|
+
} from "./investigation-task";
|
|
15
|
+
import type { RuleAnalysis } from "./rule-analyzer";
|
|
16
|
+
|
|
17
|
+
/** Contexto mínimo do planejamento. */
|
|
18
|
+
export type PlannerContext = {
|
|
19
|
+
watchdogId: string;
|
|
20
|
+
files: readonly string[];
|
|
21
|
+
symbols: readonly string[];
|
|
22
|
+
/** Arquivos impactados (reverse deps) do diff. */
|
|
23
|
+
impacted?: readonly string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Planejador default. */
|
|
27
|
+
export class InvestigationPlanner implements InvestigationStrategy {
|
|
28
|
+
plan(context: PlannerContext, rules: readonly RuleAnalysis[]): InvestigationPlan {
|
|
29
|
+
const tasks: InvestigationTask[] = [];
|
|
30
|
+
const allFiles = [...new Set([...context.files, ...(context.impacted ?? [])])];
|
|
31
|
+
let estimatedToolCalls = 0;
|
|
32
|
+
|
|
33
|
+
rules.forEach((analysis, index) => {
|
|
34
|
+
const symbols = analysis.signals
|
|
35
|
+
.filter((signal) => signal.kind === "symbol")
|
|
36
|
+
.map((signal) => signal.value);
|
|
37
|
+
const matchedFiles = filesFor(analysis, context.files);
|
|
38
|
+
const files = matchedFiles.length > 0 ? matchedFiles : [...context.files];
|
|
39
|
+
const capabilities = capabilitiesFor(analysis);
|
|
40
|
+
estimatedToolCalls +=
|
|
41
|
+
analysis.checks.length + (capabilities.length > 0 ? 1 : 0);
|
|
42
|
+
tasks.push({
|
|
43
|
+
id: `rule-${index}`,
|
|
44
|
+
rule: analysis.rule,
|
|
45
|
+
severity: analysis.severity,
|
|
46
|
+
kind: "rule-evidence",
|
|
47
|
+
files,
|
|
48
|
+
symbols: [...new Set([...symbols, ...context.symbols])].slice(0, 10),
|
|
49
|
+
capabilities,
|
|
50
|
+
priority: priorityOf(analysis, index),
|
|
51
|
+
status: "pending",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (context.impacted && context.impacted.length > 0) {
|
|
56
|
+
tasks.push({
|
|
57
|
+
id: "impact",
|
|
58
|
+
rule: "impacto das alterações",
|
|
59
|
+
severity: "attention",
|
|
60
|
+
kind: "symbol-impact",
|
|
61
|
+
files: [...context.impacted],
|
|
62
|
+
symbols: [...context.symbols],
|
|
63
|
+
capabilities: ["impact", "references"],
|
|
64
|
+
priority: 5,
|
|
65
|
+
status: "pending",
|
|
66
|
+
});
|
|
67
|
+
estimatedToolCalls += 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
tasks.sort((a, b) => a.priority - b.priority);
|
|
71
|
+
return { tasks, totalFiles: allFiles.length, estimatedToolCalls };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Arquivos da tarefa filtrados por `hints.paths` (regex) quando presentes. */
|
|
76
|
+
function filesFor(analysis: RuleAnalysis, files: readonly string[]): string[] {
|
|
77
|
+
const patterns = analysis.signals
|
|
78
|
+
.filter((signal) => signal.kind === "path")
|
|
79
|
+
.map((signal) => signal.value);
|
|
80
|
+
if (patterns.length === 0) return [...files];
|
|
81
|
+
const matchers = patterns.flatMap((pattern) => {
|
|
82
|
+
try {
|
|
83
|
+
const regex = new RegExp(pattern);
|
|
84
|
+
return [(file: string) => regex.test(file)];
|
|
85
|
+
} catch {
|
|
86
|
+
return [(file: string) => file.includes(pattern)];
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return files.filter((file) => matchers.some((match) => match(file)));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Capabilities sugeridas pela categoria/sinais da regra. */
|
|
93
|
+
function capabilitiesFor(analysis: RuleAnalysis): ToolCapability[] {
|
|
94
|
+
const capabilities = new Set<ToolCapability>(["read", "search"]);
|
|
95
|
+
const kinds = new Set(analysis.signals.map((signal) => signal.kind));
|
|
96
|
+
if (kinds.has("symbol")) {
|
|
97
|
+
capabilities.add("symbol");
|
|
98
|
+
capabilities.add("references");
|
|
99
|
+
}
|
|
100
|
+
if (kinds.has("import") || kinds.has("path")) capabilities.add("search");
|
|
101
|
+
if (analysis.category === "architecture" || analysis.category === "data") {
|
|
102
|
+
capabilities.add("impact");
|
|
103
|
+
}
|
|
104
|
+
if (analysis.category === "tests") capabilities.add("tests");
|
|
105
|
+
if (analysis.category === "security") capabilities.add("references");
|
|
106
|
+
for (const check of analysis.checks) {
|
|
107
|
+
if (check.tool === "symbol_context") capabilities.add("symbol");
|
|
108
|
+
if (check.tool === "find_references") capabilities.add("references");
|
|
109
|
+
}
|
|
110
|
+
return [...capabilities];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Prioridade (menor = investigar antes). */
|
|
114
|
+
function priorityOf(analysis: RuleAnalysis, index: number): number {
|
|
115
|
+
const confidencePenalty =
|
|
116
|
+
analysis.confidence === "high" ? 0 : analysis.confidence === "medium" ? 2 : 4;
|
|
117
|
+
return severityRank(analysis.severity) * 10 + confidencePenalty + index * 0.01;
|
|
118
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diário da investigação (`harness-transcript.json`).
|
|
3
|
+
*
|
|
4
|
+
* Registra estados, chamadas de tool, evidências, decisões do crítico e
|
|
5
|
+
* truncamentos — sem prompt, resposta ou segredo.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ToolResult } from "../tools/tool-result";
|
|
9
|
+
|
|
10
|
+
/** Registro de uma chamada de tool. */
|
|
11
|
+
export type ToolCallRecord = {
|
|
12
|
+
tool: string;
|
|
13
|
+
state: string;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
ok: boolean;
|
|
16
|
+
truncated: boolean;
|
|
17
|
+
error?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Transição de estado do agente. */
|
|
21
|
+
export type StateTransition = {
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
at: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Diário de um agente. */
|
|
28
|
+
export class ReviewJournal {
|
|
29
|
+
readonly toolCalls: ToolCallRecord[] = [];
|
|
30
|
+
readonly transitions: StateTransition[] = [];
|
|
31
|
+
readonly notes: string[] = [];
|
|
32
|
+
private readonly startedAt = Date.now();
|
|
33
|
+
|
|
34
|
+
/** Registra uma chamada concluída. */
|
|
35
|
+
recordToolCall(
|
|
36
|
+
tool: string,
|
|
37
|
+
state: string,
|
|
38
|
+
durationMs: number,
|
|
39
|
+
result: ToolResult<unknown>,
|
|
40
|
+
): void {
|
|
41
|
+
this.toolCalls.push({
|
|
42
|
+
tool,
|
|
43
|
+
state,
|
|
44
|
+
durationMs: Math.round(durationMs),
|
|
45
|
+
ok: result.ok,
|
|
46
|
+
truncated: result.truncated === true,
|
|
47
|
+
...(result.error ? { error: result.error } : {}),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Registra uma transição de estado. */
|
|
52
|
+
transition(from: string, to: string): void {
|
|
53
|
+
this.transitions.push({ from, to, at: new Date().toISOString() });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Nota livre (hipótese, decisão, motivo de skip). */
|
|
57
|
+
note(message: string): void {
|
|
58
|
+
this.notes.push(message);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Métricas agregadas por tool. */
|
|
62
|
+
metrics(): Record<string, { calls: number; errors: number; durationMs: number }> {
|
|
63
|
+
const metrics: Record<
|
|
64
|
+
string,
|
|
65
|
+
{ calls: number; errors: number; durationMs: number }
|
|
66
|
+
> = {};
|
|
67
|
+
for (const call of this.toolCalls) {
|
|
68
|
+
const entry = metrics[call.tool] ?? { calls: 0, errors: 0, durationMs: 0 };
|
|
69
|
+
entry.calls++;
|
|
70
|
+
if (!call.ok) entry.errors++;
|
|
71
|
+
entry.durationMs += call.durationMs;
|
|
72
|
+
metrics[call.tool] = entry;
|
|
73
|
+
}
|
|
74
|
+
return metrics;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Snapshot serializável. */
|
|
78
|
+
toJSON(): Record<string, unknown> {
|
|
79
|
+
return {
|
|
80
|
+
elapsedMs: Date.now() - this.startedAt,
|
|
81
|
+
toolCalls: this.toolCalls,
|
|
82
|
+
transitions: this.transitions,
|
|
83
|
+
notes: this.notes,
|
|
84
|
+
metrics: this.metrics(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Análise determinística de regras (`RuleAnalyzer`).
|
|
3
|
+
*
|
|
4
|
+
* Transforma uma regra textual em polaridade, categoria, keywords, sinais e
|
|
5
|
+
* checks. É a base do planner e do `rule_signals` tool. Um analyzer por IA pode
|
|
6
|
+
* complementar regras ambíguas, mas o resultado continua validado.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { POLARITY_WORDS, polarityOf, type RuleIntent } from "../../core/rule-text";
|
|
10
|
+
import type { Rule, Severity } from "../../core/types";
|
|
11
|
+
import {
|
|
12
|
+
DefaultRuleCheckFactory,
|
|
13
|
+
HeuristicRuleSignalExtractor,
|
|
14
|
+
type RuleCheck,
|
|
15
|
+
type RuleCheckFactory,
|
|
16
|
+
type RuleSignal,
|
|
17
|
+
type RuleSignalExtractor,
|
|
18
|
+
} from "./rule-signals";
|
|
19
|
+
|
|
20
|
+
export { polarityOf, type RuleIntent } from "../../core/rule-text";
|
|
21
|
+
|
|
22
|
+
/** Categoria da regra. */
|
|
23
|
+
export type RuleCategory =
|
|
24
|
+
| "architecture"
|
|
25
|
+
| "security"
|
|
26
|
+
| "data"
|
|
27
|
+
| "tests"
|
|
28
|
+
| "performance"
|
|
29
|
+
| "style"
|
|
30
|
+
| "other";
|
|
31
|
+
|
|
32
|
+
/** Regra analisada (entrada do planner). */
|
|
33
|
+
export type RuleAnalysis = {
|
|
34
|
+
rule: string;
|
|
35
|
+
severity: Severity;
|
|
36
|
+
intent: RuleIntent;
|
|
37
|
+
category: RuleCategory;
|
|
38
|
+
keywords: string[];
|
|
39
|
+
signals: RuleSignal[];
|
|
40
|
+
checks: RuleCheck[];
|
|
41
|
+
confidence: "high" | "medium" | "low";
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const GENERIC_STOPWORDS = [
|
|
45
|
+
"para",
|
|
46
|
+
"pelo",
|
|
47
|
+
"pela",
|
|
48
|
+
"como",
|
|
49
|
+
"ser",
|
|
50
|
+
"que",
|
|
51
|
+
"com",
|
|
52
|
+
"sem",
|
|
53
|
+
"uma",
|
|
54
|
+
"dos",
|
|
55
|
+
"das",
|
|
56
|
+
"nos",
|
|
57
|
+
"nas",
|
|
58
|
+
"por",
|
|
59
|
+
"usar",
|
|
60
|
+
"todo",
|
|
61
|
+
"toda",
|
|
62
|
+
"quando",
|
|
63
|
+
"onde",
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
/** Stopwords + palavras de polaridade (fonte única em `core/rule-text`). */
|
|
67
|
+
const STOPWORDS = new Set<string>([...GENERIC_STOPWORDS, ...POLARITY_WORDS]);
|
|
68
|
+
|
|
69
|
+
const CATEGORY_HINTS: readonly [RuleCategory, readonly string[]][] = [
|
|
70
|
+
[
|
|
71
|
+
"security",
|
|
72
|
+
[
|
|
73
|
+
"segredo",
|
|
74
|
+
"senha",
|
|
75
|
+
"token",
|
|
76
|
+
"auth",
|
|
77
|
+
"sanit",
|
|
78
|
+
"injection",
|
|
79
|
+
"sql",
|
|
80
|
+
"cripto",
|
|
81
|
+
"permiss",
|
|
82
|
+
],
|
|
83
|
+
],
|
|
84
|
+
["data", ["banco", "database", "db", "query", "migration", "schema", "transa"]],
|
|
85
|
+
["tests", ["teste", "test", "spec", "cobertura", "mock", "fixture"]],
|
|
86
|
+
[
|
|
87
|
+
"performance",
|
|
88
|
+
["cache", "performance", "latência", "latencia", "n+1", "índice", "indice"],
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
"architecture",
|
|
92
|
+
[
|
|
93
|
+
"camada",
|
|
94
|
+
"arquitetura",
|
|
95
|
+
"rota",
|
|
96
|
+
"controller",
|
|
97
|
+
"service",
|
|
98
|
+
"domínio",
|
|
99
|
+
"dominio",
|
|
100
|
+
"depend",
|
|
101
|
+
],
|
|
102
|
+
],
|
|
103
|
+
["style", ["nome", "formata", "lint", "coment", "estilo", "padrão", "padrao"]],
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
/** Analisador determinístico. */
|
|
107
|
+
export class RuleAnalyzer {
|
|
108
|
+
private readonly extractor: RuleSignalExtractor;
|
|
109
|
+
private readonly checks: RuleCheckFactory;
|
|
110
|
+
|
|
111
|
+
constructor(
|
|
112
|
+
extractor: RuleSignalExtractor = new HeuristicRuleSignalExtractor(),
|
|
113
|
+
checks: RuleCheckFactory = new DefaultRuleCheckFactory(),
|
|
114
|
+
) {
|
|
115
|
+
this.extractor = extractor;
|
|
116
|
+
this.checks = checks;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Analisa todas as regras (ordem estável). */
|
|
120
|
+
analyze(rules: readonly Rule[]): RuleAnalysis[] {
|
|
121
|
+
return rules.map((rule) => this.analyzeRule(rule));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Analisa uma regra. */
|
|
125
|
+
analyzeRule(rule: Rule): RuleAnalysis {
|
|
126
|
+
const intent = polarityOf(rule.text);
|
|
127
|
+
const signals = this.extractor.extract(rule);
|
|
128
|
+
const category = categoryOf(rule.text, signals);
|
|
129
|
+
const confidence = confidenceOf(rule, signals);
|
|
130
|
+
return {
|
|
131
|
+
rule: rule.text,
|
|
132
|
+
severity: rule.severity,
|
|
133
|
+
intent,
|
|
134
|
+
category,
|
|
135
|
+
keywords: keywordsOf(rule.text),
|
|
136
|
+
signals,
|
|
137
|
+
checks: this.checks.create(rule, signals),
|
|
138
|
+
confidence,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Categoria inferida por keywords e sinais. */
|
|
144
|
+
export function categoryOf(text: string, signals: readonly RuleSignal[]): RuleCategory {
|
|
145
|
+
const haystack =
|
|
146
|
+
`${text} ${signals.map((signal) => signal.value).join(" ")}`.toLowerCase();
|
|
147
|
+
for (const [category, hints] of CATEGORY_HINTS) {
|
|
148
|
+
if (hints.some((hint) => haystack.includes(hint))) return category;
|
|
149
|
+
}
|
|
150
|
+
return "other";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Keywords normalizadas (sem stopwords, limitadas). */
|
|
154
|
+
export function keywordsOf(text: string): string[] {
|
|
155
|
+
const words = text
|
|
156
|
+
.toLowerCase()
|
|
157
|
+
.replace(/[^\p{L}\p{N}+\-/]+/gu, " ")
|
|
158
|
+
.split(/\s+/)
|
|
159
|
+
.filter((word) => word.length >= 4 && !STOPWORDS.has(word));
|
|
160
|
+
return [...new Set(words)].slice(0, 12);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Confiança da análise (hints explícitos valem mais que heurística). */
|
|
164
|
+
function confidenceOf(
|
|
165
|
+
rule: Rule,
|
|
166
|
+
signals: readonly RuleSignal[],
|
|
167
|
+
): RuleAnalysis["confidence"] {
|
|
168
|
+
const hints = rule.hints;
|
|
169
|
+
const hintCount =
|
|
170
|
+
(hints?.symbols?.length ?? 0) +
|
|
171
|
+
(hints?.imports?.length ?? 0) +
|
|
172
|
+
(hints?.decorators?.length ?? 0) +
|
|
173
|
+
(hints?.paths?.length ?? 0) +
|
|
174
|
+
(hints?.checks?.length ?? 0);
|
|
175
|
+
if (hintCount > 0) return "high";
|
|
176
|
+
if (signals.length > 0) return "medium";
|
|
177
|
+
return "low";
|
|
178
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extração determinística de sinais de uma regra em linguagem natural.
|
|
3
|
+
*
|
|
4
|
+
* Cada extrator é substituível (`RuleSignalExtractor`); o default usa `hints`
|
|
5
|
+
* + termos em backticks + identificadores. Nunca chama IA.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Rule } from "../../core/types";
|
|
9
|
+
|
|
10
|
+
/** Sinal concreto sugerido por uma regra. */
|
|
11
|
+
export type RuleSignal = {
|
|
12
|
+
kind: "symbol" | "import" | "decorator" | "path" | "grep" | "ast";
|
|
13
|
+
value: string;
|
|
14
|
+
rationale?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Contrato de um extrator de sinais. */
|
|
18
|
+
export interface RuleSignalExtractor {
|
|
19
|
+
extract(rule: Rule): RuleSignal[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Extrai sinais dos `hints`, backticks e identificadores em CamelCase. */
|
|
23
|
+
export class HeuristicRuleSignalExtractor implements RuleSignalExtractor {
|
|
24
|
+
extract(rule: Rule): RuleSignal[] {
|
|
25
|
+
const signals: RuleSignal[] = [];
|
|
26
|
+
const seen = new Set<string>();
|
|
27
|
+
const push = (kind: RuleSignal["kind"], value: string, rationale?: string) => {
|
|
28
|
+
const key = `${kind}:${value}`;
|
|
29
|
+
if (!value.trim() || seen.has(key)) return;
|
|
30
|
+
seen.add(key);
|
|
31
|
+
signals.push({ kind, value, ...(rationale ? { rationale } : {}) });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const hints = rule.hints ?? {};
|
|
35
|
+
for (const item of hints.symbols ?? []) push("symbol", item, "hint");
|
|
36
|
+
for (const item of hints.imports ?? []) push("import", item, "hint");
|
|
37
|
+
for (const item of hints.decorators ?? []) push("decorator", item, "hint");
|
|
38
|
+
for (const item of hints.paths ?? []) push("path", item, "hint");
|
|
39
|
+
|
|
40
|
+
for (const match of rule.text.matchAll(/`([^`]{2,80})`/g)) {
|
|
41
|
+
const value = match[1]?.trim();
|
|
42
|
+
if (value) push(classifyLiteral(value), value, "texto da regra");
|
|
43
|
+
}
|
|
44
|
+
for (const match of rule.text.matchAll(/\b([A-Z][a-zA-Z0-9]{2,})\b/g)) {
|
|
45
|
+
push("symbol", match[1] ?? "", "identificador");
|
|
46
|
+
}
|
|
47
|
+
return signals;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Classifica um literal em backticks (import, path, símbolo). */
|
|
52
|
+
function classifyLiteral(value: string): RuleSignal["kind"] {
|
|
53
|
+
if (value.startsWith("@") || value.includes("/")) {
|
|
54
|
+
return value.includes("/") && !value.includes("@") ? "path" : "import";
|
|
55
|
+
}
|
|
56
|
+
if (/^[A-Z][a-zA-Z0-9]*$/.test(value)) return "symbol";
|
|
57
|
+
return "grep";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Gera checks concretos para uma regra. */
|
|
61
|
+
export interface RuleCheckFactory {
|
|
62
|
+
create(rule: Rule, signals: readonly RuleSignal[]): RuleCheck[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Check que prova/refuta uma regra usando uma tool. */
|
|
66
|
+
export type RuleCheck = {
|
|
67
|
+
id: string;
|
|
68
|
+
description: string;
|
|
69
|
+
tool: string;
|
|
70
|
+
input: unknown;
|
|
71
|
+
required: boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Checks default por presença de sinais. */
|
|
75
|
+
export class DefaultRuleCheckFactory implements RuleCheckFactory {
|
|
76
|
+
create(rule: Rule, signals: readonly RuleSignal[]): RuleCheck[] {
|
|
77
|
+
const checks: RuleCheck[] = [];
|
|
78
|
+
const explicit = rule.hints?.checks ?? [];
|
|
79
|
+
explicit.forEach((tool, index) => {
|
|
80
|
+
checks.push({
|
|
81
|
+
id: `${index}:${tool}`,
|
|
82
|
+
description: `Executar ${tool} para a regra`,
|
|
83
|
+
tool,
|
|
84
|
+
input: {},
|
|
85
|
+
required: true,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
const symbols = signals.filter((signal) => signal.kind === "symbol");
|
|
89
|
+
if (symbols.length > 0 && explicit.length === 0) {
|
|
90
|
+
checks.push({
|
|
91
|
+
id: "symbol-context",
|
|
92
|
+
description: `Inspecionar o contexto de ${symbols[0]?.value}`,
|
|
93
|
+
tool: "symbol_context",
|
|
94
|
+
input: { symbol: symbols[0]?.value },
|
|
95
|
+
required: false,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const imports = signals.filter((signal) => signal.kind === "import");
|
|
99
|
+
if (imports.length > 0) {
|
|
100
|
+
checks.push({
|
|
101
|
+
id: "find-references",
|
|
102
|
+
description: `Procurar usos de ${imports[0]?.value}`,
|
|
103
|
+
tool: "find_references",
|
|
104
|
+
input: { symbol: imports[0]?.value },
|
|
105
|
+
required: false,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return checks;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schemas das saídas intermediárias da máquina de estados.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { SeveritySchema } from "../../core/ai/schemas";
|
|
7
|
+
|
|
8
|
+
/** ORIENT: hipóteses iniciais. */
|
|
9
|
+
export const OrientOutputSchema = z.object({
|
|
10
|
+
hypotheses: z.array(z.string()).max(8).default([]),
|
|
11
|
+
});
|
|
12
|
+
export type OrientOutput = z.infer<typeof OrientOutputSchema>;
|
|
13
|
+
|
|
14
|
+
/** PLAN: tarefas escolhidas do plano determinístico. */
|
|
15
|
+
export const PlanOutputSchema = z.object({
|
|
16
|
+
taskIds: z.array(z.string()).default([]),
|
|
17
|
+
});
|
|
18
|
+
export type PlanOutput = z.infer<typeof PlanOutputSchema>;
|
|
19
|
+
|
|
20
|
+
/** INVESTIGATE: drafts + cobertura. */
|
|
21
|
+
export const FindingDraftSchema = z.object({
|
|
22
|
+
rule: z.string(),
|
|
23
|
+
severity: SeveritySchema,
|
|
24
|
+
file: z.string(),
|
|
25
|
+
line: z.number().int().positive().optional(),
|
|
26
|
+
message: z.string(),
|
|
27
|
+
action: z.string(),
|
|
28
|
+
evidenceIds: z.array(z.string()).default([]),
|
|
29
|
+
confidence: z.number().min(0).max(1).default(0.6),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const CoverageOutputSchema = z.object({
|
|
33
|
+
rule: z.string(),
|
|
34
|
+
status: z.enum(["checked", "not_applicable", "skipped"]),
|
|
35
|
+
reason: z.string().optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const InvestigateOutputSchema = z.object({
|
|
39
|
+
findingDrafts: z.array(FindingDraftSchema).default([]),
|
|
40
|
+
coverage: z.array(CoverageOutputSchema).default([]),
|
|
41
|
+
});
|
|
42
|
+
export type InvestigateOutput = z.infer<typeof InvestigateOutputSchema>;
|
|
43
|
+
|
|
44
|
+
/** VERIFY: decisão do crítico (opcional; determinístico por default). */
|
|
45
|
+
export const CriticOutputSchema = z.object({
|
|
46
|
+
decisions: z
|
|
47
|
+
.array(
|
|
48
|
+
z.object({
|
|
49
|
+
rule: z.string(),
|
|
50
|
+
file: z.string(),
|
|
51
|
+
verdict: z.enum(["keep", "drop", "downgrade"]),
|
|
52
|
+
reason: z.string().optional(),
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
.default([]),
|
|
56
|
+
});
|
|
57
|
+
export type CriticOutput = z.infer<typeof CriticOutputSchema>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System prompt do revisor (harness nativo).
|
|
3
|
+
*
|
|
4
|
+
* Ordem estável: papel, regras, escopo, procedimento, contrato e segurança.
|
|
5
|
+
* Descrições de tools vêm do `ToolRegistry` (nunca hardcoded).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { buildSystem } from "../../core/ai/prompts/base";
|
|
9
|
+
import { OUTPUT_FORMAT_PATH } from "../../core/paths";
|
|
10
|
+
import type { AgentContext } from "../agent/agent-context";
|
|
11
|
+
|
|
12
|
+
/** System prompt do agente. */
|
|
13
|
+
export function buildReviewerSystem(
|
|
14
|
+
context: AgentContext,
|
|
15
|
+
toolDescriptions: string,
|
|
16
|
+
): string {
|
|
17
|
+
const rules = context.watchdog.rules.map(
|
|
18
|
+
(rule) => `- [${rule.severity}] ${rule.text}`,
|
|
19
|
+
);
|
|
20
|
+
return buildSystem(
|
|
21
|
+
`Você é um revisor sênior de ${context.projectName}, especialista no domínio '${context.watchdog.name}'.`,
|
|
22
|
+
[
|
|
23
|
+
"Você investiga o diff com tools read-only e só conclui com evidência.",
|
|
24
|
+
"Nunca invente achado, arquivo, linha ou regra.",
|
|
25
|
+
"Toda regra deve terminar com cobertura (checked, not_applicable ou skipped).",
|
|
26
|
+
"blocking/attention exigem evidência rastreável.",
|
|
27
|
+
context.watchdog.definition.allowEmergentFindings
|
|
28
|
+
? "Findings emergentes de lógica/domínio usam `Lógica:`/`Domínio:`, exigem evidência e severidade máxima attention."
|
|
29
|
+
: "Todo finding deve citar uma regra existente do watchdog.",
|
|
30
|
+
"",
|
|
31
|
+
"## Regras do watchdog",
|
|
32
|
+
...rules,
|
|
33
|
+
"",
|
|
34
|
+
"## Escopo",
|
|
35
|
+
context.watchdog.scope,
|
|
36
|
+
"",
|
|
37
|
+
"## Procedimento",
|
|
38
|
+
"1. ORIENT — entenda regras, diff, AST e impacto.",
|
|
39
|
+
"2. PLAN — escolha as tarefas priorizadas.",
|
|
40
|
+
"3. INVESTIGATE — use tools para coletar evidências; refute hipóteses.",
|
|
41
|
+
"4. VERIFY — cada draft precisa de evidência válida.",
|
|
42
|
+
"5. CONCLUDE — produza o relatório.",
|
|
43
|
+
"",
|
|
44
|
+
"## Tools disponíveis",
|
|
45
|
+
toolDescriptions,
|
|
46
|
+
"",
|
|
47
|
+
"## Contrato de saída",
|
|
48
|
+
`O relatório final segue ${OUTPUT_FORMAT_PATH}.`,
|
|
49
|
+
"Em INVESTIGATE produza JSON com findingDrafts e coverage.",
|
|
50
|
+
],
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Prompt do usuário (contexto compactado; nunca arquivo inteiro). */
|
|
55
|
+
export function buildReviewerUser(context: AgentContext): string {
|
|
56
|
+
const lines = [
|
|
57
|
+
`Branch: ${context.branch}`,
|
|
58
|
+
`Watchdog: ${context.watchdog.name} (${context.watchdog.id})`,
|
|
59
|
+
`Arquivos do domínio (${context.files.length}):`,
|
|
60
|
+
...context.files.slice(0, 40).map((file) => `- ${file}`),
|
|
61
|
+
];
|
|
62
|
+
if (context.overflow.length > 0) {
|
|
63
|
+
lines.push(
|
|
64
|
+
"",
|
|
65
|
+
`Fora do orçamento (${context.overflow.length}) — NÃO ler; registrar em skipped:`,
|
|
66
|
+
...context.overflow.slice(0, 40).map((file) => `- ${file}`),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (context.deleted.length > 0) {
|
|
70
|
+
lines.push("", "Deletados:", ...context.deleted.map((file) => `- ${file}`));
|
|
71
|
+
}
|
|
72
|
+
if (context.context) {
|
|
73
|
+
lines.push("", "## Contexto da PR");
|
|
74
|
+
if (context.context.objective)
|
|
75
|
+
lines.push(`Objetivo: ${context.context.objective}`);
|
|
76
|
+
if (context.context.motivation)
|
|
77
|
+
lines.push(`Motivação: ${context.context.motivation}`);
|
|
78
|
+
for (const item of context.context.changeSummary ?? []) lines.push(`- ${item}`);
|
|
79
|
+
}
|
|
80
|
+
if (context.ast.size > 0) {
|
|
81
|
+
lines.push("", "## Estrutura (AST)");
|
|
82
|
+
for (const [file, analysis] of context.ast) {
|
|
83
|
+
if (!analysis.delta) continue;
|
|
84
|
+
const delta = analysis.delta;
|
|
85
|
+
lines.push(
|
|
86
|
+
`- ${file}: +${delta.addedSymbols.length} −${delta.removedSymbols.length} ~${delta.modifiedSymbols.length}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (context.impact && context.impact.affectedFiles.length > 0) {
|
|
91
|
+
lines.push(
|
|
92
|
+
"",
|
|
93
|
+
`Impacto: ${context.impact.affectedFiles.length} arquivo(s) importam o diff.`,
|
|
94
|
+
...context.impact.affectedFiles.slice(0, 20).map((file) => `- ${file}`),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (context.docs.length > 0) {
|
|
98
|
+
lines.push("", "Docs de contexto:", ...context.docs.map((doc) => `- ${doc}`));
|
|
99
|
+
}
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt do estado CONCLUDE (documentação do contrato).
|
|
3
|
+
*
|
|
4
|
+
* A conclusão do harness é determinística (bits de evidência/coverage); este
|
|
5
|
+
* texto existe para manter o contrato explícito e servir a um futuro modo
|
|
6
|
+
* "conclude por modelo".
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Instruções de conclusão. */
|
|
10
|
+
export function concludePrompt(): string {
|
|
11
|
+
return [
|
|
12
|
+
"Conclua com base apenas nos drafts sobreviventes ao VERIFY.",
|
|
13
|
+
"Não adicione achados novos.",
|
|
14
|
+
"Toda regra sem achado deve constar como coverage checked/not_applicable/skipped.",
|
|
15
|
+
].join("\n");
|
|
16
|
+
}
|