@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,87 @@
1
+ /**
2
+ * Prompt do enriquecimento de brief (`AiBriefEnricher`).
3
+ *
4
+ * A saída é apenas um **plano sugerido** (foco, perguntas e riscos). Ela não
5
+ * pode criar regras nem achados.
6
+ */
7
+
8
+ import type { AstFileBrief } from "../../ast/types";
9
+ import type { ChurnMap, Rule, WatchdogRenderContext } from "../../types";
10
+ import { buildSystem } from "./base";
11
+
12
+ /** Entrada do prompt do brief. */
13
+ export type BriefPromptInput = {
14
+ watchdogId: string;
15
+ name: string;
16
+ description: string;
17
+ scope: string;
18
+ rules: readonly Rule[];
19
+ files: readonly string[];
20
+ churn?: ChurnMap;
21
+ context?: WatchdogRenderContext | null;
22
+ ast?: readonly AstFileBrief[];
23
+ docs?: readonly string[];
24
+ };
25
+
26
+ /** Instruções de sistema do enriquecimento. */
27
+ export function briefInstructions(): string {
28
+ return buildSystem(
29
+ "Você é um revisor sênior que prepara um plano de leitura para um watchdog (revisor) especializado.",
30
+ [
31
+ "As regras fornecidas são o PISO da revisão, não o teto.",
32
+ "Além delas, procure riscos de consistência lógica: nome/contrato vs comportamento (ex.: `fazA` que faz `fazB`), fluxos incompletos, estados inválidos, off-by-one e bordas.",
33
+ "Procure também riscos do domínio de negócio: transições de estado, idempotência, arredondamento, timezone, autorização e invariantes implícitas.",
34
+ "Sugira foco de leitura (até 5), perguntas de verificação (até 3), riscos (até 3), verificações de lógica (até 3) e de domínio (até 3).",
35
+ "Sugira verificações, nunca afirme violação; não crie regra nova nem invente arquivo, linha ou achado.",
36
+ "Se uma suspeita não couber em nenhuma regra, coloque-a em `logic`/`domain` como pergunta de verificação.",
37
+ "Não repita o texto literal das regras; o brief já as contém.",
38
+ ],
39
+ );
40
+ }
41
+
42
+ /** Prompt do usuário com o contexto compactado do watchdog. */
43
+ export function briefPrompt(input: BriefPromptInput): string {
44
+ const lines = [
45
+ `Watchdog: ${input.name} (${input.watchdogId})`,
46
+ `Escopo: ${input.scope}`,
47
+ `Objetivo: ${input.description}`,
48
+ "",
49
+ "## Regras",
50
+ ...input.rules.map((rule) => `- [${rule.severity}] ${rule.text}`),
51
+ "",
52
+ `## Arquivos (${input.files.length})`,
53
+ ...input.files.slice(0, 40).map((file) => {
54
+ const churn = input.churn?.[file];
55
+ return `- ${file}${churn ? ` (+${churn.additions}/−${churn.deletions})` : ""}`;
56
+ }),
57
+ ];
58
+ if (input.context) {
59
+ lines.push("", "## Contexto da PR");
60
+ if (input.context.objective) lines.push(`Objetivo: ${input.context.objective}`);
61
+ if (input.context.motivation)
62
+ lines.push(`Motivação: ${input.context.motivation}`);
63
+ for (const item of input.context.changeSummary ?? []) lines.push(`- ${item}`);
64
+ }
65
+ if (input.ast && input.ast.length > 0) {
66
+ lines.push("", "## Estrutura (AST)");
67
+ for (const entry of input.ast) {
68
+ const delta = entry.delta;
69
+ lines.push(
70
+ `- ${entry.file}: +${delta.addedSymbols.length} −${delta.removedSymbols.length} ~${delta.modifiedSymbols.length} símbolo(s)`,
71
+ );
72
+ }
73
+ }
74
+ if (input.docs && input.docs.length > 0) {
75
+ lines.push("", "## Docs disponíveis", ...input.docs.map((doc) => `- ${doc}`));
76
+ }
77
+ lines.push(
78
+ "",
79
+ "## Revisão além das regras",
80
+ "As regras são o piso. Aponte verificações de consistência lógica e de domínio",
81
+ "que mereçam inspeção mesmo sem regra explícita (nome vs comportamento, estados,",
82
+ "transições, idempotência, bordas). Sugira verificações; não afirme violações.",
83
+ "",
84
+ "Produza o plano de revisão em JSON (focus, questions, risks, logic, domain).",
85
+ );
86
+ return lines.join("\n");
87
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Prompt da classificação fallback (`AiClassifier`).
3
+ */
4
+
5
+ import { buildSystem } from "./base";
6
+
7
+ /** Entrada do prompt de classificação. */
8
+ export type ClassificationPromptInput = {
9
+ files: readonly string[];
10
+ watchdogs: readonly { id: string; name: string; scope: string }[];
11
+ };
12
+
13
+ /** Instruções de sistema. */
14
+ export function classificationInstructions(): string {
15
+ return buildSystem(
16
+ "Você classifica arquivos alterados nos domínios (watchdogs) de uma revisão.",
17
+ [
18
+ "Atribua cada arquivo ao watchdog cujo escopo melhor corresponde.",
19
+ "Se nenhum watchdog servir, omita o arquivo.",
20
+ "Use apenas ids de watchdog fornecidos.",
21
+ ],
22
+ );
23
+ }
24
+
25
+ /** Prompt do usuário. */
26
+ export function classificationPrompt(input: ClassificationPromptInput): string {
27
+ return [
28
+ "## Watchdogs",
29
+ ...input.watchdogs.map((w) => `- ${w.id}: ${w.name} — ${w.scope}`),
30
+ "",
31
+ "## Arquivos",
32
+ ...input.files.slice(0, 100).map((file) => `- ${file}`),
33
+ "",
34
+ "Produza as atribuições em JSON.",
35
+ ].join("\n");
36
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Prompt do resumo de contexto da PR (`AiContextSummarizer`).
3
+ */
4
+
5
+ import { buildSystem, clip } from "./base";
6
+
7
+ /** Instruções de sistema do resumo. */
8
+ export function contextInstructions(): string {
9
+ return buildSystem(
10
+ "Você resume o contexto de uma Pull Request para revisores técnicos.",
11
+ [
12
+ "Objetivo em uma frase (máx. 200 caracteres).",
13
+ "Motivação em uma frase (máx. 240 caracteres).",
14
+ "Resumo das alterações em até 4 bullets (máx. 160 caracteres cada).",
15
+ "Não invente informação ausente do contexto.",
16
+ ],
17
+ );
18
+ }
19
+
20
+ /** Prompt do usuário com o contexto bruto da PR. */
21
+ export function contextPrompt(raw: string): string {
22
+ return [
23
+ "Resuma o contexto da PR abaixo.",
24
+ "",
25
+ "```json",
26
+ clip(raw, 12_000),
27
+ "```",
28
+ ].join("\n");
29
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Prompt da pré-análise por IA (`AiPreAnalyzer`).
3
+ *
4
+ * Candidatos não são achados: são indícios para o watchdog confirmar.
5
+ */
6
+
7
+ import type { Rule } from "../../types";
8
+ import { buildSystem } from "./base";
9
+
10
+ /** Entrada do prompt. */
11
+ export type PreAnalysisPromptInput = {
12
+ watchdogId: string;
13
+ name: string;
14
+ rules: readonly Rule[];
15
+ files: readonly { file: string; snippet: string }[];
16
+ };
17
+
18
+ /** Instruções de sistema. */
19
+ export function preAnalysisInstructions(): string {
20
+ return buildSystem(
21
+ "Você faz uma pré-análise de possíveis violações de regras em um diff.",
22
+ [
23
+ "Produza candidatos apenas quando houver indício concreto.",
24
+ "Use o texto de uma das regras fornecidas.",
25
+ "Confiança calibrada entre 0 e 1; prefira valores baixos na dúvida.",
26
+ "Não invente arquivos.",
27
+ ],
28
+ );
29
+ }
30
+
31
+ /** Prompt do usuário. */
32
+ export function preAnalysisPrompt(input: PreAnalysisPromptInput): string {
33
+ return [
34
+ `Watchdog: ${input.name} (${input.watchdogId})`,
35
+ "",
36
+ "## Regras",
37
+ ...input.rules.map((rule) => `- [${rule.severity}] ${rule.text}`),
38
+ "",
39
+ "## Arquivos e trechos",
40
+ ...input.files.flatMap((item) => [
41
+ `### ${item.file}`,
42
+ "```",
43
+ item.snippet,
44
+ "```",
45
+ ]),
46
+ "",
47
+ "Produza os candidatos em JSON.",
48
+ ].join("\n");
49
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Prompt de confirmação de findings (`AiVerifier`).
3
+ */
4
+
5
+ import { buildSystem } from "./base";
6
+
7
+ /** Entrada do prompt. */
8
+ export type VerifyPromptInput = {
9
+ findings: readonly {
10
+ id: string;
11
+ rule: string;
12
+ file: string;
13
+ line: number | null;
14
+ message: string;
15
+ snippets: string;
16
+ }[];
17
+ };
18
+
19
+ /** Instruções de sistema. */
20
+ export function verifyInstructions(): string {
21
+ return buildSystem("Você confirma ou refuta achados de revisão de código.", [
22
+ "Para cada achado, decida se a violação persiste no código atual.",
23
+ "Baseie-se apenas no trecho fornecido.",
24
+ "Confiança calibrada entre 0 e 1.",
25
+ "Não invente ids.",
26
+ ]);
27
+ }
28
+
29
+ /** Prompt do usuário. */
30
+ export function verifyPrompt(input: VerifyPromptInput): string {
31
+ const blocks = input.findings.flatMap((finding) => [
32
+ `### ${finding.id} — ${finding.rule}`,
33
+ `Arquivo: ${finding.file}${finding.line ? `:${finding.line}` : ""}`,
34
+ `Achado: ${finding.message}`,
35
+ "```",
36
+ finding.snippets,
37
+ "```",
38
+ ]);
39
+ return [...blocks, "", "Produza os vereditos em JSON."].join("\n");
40
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Schemas Zod da camada de IA.
3
+ *
4
+ * Espelham os contratos existentes (`ContextSummary`, `WatchdogReport`) e os
5
+ * planos das features. Todo schema é fechado por validação: saída inválida do
6
+ * modelo vira falha fail-open, nunca dado corrompido.
7
+ */
8
+
9
+ import { z } from "zod";
10
+
11
+ export const SeveritySchema = z.enum(["blocking", "attention", "suggestion", "nit"]);
12
+
13
+ /** Resumo da PR (`context-summary.json`). */
14
+ export const ContextSummarySchema = z.object({
15
+ available: z.boolean(),
16
+ source: z
17
+ .object({
18
+ number: z.number().optional(),
19
+ title: z.string().optional(),
20
+ url: z.string().optional(),
21
+ base: z.string().optional(),
22
+ head: z.string().optional(),
23
+ })
24
+ .optional(),
25
+ objective: z.string().max(200).optional(),
26
+ motivation: z.string().max(240).optional(),
27
+ changeSummary: z.array(z.string().max(160)).max(4).optional(),
28
+ });
29
+ export type ContextSummaryPlan = z.infer<typeof ContextSummarySchema>;
30
+
31
+ /** Plano de revisão sugerido por IA para um brief. */
32
+ export const BriefPlanSchema = z.object({
33
+ focus: z.array(z.string()).max(5).default([]),
34
+ questions: z.array(z.string()).max(3).default([]),
35
+ risks: z.array(z.string()).max(3).default([]),
36
+ /** Verificações de consistência lógica (fora das regras explícitas). */
37
+ logic: z.array(z.string()).max(3).default([]),
38
+ /** Riscos do domínio de negócio (fora das regras explícitas). */
39
+ domain: z.array(z.string()).max(3).default([]),
40
+ });
41
+ export type BriefPlan = z.infer<typeof BriefPlanSchema>;
42
+
43
+ /** Classificação de arquivos não classificados. */
44
+ export const ClassificationSchema = z.object({
45
+ assignments: z
46
+ .array(
47
+ z.object({
48
+ file: z.string(),
49
+ watchdogId: z.string(),
50
+ confidence: z.number().min(0).max(1).optional(),
51
+ }),
52
+ )
53
+ .max(200)
54
+ .default([]),
55
+ });
56
+ export type AiClassification = z.infer<typeof ClassificationSchema>;
57
+
58
+ /** Candidato de pré-análise (`JevCandidate`-like). */
59
+ export const CandidateSchema = z.object({
60
+ candidates: z
61
+ .array(
62
+ z.object({
63
+ file: z.string(),
64
+ watchdogId: z.string(),
65
+ rule: z.string(),
66
+ severity: SeveritySchema,
67
+ confidence: z.number().min(0).max(1),
68
+ reason: z.string().optional(),
69
+ }),
70
+ )
71
+ .max(200)
72
+ .default([]),
73
+ });
74
+ export type AiCandidate = z.infer<typeof CandidateSchema>["candidates"][number];
75
+
76
+ /** Relatório de um watchdog (espelha `OUTPUT-FORMAT.md`). */
77
+ export const WatchdogReportSchema = z.object({
78
+ watchdogId: z.string(),
79
+ reviewedFiles: z.array(z.string()).default([]),
80
+ skipped: z.array(z.string()).default([]),
81
+ findings: z
82
+ .array(
83
+ z.object({
84
+ rule: z.string(),
85
+ severity: SeveritySchema,
86
+ file: z.string(),
87
+ line: z.number().int().positive().optional(),
88
+ message: z.string(),
89
+ action: z.string(),
90
+ evidence: z.string().optional(),
91
+ }),
92
+ )
93
+ .default([]),
94
+ });
95
+ export type AiWatchdogReport = z.infer<typeof WatchdogReportSchema>;
96
+
97
+ /** Veredito de verificação (confirma/refuta um finding ambíguo). */
98
+ export const VerifySchema = z.object({
99
+ verdicts: z
100
+ .array(
101
+ z.object({
102
+ id: z.string(),
103
+ persisting: z.boolean(),
104
+ confidence: z.number().min(0).max(1),
105
+ reason: z.string().optional(),
106
+ }),
107
+ )
108
+ .default([]),
109
+ });
110
+ export type AiVerdict = z.infer<typeof VerifySchema>["verdicts"][number];
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Contratos genéricos da camada de IA (biblioteca).
3
+ *
4
+ * `core/ai` não conhece o harness: expõe modelo, uso, tools e resultado. O
5
+ * harness nativo (`src/harness/`) consome estes contratos.
6
+ */
7
+
8
+ import type { ZodType, ZodTypeAny } from "zod";
9
+ import type { AiProviderKind } from "../config";
10
+
11
+ /** Uso de tokens de uma chamada (ou acumulado de uma review). */
12
+ export type AiUsage = {
13
+ inputTokens: number;
14
+ outputTokens: number;
15
+ totalTokens: number;
16
+ calls: number;
17
+ };
18
+
19
+ /** Uso zerado (novo objeto — nunca compartilhe para mutação). */
20
+ export function emptyUsage(): AiUsage {
21
+ return { inputTokens: 0, outputTokens: 0, totalTokens: 0, calls: 0 };
22
+ }
23
+
24
+ /** Soma dois usos sem mutar os originais. */
25
+ export function addUsage(a: AiUsage, b: AiUsage): AiUsage {
26
+ return {
27
+ inputTokens: a.inputTokens + b.inputTokens,
28
+ outputTokens: a.outputTokens + b.outputTokens,
29
+ totalTokens: a.totalTokens + b.totalTokens,
30
+ calls: a.calls + b.calls,
31
+ };
32
+ }
33
+
34
+ /** Tool exposta ao modelo (execução já vinculada a um contexto). */
35
+ export type AiTool = {
36
+ name: string;
37
+ description: string;
38
+ inputSchema: ZodTypeAny;
39
+ execute(input: unknown): Promise<string>;
40
+ };
41
+
42
+ /** Pedido de geração de modelo (texto ou objeto estruturado). */
43
+ export type ModelRequest<T> = {
44
+ /** Instruções de sistema. */
45
+ instructions?: string;
46
+ /** Prompt do usuário. */
47
+ prompt: string;
48
+ /** Tools disponíveis para o loop do modelo. */
49
+ tools?: Record<string, AiTool>;
50
+ /** Schema de saída (quando ausente, a saída é texto). */
51
+ outputSchema?: ZodType<T>;
52
+ maxSteps?: number;
53
+ maxOutputTokens?: number;
54
+ temperature?: number;
55
+ signal?: AbortSignal;
56
+ };
57
+
58
+ /** Resposta do modelo. */
59
+ export type ModelResponse<T> = {
60
+ output: T;
61
+ usage: AiUsage;
62
+ steps: number;
63
+ finishReason: string;
64
+ };
65
+
66
+ /** Provider de modelo (adaptador do AI SDK ou fake nos testes). */
67
+ export interface ModelProvider {
68
+ readonly name: AiProviderKind;
69
+ readonly modelId: string;
70
+ generate<T>(request: ModelRequest<T>): Promise<ModelResponse<T>>;
71
+ }
72
+
73
+ /** Resultado fail-open de uma chamada de IA. */
74
+ export type AiResult<T> =
75
+ | { ok: true; output: T; usage: AiUsage; model: string }
76
+ | { ok: false; error: string; usage?: AiUsage };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Orçamento de IA por review (chamadas e tokens).
3
+ *
4
+ * O budget é consultado antes de cada chamada e atualizado com o uso real. Ao
5
+ * estourar, o chamador decide parar (features do core) ou falhar (harness).
6
+ */
7
+
8
+ import { AiBudgetError } from "./errors";
9
+ import { type AiUsage, addUsage, emptyUsage } from "./types";
10
+
11
+ /** Tetos do budget (ausente = sem teto). */
12
+ export type AiBudgetLimits = {
13
+ maxCalls?: number;
14
+ maxTokens?: number;
15
+ };
16
+
17
+ /** Orçamento de uso acumulado. */
18
+ export class AiUsageBudget {
19
+ private readonly limits: AiBudgetLimits;
20
+ private readonly total: AiUsage = emptyUsage();
21
+
22
+ constructor(limits: AiBudgetLimits = {}) {
23
+ this.limits = limits;
24
+ }
25
+
26
+ get usage(): AiUsage {
27
+ return { ...this.total };
28
+ }
29
+
30
+ get maxCalls(): number | undefined {
31
+ return this.limits.maxCalls;
32
+ }
33
+
34
+ /** `true` quando ainda há orçamento para outra chamada. */
35
+ get canCall(): boolean {
36
+ if (
37
+ this.limits.maxCalls !== undefined &&
38
+ this.total.calls >= this.limits.maxCalls
39
+ ) {
40
+ return false;
41
+ }
42
+ if (
43
+ this.limits.maxTokens !== undefined &&
44
+ this.total.totalTokens >= this.limits.maxTokens
45
+ ) {
46
+ return false;
47
+ }
48
+ return true;
49
+ }
50
+
51
+ /** Lança quando o orçamento acabou. */
52
+ assertCanCall(): void {
53
+ if (!this.canCall) throw new AiBudgetError(this.describeExhaustion());
54
+ }
55
+
56
+ /** Registra o uso de uma chamada concluída. */
57
+ record(usage: AiUsage): void {
58
+ const merged = addUsage(this.total, usage);
59
+ this.total.inputTokens = merged.inputTokens;
60
+ this.total.outputTokens = merged.outputTokens;
61
+ this.total.totalTokens = merged.totalTokens;
62
+ this.total.calls = merged.calls;
63
+ }
64
+
65
+ /** Motivo legível do esgotamento. */
66
+ private describeExhaustion(): string {
67
+ if (
68
+ this.limits.maxCalls !== undefined &&
69
+ this.total.calls >= this.limits.maxCalls
70
+ ) {
71
+ return `Teto de ${this.limits.maxCalls} chamada(s) de IA por review atingido.`;
72
+ }
73
+ return `Teto de ${this.limits.maxTokens ?? 0} token(s) de IA por review atingido.`;
74
+ }
75
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Registro de uso de IA por review (`ai-usage.json`).
3
+ *
4
+ * Guarda uma entrada por chamada (feature, modelo, uso) e o total agregado.
5
+ * Nunca grava prompt, resposta ou segredo.
6
+ */
7
+
8
+ import { systemClock } from "../services/clock";
9
+ import type { StorageProvider } from "../storage/types";
10
+ import { type AiUsage, addUsage, emptyUsage } from "./types";
11
+
12
+ /** Entrada de uso de uma chamada. */
13
+ export type AiUsageEntry = {
14
+ feature: string;
15
+ model: string;
16
+ at: string;
17
+ usage: AiUsage;
18
+ };
19
+
20
+ /** Formato persistido em `ai-usage.json`. */
21
+ export type AiUsageFile = {
22
+ provider: string;
23
+ model: string;
24
+ total: AiUsage;
25
+ entries: AiUsageEntry[];
26
+ };
27
+
28
+ /** Acumulador de uso (em memória) com persistência opcional. */
29
+ export class AiUsageRecorder {
30
+ private readonly entries: AiUsageEntry[] = [];
31
+ private readonly provider: string;
32
+ private readonly model: string;
33
+
34
+ constructor(provider = "unknown", model = "unknown") {
35
+ this.provider = provider;
36
+ this.model = model;
37
+ }
38
+
39
+ /** Registra uma chamada concluída. */
40
+ record(feature: string, model: string, usage: AiUsage): void {
41
+ this.entries.push({ feature, model, at: systemClock(), usage: { ...usage } });
42
+ }
43
+
44
+ /** Total agregado de todas as chamadas. */
45
+ get total(): AiUsage {
46
+ return this.entries.reduce(
47
+ (acc, entry) => addUsage(acc, entry.usage),
48
+ emptyUsage(),
49
+ );
50
+ }
51
+
52
+ /** Snapshot serializável. */
53
+ toJSON(): AiUsageFile {
54
+ return {
55
+ provider: this.provider,
56
+ model: this.model,
57
+ total: this.total,
58
+ entries: [...this.entries],
59
+ };
60
+ }
61
+
62
+ /** Serializa para o artefato. */
63
+ serialize(): string {
64
+ return `${JSON.stringify(this.toJSON(), null, 2)}\n`;
65
+ }
66
+
67
+ /** Persiste `ai-usage.json` no storage (fail-open). */
68
+ write(storage: StorageProvider, reviewId: string): void {
69
+ try {
70
+ storage.writeArtifact(reviewId, {
71
+ name: "ai-usage.json",
72
+ kind: "ai-usage",
73
+ content: this.serialize(),
74
+ });
75
+ } catch {
76
+ // Observer: nunca derruba a revisão.
77
+ }
78
+ }
79
+ }
@@ -14,6 +14,17 @@ export const SKILL_DIR = join(import.meta.dir, "..", "skill");
14
14
  /** Diretório de assets de mídia do pacote (root `assets/` — logo etc.). */
15
15
  export const MEDIA_DIR = join(import.meta.dir, "..", "..", "assets");
16
16
 
17
+ /** Diretório das grammars tree-sitter embarcadas (wasm). */
18
+ export const GRAMMAR_DIR = join(MEDIA_DIR, "grammars");
19
+
20
+ /** Runtime e grammars copiados para a camada do projeto pelo setup/update. */
21
+ export const GRAMMAR_FILES = [
22
+ "web-tree-sitter.wasm",
23
+ "tree-sitter-typescript.wasm",
24
+ "tree-sitter-tsx.wasm",
25
+ "tree-sitter-javascript.wasm",
26
+ ] as const;
27
+
17
28
  /** Nome do arquivo de schema de output dos watchdogs. */
18
29
  export const OUTPUT_FORMAT_FILE = "OUTPUT-FORMAT.md";
19
30
 
@@ -29,3 +40,8 @@ export function assetPath(fileName: string): string {
29
40
  export function mediaPath(fileName: string): string {
30
41
  return join(MEDIA_DIR, fileName);
31
42
  }
43
+
44
+ /** Caminho absoluto de uma grammar wasm em `assets/grammars/`. */
45
+ export function grammarPath(fileName: string): string {
46
+ return join(GRAMMAR_DIR, fileName);
47
+ }