@payconductor/houndsman 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +24 -4
  2. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  3. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  4. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  5. package/assets/grammars/web-tree-sitter.wasm +0 -0
  6. package/package.json +11 -3
  7. package/src/cli/commands/command.ts +5 -0
  8. package/src/cli/commands/doctor-command.ts +116 -24
  9. package/src/cli/commands/hunt-command.ts +25 -0
  10. package/src/cli/help.ts +17 -1
  11. package/src/cli/index.ts +10 -6
  12. package/src/core/ai/ai-client.ts +221 -0
  13. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  14. package/src/core/ai/errors.ts +36 -0
  15. package/src/core/ai/features/brief-enricher.ts +62 -0
  16. package/src/core/ai/features/classifier.ts +50 -0
  17. package/src/core/ai/features/context-summarizer.ts +31 -0
  18. package/src/core/ai/features/pre-analyzer.ts +51 -0
  19. package/src/core/ai/features/verifier.ts +45 -0
  20. package/src/core/ai/index.ts +56 -0
  21. package/src/core/ai/model-factory.ts +137 -0
  22. package/src/core/ai/prompts/base.ts +36 -0
  23. package/src/core/ai/prompts/brief.ts +87 -0
  24. package/src/core/ai/prompts/classification.ts +36 -0
  25. package/src/core/ai/prompts/context.ts +29 -0
  26. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  27. package/src/core/ai/prompts/verify.ts +40 -0
  28. package/src/core/ai/schemas.ts +110 -0
  29. package/src/core/ai/types.ts +76 -0
  30. package/src/core/ai/usage-budget.ts +75 -0
  31. package/src/core/ai/usage-recorder.ts +79 -0
  32. package/src/core/assets.ts +16 -0
  33. package/src/core/ast/diff.ts +220 -0
  34. package/src/core/ast/impact.ts +127 -0
  35. package/src/core/ast/parser.ts +167 -0
  36. package/src/core/ast/service.ts +233 -0
  37. package/src/core/ast/symbols.ts +294 -0
  38. package/src/core/ast/types.ts +125 -0
  39. package/src/core/brief.ts +138 -0
  40. package/src/core/commands/findings.ts +59 -1
  41. package/src/core/commands/review.ts +2 -0
  42. package/src/core/commands/scaffold.ts +159 -10
  43. package/src/core/commands/update.ts +5 -3
  44. package/src/core/config.ts +85 -0
  45. package/src/core/defaults.ts +27 -0
  46. package/src/core/domain/watchdog.ts +43 -5
  47. package/src/core/engine/houndsman-engine.ts +16 -1
  48. package/src/core/engine/review-scope.ts +52 -6
  49. package/src/core/engine/review-session.ts +27 -2
  50. package/src/core/env.ts +101 -0
  51. package/src/core/file-classifier.ts +25 -4
  52. package/src/core/jev/jev-client.ts +8 -7
  53. package/src/core/jev/jev-verify.ts +2 -1
  54. package/src/core/logging/index.ts +17 -0
  55. package/src/core/logging/logger.ts +187 -0
  56. package/src/core/orchestrator.ts +357 -9
  57. package/src/core/project-config.ts +123 -1
  58. package/src/core/registry-check.ts +148 -6
  59. package/src/core/renderers/manifest-writer.ts +33 -0
  60. package/src/core/rule-text.ts +97 -0
  61. package/src/core/services/file-classifier.ts +10 -3
  62. package/src/core/services/verifier.ts +14 -3
  63. package/src/core/storage/paths.ts +6 -5
  64. package/src/core/storage/types.ts +6 -0
  65. package/src/core/storage/work-area.ts +12 -0
  66. package/src/core/types.ts +37 -1
  67. package/src/core/verify.ts +17 -5
  68. package/src/harness/README.md +15 -0
  69. package/src/harness/agent/agent-context.ts +33 -0
  70. package/src/harness/agent/agent-errors.ts +30 -0
  71. package/src/harness/agent/agent-factory.ts +45 -0
  72. package/src/harness/agent/agent-reporter.ts +34 -0
  73. package/src/harness/agent/agent-result.ts +16 -0
  74. package/src/harness/agent/agent-runner.ts +32 -0
  75. package/src/harness/agent/agent-scheduler.ts +37 -0
  76. package/src/harness/agent/review-agent.ts +391 -0
  77. package/src/harness/config/harness-config.ts +84 -0
  78. package/src/harness/hunt/hunt-harness.ts +83 -0
  79. package/src/harness/hunt/hunt-options.ts +75 -0
  80. package/src/harness/hunt/hunt-result.ts +17 -0
  81. package/src/harness/hunt/hunt-session.ts +372 -0
  82. package/src/harness/hunt/review-planner.ts +98 -0
  83. package/src/harness/index.ts +36 -0
  84. package/src/harness/investigation/agent-state.ts +134 -0
  85. package/src/harness/investigation/evidence-verifier.ts +129 -0
  86. package/src/harness/investigation/evidence.ts +110 -0
  87. package/src/harness/investigation/finding-critic.ts +96 -0
  88. package/src/harness/investigation/investigation-planner.ts +118 -0
  89. package/src/harness/investigation/investigation-task.ts +10 -0
  90. package/src/harness/investigation/review-journal.ts +87 -0
  91. package/src/harness/investigation/rule-analyzer.ts +178 -0
  92. package/src/harness/investigation/rule-signals.ts +110 -0
  93. package/src/harness/prompts/agent-schemas.ts +57 -0
  94. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  95. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  96. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  97. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  98. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  99. package/src/harness/tools/agent-tool.ts +102 -0
  100. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  101. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  102. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  103. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  104. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  105. package/src/harness/tools/builtin/helpers.ts +115 -0
  106. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  107. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  108. package/src/harness/tools/builtin/index.ts +59 -0
  109. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  110. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  111. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  112. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  113. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  114. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  115. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  116. package/src/harness/tools/path-guard.ts +90 -0
  117. package/src/harness/tools/result-limiter.ts +49 -0
  118. package/src/harness/tools/secret-redactor.ts +54 -0
  119. package/src/harness/tools/tool-cache.ts +50 -0
  120. package/src/harness/tools/tool-call-budget.ts +60 -0
  121. package/src/harness/tools/tool-context.ts +5 -0
  122. package/src/harness/tools/tool-middleware.ts +105 -0
  123. package/src/harness/tools/tool-policy.ts +51 -0
  124. package/src/harness/tools/tool-registry.ts +112 -0
  125. package/src/harness/tools/tool-result.ts +15 -0
  126. package/src/harness/tools/toolbox.ts +36 -0
  127. package/src/harness/validation/finding-validator.ts +119 -0
  128. package/src/index.ts +46 -0
  129. package/src/server/client/utils/constants.ts +27 -5
  130. package/src/server/client/utils/types.ts +6 -0
  131. package/src/server/index.ts +3 -2
  132. package/src/skill/OUTPUT-FORMAT.md +12 -0
  133. package/src/skill/SETUP.md +6 -2
  134. package/src/skill/hunt.md +14 -2
  135. package/src/skill/verify.md +1 -1
  136. package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
  137. package/src/tests/ai-core.test.ts +120 -0
  138. package/src/tests/ai-features.test.ts +183 -0
  139. package/src/tests/ast.test.ts +335 -0
  140. package/src/tests/env.test.ts +69 -0
  141. package/src/tests/fallback.test.ts +172 -0
  142. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  143. package/src/tests/fixtures/fake-investigation.ts +64 -0
  144. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  145. package/src/tests/harness-agent.test.ts +122 -0
  146. package/src/tests/harness-investigation.test.ts +206 -0
  147. package/src/tests/harness-tools.test.ts +148 -0
  148. package/src/tests/hunt-command.test.ts +212 -0
  149. package/src/tests/logger.test.ts +83 -0
  150. package/src/tests/review-scope.test.ts +11 -11
  151. package/src/tests/rule-analyzer.test.ts +61 -0
  152. package/src/tests/tool-contracts.test.ts +82 -0
  153. package/src/tests/verify.test.ts +1 -1
@@ -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
+ }
@@ -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,10 @@
1
+ /**
2
+ * Tipos de tarefa/plano — reexportados de `evidence.ts` para evitar ciclos.
3
+ */
4
+
5
+ export type {
6
+ InvestigationPlan,
7
+ InvestigationStrategy,
8
+ InvestigationTask,
9
+ InvestigationTaskKind,
10
+ } from "./evidence";
@@ -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
+ }