@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,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planejador da review nativa — monta um `AgentContext` por watchdog ativo.
|
|
3
|
+
*
|
|
4
|
+
* Lê a classificação já preparada (prepare) e aplica o orçamento por brief.
|
|
5
|
+
* Nada aqui chama IA.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AstAnalysis, AstImpact } from "../../core/ast/types";
|
|
9
|
+
import type { HoundsmanConfig } from "../../core/config";
|
|
10
|
+
import type { DiffSource } from "../../core/diff-source";
|
|
11
|
+
import { discoverDocs } from "../../core/doc-discoverer";
|
|
12
|
+
import { selectByBudget } from "../../core/domain/budget";
|
|
13
|
+
import type { RunClassification } from "../../core/engine/review-scope";
|
|
14
|
+
import type { ProjectConfig } from "../../core/project-config";
|
|
15
|
+
import type { ContextSummary } from "../../core/report-builder";
|
|
16
|
+
import type { ChurnMap } from "../../core/types";
|
|
17
|
+
import type { AgentContext } from "../agent/agent-context";
|
|
18
|
+
import { RuleAnalyzer } from "../investigation/rule-analyzer";
|
|
19
|
+
|
|
20
|
+
/** Entrada do planejador. */
|
|
21
|
+
export type ReviewPlannerInput = {
|
|
22
|
+
config: HoundsmanConfig;
|
|
23
|
+
project: ProjectConfig;
|
|
24
|
+
classification: RunClassification;
|
|
25
|
+
context: ContextSummary | null;
|
|
26
|
+
ast: ReadonlyMap<string, AstAnalysis>;
|
|
27
|
+
impact?: AstImpact;
|
|
28
|
+
repoRoot: string;
|
|
29
|
+
diff: DiffSource;
|
|
30
|
+
/** Branch alvo da review (prompt do agente). */
|
|
31
|
+
branch: string;
|
|
32
|
+
/** Filtra um único watchdog (`--watchdog`). */
|
|
33
|
+
watchdog?: string;
|
|
34
|
+
maxCalls: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Um watchdog planejado. */
|
|
38
|
+
export type PlannedWatchdog = {
|
|
39
|
+
context: AgentContext;
|
|
40
|
+
files: string[];
|
|
41
|
+
overflow: string[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Planeja os agentes da review. */
|
|
45
|
+
export class ReviewPlanner {
|
|
46
|
+
private readonly input: ReviewPlannerInput;
|
|
47
|
+
|
|
48
|
+
constructor(input: ReviewPlannerInput) {
|
|
49
|
+
this.input = input;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Watchdogs ativos (filtrados) com contexto pronto. */
|
|
53
|
+
plan(): PlannedWatchdog[] {
|
|
54
|
+
const { config, project, classification, watchdog } = this.input;
|
|
55
|
+
const ids = Object.keys(classification.watchdogs).filter(
|
|
56
|
+
(id) => !watchdog || id === watchdog,
|
|
57
|
+
);
|
|
58
|
+
const docs = discoverDocs(Object.keys(classification.files), config);
|
|
59
|
+
const maxFiles = project.maxFilesPerBrief;
|
|
60
|
+
const analyzer = new RuleAnalyzer();
|
|
61
|
+
const planned: PlannedWatchdog[] = [];
|
|
62
|
+
|
|
63
|
+
for (const id of ids) {
|
|
64
|
+
const domain = project.watchdogs.find(id);
|
|
65
|
+
if (!domain) continue;
|
|
66
|
+
const churn: ChurnMap = classification.churn;
|
|
67
|
+
const { selected, overflow } = selectByBudget(
|
|
68
|
+
classification.watchdogs[id] ?? [],
|
|
69
|
+
churn,
|
|
70
|
+
maxFiles,
|
|
71
|
+
);
|
|
72
|
+
const rules = analyzer.analyze(domain.rules);
|
|
73
|
+
planned.push({
|
|
74
|
+
files: selected,
|
|
75
|
+
overflow,
|
|
76
|
+
context: {
|
|
77
|
+
watchdog: domain,
|
|
78
|
+
projectName: project.projectName,
|
|
79
|
+
branch: this.input.branch,
|
|
80
|
+
files: selected,
|
|
81
|
+
overflow,
|
|
82
|
+
deleted: classification.deleted,
|
|
83
|
+
docs,
|
|
84
|
+
context: this.input.context,
|
|
85
|
+
ast: this.input.ast,
|
|
86
|
+
impact: this.input.impact,
|
|
87
|
+
outputPath: "",
|
|
88
|
+
maxSteps: project.aiMaxStepsFor(id),
|
|
89
|
+
maxCalls: this.input.maxCalls,
|
|
90
|
+
repoRoot: this.input.repoRoot,
|
|
91
|
+
diff: this.input.diff,
|
|
92
|
+
rules,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return planned;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel do harness nativo.
|
|
3
|
+
*
|
|
4
|
+
* Atenção: `src/skill/` é o harness **externo** (markdown). Este é o runtime de
|
|
5
|
+
* agentes do próprio houndsman.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type { AgentContext } from "./agent/agent-context";
|
|
9
|
+
export { AgentFactory } from "./agent/agent-factory";
|
|
10
|
+
export type { AgentReporter } from "./agent/agent-reporter";
|
|
11
|
+
export { LoggerReporter, SilentReporter } from "./agent/agent-reporter";
|
|
12
|
+
export type { AgentResult } from "./agent/agent-result";
|
|
13
|
+
export { AgentRunner } from "./agent/agent-runner";
|
|
14
|
+
export { AgentScheduler } from "./agent/agent-scheduler";
|
|
15
|
+
export { ReviewAgent } from "./agent/review-agent";
|
|
16
|
+
export { HarnessConfig, type HarnessOverrides } from "./config/harness-config";
|
|
17
|
+
export { HuntHarness } from "./hunt/hunt-harness";
|
|
18
|
+
export { defaultHuntOptions, type HuntOptions, parseHuntArgs } from "./hunt/hunt-options";
|
|
19
|
+
export type { HuntResult } from "./hunt/hunt-result";
|
|
20
|
+
export { AgentState } from "./investigation/agent-state";
|
|
21
|
+
export type {
|
|
22
|
+
CriticDecision,
|
|
23
|
+
Evidence,
|
|
24
|
+
FindingDraft,
|
|
25
|
+
RuleCoverage,
|
|
26
|
+
} from "./investigation/evidence";
|
|
27
|
+
export { EvidenceVerifier } from "./investigation/evidence-verifier";
|
|
28
|
+
export { FindingCritic } from "./investigation/finding-critic";
|
|
29
|
+
export { InvestigationPlanner } from "./investigation/investigation-planner";
|
|
30
|
+
export { ReviewJournal } from "./investigation/review-journal";
|
|
31
|
+
export { type RuleAnalysis, RuleAnalyzer } from "./investigation/rule-analyzer";
|
|
32
|
+
export type { AgentTool, ToolContext, ToolResult } from "./tools/agent-tool";
|
|
33
|
+
export { type AgentStateName, ToolPolicy } from "./tools/tool-policy";
|
|
34
|
+
export { ToolRegistry } from "./tools/tool-registry";
|
|
35
|
+
export { Toolbox } from "./tools/toolbox";
|
|
36
|
+
export { FindingValidator } from "./validation/finding-validator";
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estado de um agente de investigação.
|
|
3
|
+
*
|
|
4
|
+
* Acumula tarefas, hipóteses, evidências, cobertura de regras, drafts e custo.
|
|
5
|
+
* É a única fonte de verdade que o verificador/crítico consultam.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type Evidence,
|
|
10
|
+
type EvidenceKind,
|
|
11
|
+
evidenceId,
|
|
12
|
+
type FindingDraft,
|
|
13
|
+
type RuleCoverage,
|
|
14
|
+
type RuleCoverageStatus,
|
|
15
|
+
} from "./evidence";
|
|
16
|
+
import type { InvestigationTask } from "./investigation-task";
|
|
17
|
+
|
|
18
|
+
/** Entrada para registrar uma evidência. */
|
|
19
|
+
export type EvidenceInput = Omit<Evidence, "id"> & { id?: string };
|
|
20
|
+
|
|
21
|
+
/** Estado mutável de um agente. */
|
|
22
|
+
export class AgentState {
|
|
23
|
+
readonly tasks: InvestigationTask[] = [];
|
|
24
|
+
readonly hypotheses: string[] = [];
|
|
25
|
+
readonly evidence = new Map<string, Evidence>();
|
|
26
|
+
readonly coverage = new Map<string, RuleCoverage>();
|
|
27
|
+
readonly drafts: FindingDraft[] = [];
|
|
28
|
+
readonly toolCalls = new Map<string, number>();
|
|
29
|
+
private sequence = 0;
|
|
30
|
+
|
|
31
|
+
/** Registra as tarefas do plano. */
|
|
32
|
+
setTasks(tasks: InvestigationTask[]): void {
|
|
33
|
+
this.tasks.push(...tasks);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Tarefas por status. */
|
|
37
|
+
tasksByStatus(status: InvestigationTask["status"]): InvestigationTask[] {
|
|
38
|
+
return this.tasks.filter((task) => task.status === status);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Marca uma tarefa como concluída. */
|
|
42
|
+
completeTask(id: string, reason?: string): void {
|
|
43
|
+
const task = this.tasks.find((item) => item.id === id);
|
|
44
|
+
if (!task) return;
|
|
45
|
+
task.status = "done";
|
|
46
|
+
if (reason) task.reason = reason;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Marca uma tarefa como pulada (com motivo). */
|
|
50
|
+
skipTask(id: string, reason: string): void {
|
|
51
|
+
const task = this.tasks.find((item) => item.id === id);
|
|
52
|
+
if (!task) return;
|
|
53
|
+
task.status = "skipped";
|
|
54
|
+
task.reason = reason;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Adiciona uma hipótese (sem duplicar). */
|
|
58
|
+
addHypothesis(hypothesis: string): void {
|
|
59
|
+
const value = hypothesis.trim();
|
|
60
|
+
if (value && !this.hypotheses.includes(value)) this.hypotheses.push(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Registra evidência e devolve o id gerado. */
|
|
64
|
+
addEvidence(input: EvidenceInput): Evidence {
|
|
65
|
+
const id = input.id?.trim() ? input.id.trim() : evidenceId(++this.sequence);
|
|
66
|
+
const evidence: Evidence = { ...input, id };
|
|
67
|
+
this.evidence.set(id, evidence);
|
|
68
|
+
return evidence;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Evidência por id (undefined se não existe). */
|
|
72
|
+
getEvidence(id: string): Evidence | undefined {
|
|
73
|
+
return this.evidence.get(id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Evidências de uma tool/esécie. */
|
|
77
|
+
evidenceOfKind(kind: EvidenceKind): Evidence[] {
|
|
78
|
+
return [...this.evidence.values()].filter((item) => item.kind === kind);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Adiciona um draft de finding. */
|
|
82
|
+
addDraft(draft: FindingDraft): void {
|
|
83
|
+
this.drafts.push(draft);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Define a cobertura de uma regra (idempotente por texto). */
|
|
87
|
+
setCoverage(rule: string, status: RuleCoverageStatus, reason?: string): void {
|
|
88
|
+
const existing = this.coverage.get(rule);
|
|
89
|
+
if (existing) {
|
|
90
|
+
existing.status = status;
|
|
91
|
+
if (reason) existing.reason = reason;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.coverage.set(rule, {
|
|
95
|
+
rule,
|
|
96
|
+
status,
|
|
97
|
+
files: [],
|
|
98
|
+
symbols: [],
|
|
99
|
+
...(reason ? { reason } : {}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Cobertura atual de uma regra. */
|
|
104
|
+
coverageOf(rule: string): RuleCoverage | undefined {
|
|
105
|
+
return this.coverage.get(rule);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `true` quando a regra já foi coberta de alguma forma. */
|
|
109
|
+
isCovered(rule: string): boolean {
|
|
110
|
+
return this.coverage.has(rule);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Registra o uso de uma tool (contagem por nome). */
|
|
114
|
+
recordToolCall(name: string): void {
|
|
115
|
+
this.toolCalls.set(name, (this.toolCalls.get(name) ?? 0) + 1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Chamadas registradas de uma tool. */
|
|
119
|
+
callsOf(name: string): number {
|
|
120
|
+
return this.toolCalls.get(name) ?? 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Snapshot serializável (sem prompts). */
|
|
124
|
+
toJSON(): Record<string, unknown> {
|
|
125
|
+
return {
|
|
126
|
+
hypotheses: this.hypotheses,
|
|
127
|
+
tasks: this.tasks,
|
|
128
|
+
evidence: [...this.evidence.values()],
|
|
129
|
+
coverage: [...this.coverage.values()],
|
|
130
|
+
drafts: this.drafts,
|
|
131
|
+
toolCalls: Object.fromEntries(this.toolCalls),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verificador de evidências (`EvidenceVerifier`).
|
|
3
|
+
*
|
|
4
|
+
* Confere arquivo/linha/snippet contra o working tree. Evidência inválida não
|
|
5
|
+
* sustenta finding: o draft é descartado (ou reancorado quando o snippet existe
|
|
6
|
+
* em outra linha).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { normalizeLine } from "../../core/finding-anchor";
|
|
10
|
+
import type { AgentState } from "./agent-state";
|
|
11
|
+
import type { Evidence, FindingDraft } from "./evidence";
|
|
12
|
+
|
|
13
|
+
/** Resultado da verificação de uma evidência. */
|
|
14
|
+
export type EvidenceVerdict = {
|
|
15
|
+
evidence: Evidence;
|
|
16
|
+
valid: boolean;
|
|
17
|
+
reason?: string;
|
|
18
|
+
/** Linhas corrigidas quando o snippet foi reancorado. */
|
|
19
|
+
startLine?: number;
|
|
20
|
+
endLine?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Resultado da verificação de um draft. */
|
|
24
|
+
export type DraftVerdict = {
|
|
25
|
+
draft: FindingDraft;
|
|
26
|
+
valid: boolean;
|
|
27
|
+
evidence: Evidence[];
|
|
28
|
+
reason?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Verificador de evidências por conteúdo. */
|
|
32
|
+
export class EvidenceVerifier {
|
|
33
|
+
private readonly allowedFiles: ReadonlySet<string>;
|
|
34
|
+
private readonly readFile: (path: string) => string | null;
|
|
35
|
+
|
|
36
|
+
constructor(options: {
|
|
37
|
+
allowedFiles: Iterable<string>;
|
|
38
|
+
readFile: (path: string) => string | null;
|
|
39
|
+
}) {
|
|
40
|
+
this.allowedFiles = new Set(options.allowedFiles);
|
|
41
|
+
this.readFile = options.readFile;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Verifica uma evidência (arquivo permitido + snippet localizado). */
|
|
45
|
+
verify(evidence: Evidence): EvidenceVerdict {
|
|
46
|
+
if (!this.allowedFiles.has(evidence.file)) {
|
|
47
|
+
return { evidence, valid: false, reason: "arquivo fora do domínio/impacto" };
|
|
48
|
+
}
|
|
49
|
+
const content = this.readFile(evidence.file);
|
|
50
|
+
if (content === null) {
|
|
51
|
+
return { evidence, valid: false, reason: "arquivo ausente" };
|
|
52
|
+
}
|
|
53
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
54
|
+
const expected = evidence.snippet
|
|
55
|
+
.split("\n")
|
|
56
|
+
.map(normalizeLine)
|
|
57
|
+
.filter((line) => line.length > 0);
|
|
58
|
+
if (expected.length === 0) {
|
|
59
|
+
return { evidence, valid: false, reason: "snippet vazio" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const start = Math.max(1, evidence.startLine);
|
|
63
|
+
const actual = lines
|
|
64
|
+
.slice(start - 1, start - 1 + expected.length)
|
|
65
|
+
.map(normalizeLine);
|
|
66
|
+
if (sameLines(actual, expected)) {
|
|
67
|
+
return {
|
|
68
|
+
evidence,
|
|
69
|
+
valid: true,
|
|
70
|
+
startLine: start,
|
|
71
|
+
endLine: start + expected.length - 1,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const relocated = lines.findIndex((_line, index) =>
|
|
76
|
+
sameLines(
|
|
77
|
+
lines.slice(index, index + expected.length).map(normalizeLine),
|
|
78
|
+
expected,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
if (relocated >= 0) {
|
|
82
|
+
return {
|
|
83
|
+
evidence,
|
|
84
|
+
valid: true,
|
|
85
|
+
reason: "reancorado",
|
|
86
|
+
startLine: relocated + 1,
|
|
87
|
+
endLine: relocated + expected.length,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { evidence, valid: false, reason: "snippet não encontrado no arquivo" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Verifica um draft: precisa de ao menos uma evidência válida. */
|
|
94
|
+
verifyDraft(draft: FindingDraft, state: AgentState): DraftVerdict {
|
|
95
|
+
const evidence: Evidence[] = [];
|
|
96
|
+
const reasons: string[] = [];
|
|
97
|
+
for (const id of draft.evidenceIds) {
|
|
98
|
+
const item = state.getEvidence(id);
|
|
99
|
+
if (!item) {
|
|
100
|
+
reasons.push(`evidência '${id}' inexistente`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const verdict = this.verify(item);
|
|
104
|
+
if (verdict.valid) {
|
|
105
|
+
evidence.push({
|
|
106
|
+
...item,
|
|
107
|
+
startLine: verdict.startLine ?? item.startLine,
|
|
108
|
+
endLine: verdict.endLine ?? item.endLine,
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
reasons.push(verdict.reason ?? "evidência inválida");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
draft,
|
|
116
|
+
valid: evidence.length > 0,
|
|
117
|
+
evidence,
|
|
118
|
+
...(evidence.length === 0
|
|
119
|
+
? { reason: reasons.join("; ") || "sem evidência" }
|
|
120
|
+
: {}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Compara linhas normalizadas. */
|
|
126
|
+
function sameLines(a: readonly string[], b: readonly string[]): boolean {
|
|
127
|
+
if (a.length !== b.length) return false;
|
|
128
|
+
return a.every((line, index) => line === b[index]);
|
|
129
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipos de investigação (harness nativo).
|
|
3
|
+
*
|
|
4
|
+
* Uma revisão é uma investigação: regras viram tarefas, tarefas produzem
|
|
5
|
+
* evidências, e só drafts com evidência válida viram findings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Severity } from "../../core/types";
|
|
9
|
+
import type { ToolCapability } from "../tools/agent-tool";
|
|
10
|
+
|
|
11
|
+
/** Tipo de tarefa investigável. */
|
|
12
|
+
export type InvestigationTaskKind =
|
|
13
|
+
| "rule-evidence"
|
|
14
|
+
| "symbol-impact"
|
|
15
|
+
| "tests"
|
|
16
|
+
| "context";
|
|
17
|
+
|
|
18
|
+
/** Tarefa de investigação derivada de uma regra. */
|
|
19
|
+
export type InvestigationTask = {
|
|
20
|
+
id: string;
|
|
21
|
+
rule: string;
|
|
22
|
+
severity: Severity;
|
|
23
|
+
kind: InvestigationTaskKind;
|
|
24
|
+
files: string[];
|
|
25
|
+
symbols: string[];
|
|
26
|
+
capabilities: ToolCapability[];
|
|
27
|
+
/** Prioridade (menor = antes). */
|
|
28
|
+
priority: number;
|
|
29
|
+
status: "pending" | "in_progress" | "done" | "skipped";
|
|
30
|
+
reason?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Plano completo de um agente. */
|
|
34
|
+
export type InvestigationPlan = {
|
|
35
|
+
tasks: InvestigationTask[];
|
|
36
|
+
totalFiles: number;
|
|
37
|
+
estimatedToolCalls: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Estratégia substituível de planejamento. */
|
|
41
|
+
export interface InvestigationStrategy {
|
|
42
|
+
plan(
|
|
43
|
+
context: {
|
|
44
|
+
watchdogId: string;
|
|
45
|
+
files: readonly string[];
|
|
46
|
+
symbols: readonly string[];
|
|
47
|
+
},
|
|
48
|
+
rules: readonly import("./rule-analyzer").RuleAnalysis[],
|
|
49
|
+
): InvestigationPlan;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Evidência coletada por uma tool. */
|
|
53
|
+
export type EvidenceKind =
|
|
54
|
+
| "definition"
|
|
55
|
+
| "reference"
|
|
56
|
+
| "import"
|
|
57
|
+
| "diff"
|
|
58
|
+
| "test"
|
|
59
|
+
| "doc";
|
|
60
|
+
|
|
61
|
+
/** Trecho de código/docs que sustenta uma conclusão. */
|
|
62
|
+
export type Evidence = {
|
|
63
|
+
id: string;
|
|
64
|
+
file: string;
|
|
65
|
+
startLine: number;
|
|
66
|
+
endLine: number;
|
|
67
|
+
snippet: string;
|
|
68
|
+
kind: EvidenceKind;
|
|
69
|
+
sourceTool: string;
|
|
70
|
+
confidence: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Status de cobertura de uma regra. */
|
|
74
|
+
export type RuleCoverageStatus = "checked" | "not_applicable" | "skipped";
|
|
75
|
+
|
|
76
|
+
/** Cobertura de uma regra (toda regra precisa terminar aqui). */
|
|
77
|
+
export type RuleCoverage = {
|
|
78
|
+
rule: string;
|
|
79
|
+
status: RuleCoverageStatus;
|
|
80
|
+
files: string[];
|
|
81
|
+
symbols: string[];
|
|
82
|
+
reason?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Achado proposto pelo agente (ainda não é finding final). */
|
|
86
|
+
export type FindingDraft = {
|
|
87
|
+
rule: string;
|
|
88
|
+
severity: Severity;
|
|
89
|
+
file: string;
|
|
90
|
+
line?: number;
|
|
91
|
+
message: string;
|
|
92
|
+
action: string;
|
|
93
|
+
evidenceIds: string[];
|
|
94
|
+
confidence: number;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Decisão do crítico sobre um draft. */
|
|
98
|
+
export type CriticVerdict = "keep" | "drop" | "downgrade";
|
|
99
|
+
|
|
100
|
+
/** Resultado do crítico. */
|
|
101
|
+
export type CriticDecision = {
|
|
102
|
+
draft: FindingDraft;
|
|
103
|
+
verdict: CriticVerdict;
|
|
104
|
+
reason: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Gera o id de uma evidência (determinístico por ordem de coleta). */
|
|
108
|
+
export function evidenceId(sequence: number): string {
|
|
109
|
+
return `ev-${String(sequence).padStart(3, "0")}`;
|
|
110
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crítico de findings (`FindingCritic`).
|
|
3
|
+
*
|
|
4
|
+
* Segunda passada com o objetivo de **refutar** cada draft. Regras:
|
|
5
|
+
* - sem evidência válida → drop;
|
|
6
|
+
* - regra inexistente → drop;
|
|
7
|
+
* - confiança baixa → downgrade; muito baixa → drop;
|
|
8
|
+
* - `blocking`/`attention` exigem crítica (determinística aqui; um crítico
|
|
9
|
+
* por IA pode ser injetado no futuro).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { emergentText, isEmergentRule } from "../../core/rule-text";
|
|
13
|
+
import { type Severity, severityRank } from "../../core/types";
|
|
14
|
+
import type { AgentState } from "./agent-state";
|
|
15
|
+
import type { CriticDecision, FindingDraft } from "./evidence";
|
|
16
|
+
import type { EvidenceVerifier } from "./evidence-verifier";
|
|
17
|
+
|
|
18
|
+
/** Regras válidas do watchdog (texto). */
|
|
19
|
+
export type CriticOptions = {
|
|
20
|
+
knownRules: ReadonlySet<string>;
|
|
21
|
+
verifier: EvidenceVerifier;
|
|
22
|
+
/** Aceita findings `Lógica:`/`Domínio:` fora das regras. */
|
|
23
|
+
allowEmergentFindings?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Crítico determinístico. */
|
|
27
|
+
export class FindingCritic {
|
|
28
|
+
private readonly options: CriticOptions;
|
|
29
|
+
|
|
30
|
+
constructor(options: CriticOptions) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Avalia todos os drafts, na ordem. */
|
|
35
|
+
review(drafts: readonly FindingDraft[], state: AgentState): CriticDecision[] {
|
|
36
|
+
return drafts.map((draft) => this.reviewOne(draft, state));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Avalia um draft. */
|
|
40
|
+
reviewOne(draft: FindingDraft, state: AgentState): CriticDecision {
|
|
41
|
+
const known = this.options.knownRules.has(draft.rule.trim());
|
|
42
|
+
const emergent = isEmergentRule(draft.rule);
|
|
43
|
+
if (!known && !(emergent && this.options.allowEmergentFindings)) {
|
|
44
|
+
return {
|
|
45
|
+
draft,
|
|
46
|
+
verdict: "drop",
|
|
47
|
+
reason: emergent
|
|
48
|
+
? "finding emergente desabilitado no watchdog"
|
|
49
|
+
: "regra não existe no watchdog",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (emergent && !emergentText(draft.rule)) {
|
|
53
|
+
return { draft, verdict: "drop", reason: "finding emergente sem descrição" };
|
|
54
|
+
}
|
|
55
|
+
const verdict = this.options.verifier.verifyDraft(draft, state);
|
|
56
|
+
if (!verdict.valid) {
|
|
57
|
+
return { draft, verdict: "drop", reason: verdict.reason ?? "sem evidência" };
|
|
58
|
+
}
|
|
59
|
+
if (draft.confidence < 0.4) {
|
|
60
|
+
return { draft, verdict: "drop", reason: "confiança abaixo de 0.4" };
|
|
61
|
+
}
|
|
62
|
+
const normalized = emergent
|
|
63
|
+
? { ...draft, severity: capSeverity(draft.severity, "attention") }
|
|
64
|
+
: draft;
|
|
65
|
+
if (
|
|
66
|
+
normalized.confidence < 0.7 &&
|
|
67
|
+
(normalized.severity === "blocking" || normalized.severity === "attention")
|
|
68
|
+
) {
|
|
69
|
+
return {
|
|
70
|
+
draft: normalized,
|
|
71
|
+
verdict: "downgrade",
|
|
72
|
+
reason: "confiança insuficiente para a severidade",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { draft: normalized, verdict: "keep", reason: "evidência válida" };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Limita a severidade a um teto (rank menor = mais grave). */
|
|
80
|
+
export function capSeverity(severity: Severity, max: Severity): Severity {
|
|
81
|
+
return severityRank(severity) < severityRank(max) ? max : severity;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Rebaixa uma severidade em um nível. */
|
|
85
|
+
export function downgrade(severity: Severity): Severity {
|
|
86
|
+
switch (severity) {
|
|
87
|
+
case "blocking":
|
|
88
|
+
return "attention";
|
|
89
|
+
case "attention":
|
|
90
|
+
return "suggestion";
|
|
91
|
+
case "suggestion":
|
|
92
|
+
return "nit";
|
|
93
|
+
default:
|
|
94
|
+
return "nit";
|
|
95
|
+
}
|
|
96
|
+
}
|