@payconductor/houndsman 0.4.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 (196) hide show
  1. package/README.md +35 -41
  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/commands/status-command.ts +46 -0
  11. package/src/cli/help.ts +18 -1
  12. package/src/cli/index.ts +13 -6
  13. package/src/core/ai/ai-client.ts +221 -0
  14. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  15. package/src/core/ai/errors.ts +36 -0
  16. package/src/core/ai/features/brief-enricher.ts +62 -0
  17. package/src/core/ai/features/classifier.ts +50 -0
  18. package/src/core/ai/features/context-summarizer.ts +31 -0
  19. package/src/core/ai/features/pre-analyzer.ts +51 -0
  20. package/src/core/ai/features/verifier.ts +45 -0
  21. package/src/core/ai/index.ts +56 -0
  22. package/src/core/ai/model-factory.ts +137 -0
  23. package/src/core/ai/prompts/base.ts +36 -0
  24. package/src/core/ai/prompts/brief.ts +87 -0
  25. package/src/core/ai/prompts/classification.ts +36 -0
  26. package/src/core/ai/prompts/context.ts +29 -0
  27. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  28. package/src/core/ai/prompts/verify.ts +40 -0
  29. package/src/core/ai/schemas.ts +110 -0
  30. package/src/core/ai/types.ts +76 -0
  31. package/src/core/ai/usage-budget.ts +75 -0
  32. package/src/core/ai/usage-recorder.ts +79 -0
  33. package/src/core/assets.ts +16 -0
  34. package/src/core/ast/diff.ts +220 -0
  35. package/src/core/ast/impact.ts +127 -0
  36. package/src/core/ast/parser.ts +167 -0
  37. package/src/core/ast/service.ts +233 -0
  38. package/src/core/ast/symbols.ts +294 -0
  39. package/src/core/ast/types.ts +125 -0
  40. package/src/core/brief.ts +138 -0
  41. package/src/core/commands/findings.ts +120 -6
  42. package/src/core/commands/review.ts +2 -0
  43. package/src/core/commands/scaffold.ts +159 -10
  44. package/src/core/commands/status.ts +63 -0
  45. package/src/core/commands/update.ts +5 -3
  46. package/src/core/config.ts +85 -0
  47. package/src/core/defaults.ts +27 -0
  48. package/src/core/domain/watchdog.ts +43 -5
  49. package/src/core/engine/houndsman-engine.ts +16 -1
  50. package/src/core/engine/review-scope.ts +77 -13
  51. package/src/core/engine/review-session.ts +27 -2
  52. package/src/core/env.ts +101 -0
  53. package/src/core/file-classifier.ts +25 -4
  54. package/src/core/findings-state.ts +21 -4
  55. package/src/core/github-review.ts +620 -57
  56. package/src/core/jev/jev-client.ts +8 -7
  57. package/src/core/jev/jev-verify.ts +2 -1
  58. package/src/core/logging/index.ts +17 -0
  59. package/src/core/logging/logger.ts +187 -0
  60. package/src/core/orchestrator.ts +454 -16
  61. package/src/core/paths.ts +3 -0
  62. package/src/core/project-config.ts +123 -1
  63. package/src/core/registry-check.ts +148 -6
  64. package/src/core/renderers/manifest-writer.ts +33 -0
  65. package/src/core/renderers/resume-renderer.ts +151 -0
  66. package/src/core/report-builder.ts +7 -4
  67. package/src/core/rule-text.ts +97 -0
  68. package/src/core/run-status.ts +39 -5
  69. package/src/core/services/file-classifier.ts +10 -3
  70. package/src/core/services/verifier.ts +14 -3
  71. package/src/core/storage/file/file-storage.ts +2 -0
  72. package/src/core/storage/paths.ts +6 -5
  73. package/src/core/storage/sqlite/mappers.ts +2 -0
  74. package/src/core/storage/sqlite/schema.ts +9 -1
  75. package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
  76. package/src/core/storage/types.ts +15 -0
  77. package/src/core/storage/work-area.ts +12 -0
  78. package/src/core/types.ts +37 -1
  79. package/src/core/verify.ts +17 -5
  80. package/src/harness/README.md +15 -0
  81. package/src/harness/agent/agent-context.ts +33 -0
  82. package/src/harness/agent/agent-errors.ts +30 -0
  83. package/src/harness/agent/agent-factory.ts +45 -0
  84. package/src/harness/agent/agent-reporter.ts +34 -0
  85. package/src/harness/agent/agent-result.ts +16 -0
  86. package/src/harness/agent/agent-runner.ts +32 -0
  87. package/src/harness/agent/agent-scheduler.ts +37 -0
  88. package/src/harness/agent/review-agent.ts +391 -0
  89. package/src/harness/config/harness-config.ts +84 -0
  90. package/src/harness/hunt/hunt-harness.ts +83 -0
  91. package/src/harness/hunt/hunt-options.ts +75 -0
  92. package/src/harness/hunt/hunt-result.ts +17 -0
  93. package/src/harness/hunt/hunt-session.ts +372 -0
  94. package/src/harness/hunt/review-planner.ts +98 -0
  95. package/src/harness/index.ts +36 -0
  96. package/src/harness/investigation/agent-state.ts +134 -0
  97. package/src/harness/investigation/evidence-verifier.ts +129 -0
  98. package/src/harness/investigation/evidence.ts +110 -0
  99. package/src/harness/investigation/finding-critic.ts +96 -0
  100. package/src/harness/investigation/investigation-planner.ts +118 -0
  101. package/src/harness/investigation/investigation-task.ts +10 -0
  102. package/src/harness/investigation/review-journal.ts +87 -0
  103. package/src/harness/investigation/rule-analyzer.ts +178 -0
  104. package/src/harness/investigation/rule-signals.ts +110 -0
  105. package/src/harness/prompts/agent-schemas.ts +57 -0
  106. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  107. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  108. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  109. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  110. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  111. package/src/harness/tools/agent-tool.ts +102 -0
  112. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  113. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  114. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  115. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  116. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  117. package/src/harness/tools/builtin/helpers.ts +115 -0
  118. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  119. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  120. package/src/harness/tools/builtin/index.ts +59 -0
  121. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  122. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  123. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  124. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  125. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  126. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  127. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  128. package/src/harness/tools/path-guard.ts +90 -0
  129. package/src/harness/tools/result-limiter.ts +49 -0
  130. package/src/harness/tools/secret-redactor.ts +54 -0
  131. package/src/harness/tools/tool-cache.ts +50 -0
  132. package/src/harness/tools/tool-call-budget.ts +60 -0
  133. package/src/harness/tools/tool-context.ts +5 -0
  134. package/src/harness/tools/tool-middleware.ts +105 -0
  135. package/src/harness/tools/tool-policy.ts +51 -0
  136. package/src/harness/tools/tool-registry.ts +112 -0
  137. package/src/harness/tools/tool-result.ts +15 -0
  138. package/src/harness/tools/toolbox.ts +36 -0
  139. package/src/harness/validation/finding-validator.ts +119 -0
  140. package/src/index.ts +46 -0
  141. package/src/server/client/App.tsx +26 -9
  142. package/src/server/client/ArtifactsPanel.tsx +12 -5
  143. package/src/server/client/AutoRefresh.tsx +1 -1
  144. package/src/server/client/Filters.tsx +7 -2
  145. package/src/server/client/FindingCard.tsx +14 -10
  146. package/src/server/client/FindingsList.tsx +2 -2
  147. package/src/server/client/MarkdownText.tsx +92 -12
  148. package/src/server/client/ResumePanel.tsx +368 -0
  149. package/src/server/client/RunList.tsx +3 -3
  150. package/src/server/client/SubmitModal.tsx +49 -6
  151. package/src/server/client/SummaryBar.tsx +1 -1
  152. package/src/server/client/icons.tsx +17 -0
  153. package/src/server/client/{constants.ts → utils/constants.ts} +61 -10
  154. package/src/server/client/utils/markdown.ts +319 -0
  155. package/src/server/client/{types.ts → utils/types.ts} +64 -0
  156. package/src/server/index.ts +66 -7
  157. package/src/server/run-repository.ts +9 -0
  158. package/src/skill/OUTPUT-FORMAT.md +12 -0
  159. package/src/skill/SETUP.md +6 -2
  160. package/src/skill/example-review.md +22 -14
  161. package/src/skill/hunt.md +45 -19
  162. package/src/skill/verify.md +3 -2
  163. package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
  164. package/src/tests/ai-core.test.ts +120 -0
  165. package/src/tests/ai-features.test.ts +183 -0
  166. package/src/tests/ast.test.ts +335 -0
  167. package/src/tests/baseline.test.ts +3 -0
  168. package/src/tests/code-highlight.test.ts +1 -1
  169. package/src/tests/env.test.ts +69 -0
  170. package/src/tests/fallback.test.ts +172 -0
  171. package/src/tests/findings-state.test.ts +50 -2
  172. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  173. package/src/tests/fixtures/fake-investigation.ts +64 -0
  174. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  175. package/src/tests/github-review.test.ts +409 -60
  176. package/src/tests/harness-agent.test.ts +122 -0
  177. package/src/tests/harness-investigation.test.ts +206 -0
  178. package/src/tests/harness-tools.test.ts +148 -0
  179. package/src/tests/hunt-command.test.ts +212 -0
  180. package/src/tests/logger.test.ts +83 -0
  181. package/src/tests/markdown.test.ts +61 -1
  182. package/src/tests/review-resume.test.ts +101 -0
  183. package/src/tests/review-scope.test.ts +302 -0
  184. package/src/tests/review-scripts.test.ts +73 -0
  185. package/src/tests/rule-analyzer.test.ts +61 -0
  186. package/src/tests/run-status-view.test.ts +94 -0
  187. package/src/tests/run-status.test.ts +53 -0
  188. package/src/tests/server.test.ts +76 -21
  189. package/src/tests/storage.test.ts +63 -0
  190. package/src/tests/tool-contracts.test.ts +82 -0
  191. package/src/tests/verify.test.ts +1 -1
  192. package/src/server/client/markdown.ts +0 -153
  193. /package/src/server/client/{api.ts → utils/api.ts} +0 -0
  194. /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
  195. /package/src/server/client/{format.ts → utils/format.ts} +0 -0
  196. /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Fachada de IA (`core/ai`) — fail-open, budget e uso.
3
+ *
4
+ * As features do core usam `generateObject`/`generateText` e tratam `ok:false`
5
+ * como "sem IA": a revisão determinística segue idêntica. O harness nativo usa
6
+ * `requireProvider` e falha com mensagem clara quando a IA não está configurada.
7
+ */
8
+
9
+ import type { ZodType } from "zod";
10
+ import type { ProjectConfig } from "../project-config";
11
+ import { AiConfigurationError, errorMessage } from "./errors";
12
+ import { ModelFactory } from "./model-factory";
13
+ import type { AiResult, AiTool, AiUsage, ModelProvider, ModelRequest } from "./types";
14
+ import { AiUsageBudget } from "./usage-budget";
15
+ import { AiUsageRecorder } from "./usage-recorder";
16
+
17
+ /** Opções de construção do cliente. */
18
+ export type AiClientOptions = {
19
+ project: ProjectConfig;
20
+ /** Provider injetado (testes) — dispensa chave/rede. */
21
+ provider?: ModelProvider | null;
22
+ factory?: ModelFactory;
23
+ recorder?: AiUsageRecorder;
24
+ budget?: AiUsageBudget;
25
+ };
26
+
27
+ /** Pedido de objeto estruturado. */
28
+ export type ObjectRequest<T> = {
29
+ instructions?: string;
30
+ prompt: string;
31
+ outputSchema: ZodType<T>;
32
+ tools?: Record<string, AiTool>;
33
+ maxSteps?: number;
34
+ maxOutputTokens?: number;
35
+ temperature?: number;
36
+ signal?: AbortSignal;
37
+ };
38
+
39
+ /** Pedido de texto livre. */
40
+ export type TextRequest = Omit<ObjectRequest<string>, "outputSchema">;
41
+
42
+ /** Resultado da validação de credencial (doctor). */
43
+ export type AiCredentialCheck = {
44
+ ok: boolean;
45
+ kind: "ok" | "missing" | "invalid" | "unreachable";
46
+ detail: string;
47
+ model?: string;
48
+ };
49
+
50
+ /** Cliente de IA compartilhado pela review. */
51
+ export class AiClient {
52
+ private readonly project: ProjectConfig;
53
+ private readonly factory: ModelFactory;
54
+ private readonly override: ModelProvider | null | undefined;
55
+ readonly recorder: AiUsageRecorder;
56
+ readonly budget: AiUsageBudget;
57
+
58
+ constructor(options: AiClientOptions) {
59
+ this.project = options.project;
60
+ this.factory = options.factory ?? new ModelFactory(options.project);
61
+ this.override = options.provider;
62
+ this.recorder =
63
+ options.recorder ??
64
+ new AiUsageRecorder(this.project.aiProvider, this.project.aiModel);
65
+ this.budget =
66
+ options.budget ??
67
+ new AiUsageBudget({
68
+ maxCalls: this.project.aiMaxCallsPerReview,
69
+ maxTokens: this.project.aiMaxTokensPerReview,
70
+ });
71
+ }
72
+
73
+ get modelId(): string {
74
+ return this.project.aiModel;
75
+ }
76
+
77
+ /** Configuração resolvida do projeto (features, budgets). */
78
+ get projectConfig(): ProjectConfig {
79
+ return this.project;
80
+ }
81
+
82
+ /** Features resolvidas (context/brief ligadas por default). */
83
+ get features() {
84
+ return this.project.aiFeatures;
85
+ }
86
+
87
+ /** `true` quando há provider disponível (injetado ou configurado + chave). */
88
+ get available(): boolean {
89
+ if (this.override !== undefined) return this.override !== null;
90
+ return this.factory.isConfigured;
91
+ }
92
+
93
+ /** Provider obrigatório — usado pelo harness (falha clara sem IA). */
94
+ async requireProvider(model?: string): Promise<ModelProvider> {
95
+ if (this.override === null) {
96
+ throw new AiConfigurationError(
97
+ "IA indisponível: provider injetado como null.",
98
+ );
99
+ }
100
+ if (this.override) return this.override;
101
+ return this.factory.create({ model });
102
+ }
103
+
104
+ /** Geração de objeto estruturado, fail-open. */
105
+ async generateObject<T>(
106
+ feature: string,
107
+ request: ObjectRequest<T>,
108
+ model?: string,
109
+ ): Promise<AiResult<T>> {
110
+ return this.run(
111
+ feature,
112
+ { ...request, outputSchema: request.outputSchema },
113
+ model,
114
+ );
115
+ }
116
+
117
+ /** Geração de texto, fail-open. */
118
+ async generateText(
119
+ feature: string,
120
+ request: TextRequest,
121
+ model?: string,
122
+ ): Promise<AiResult<string>> {
123
+ return this.run<string>(feature, { ...request }, model);
124
+ }
125
+
126
+ /**
127
+ * Ping mínimo para o `doctor`: valida a credencial sem gastar tokens
128
+ * relevantes. Não lança; devolve o diagnóstico.
129
+ */
130
+ async validateCredential(
131
+ options: { timeoutMs?: number } = {},
132
+ ): Promise<AiCredentialCheck> {
133
+ if (this.override === null || !this.available) {
134
+ return {
135
+ ok: false,
136
+ kind: "missing",
137
+ detail: "provider/chave de IA não configurados",
138
+ };
139
+ }
140
+ const timeoutMs = Math.max(
141
+ 1_000,
142
+ options.timeoutMs ?? Math.min(this.project.aiTimeoutMs, 8_000),
143
+ );
144
+ const result = await this.generateText("doctor-ping", {
145
+ prompt: "Responda apenas OK.",
146
+ maxSteps: 1,
147
+ maxOutputTokens: 4,
148
+ temperature: 0,
149
+ signal: AbortSignal.timeout(timeoutMs),
150
+ });
151
+ if (result.ok) {
152
+ return {
153
+ ok: true,
154
+ kind: "ok",
155
+ detail: "credencial válida",
156
+ model: result.model,
157
+ };
158
+ }
159
+ const message = result.error ?? "falha desconhecida";
160
+ if (
161
+ /401|403|unauthorized|invalid.*(key|token)|authentication|api.?key/i.test(
162
+ message,
163
+ )
164
+ ) {
165
+ return { ok: false, kind: "invalid", detail: message };
166
+ }
167
+ return { ok: false, kind: "unreachable", detail: message };
168
+ }
169
+
170
+ /** Executa a chamada respeitando budget, timeout e uso. */
171
+ private async run<T>(
172
+ feature: string,
173
+ request: Omit<ObjectRequest<T>, "outputSchema"> & {
174
+ outputSchema?: ZodType<T>;
175
+ },
176
+ model?: string,
177
+ ): Promise<AiResult<T>> {
178
+ if (!this.available) {
179
+ return { ok: false, error: "IA não configurada (fail-open)." };
180
+ }
181
+ try {
182
+ this.budget.assertCanCall();
183
+ } catch (error) {
184
+ return { ok: false, error: errorMessage(error) };
185
+ }
186
+
187
+ try {
188
+ const provider = await this.requireProvider(model);
189
+ const response = await provider.generate({
190
+ ...request,
191
+ signal: combineSignals(request.signal, this.project.aiTimeoutMs),
192
+ });
193
+ this.budget.record(response.usage);
194
+ this.recorder.record(feature, provider.modelId, response.usage);
195
+ return {
196
+ ok: true,
197
+ output: response.output,
198
+ usage: response.usage,
199
+ model: provider.modelId,
200
+ };
201
+ } catch (error) {
202
+ return { ok: false, error: errorMessage(error) };
203
+ }
204
+ }
205
+ }
206
+
207
+ /** Combina o sinal do chamador com o timeout configurado. */
208
+ function combineSignals(
209
+ signal: AbortSignal | undefined,
210
+ timeoutMs: number,
211
+ ): AbortSignal | undefined {
212
+ const timeout =
213
+ Number.isFinite(timeoutMs) && timeoutMs > 0
214
+ ? AbortSignal.timeout(timeoutMs)
215
+ : undefined;
216
+ if (!signal) return timeout;
217
+ if (!timeout) return signal;
218
+ return AbortSignal.any([signal, timeout]);
219
+ }
220
+
221
+ export type { AiUsage, ModelRequest };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Adapter do AI SDK (`generateText` + `Output.object` + tools).
3
+ *
4
+ * O resto do motor depende só de `ModelProvider`; trocar de SDK é trocar este
5
+ * arquivo. O loop de tools é do SDK (`stepCountIs`), com timeout/cancelamento
6
+ * via `AbortSignal`.
7
+ */
8
+
9
+ import type { LanguageModel, ToolSet } from "ai";
10
+ import { generateText, Output, stepCountIs, tool } from "ai";
11
+ import type { AiProviderKind } from "../config";
12
+ import { AiProviderError } from "./errors";
13
+ import {
14
+ type AiTool,
15
+ type AiUsage,
16
+ emptyUsage,
17
+ type ModelProvider,
18
+ type ModelRequest,
19
+ type ModelResponse,
20
+ } from "./types";
21
+
22
+ /** Converte as tools do motor para o formato do AI SDK. */
23
+ function toToolSet(tools: Record<string, AiTool>): ToolSet {
24
+ const entries = Object.entries(tools).map(([name, item]) => [
25
+ name,
26
+ tool({
27
+ description: item.description,
28
+ inputSchema: item.inputSchema,
29
+ execute: async (input: unknown) => item.execute(input),
30
+ }),
31
+ ]);
32
+ return Object.fromEntries(entries) as ToolSet;
33
+ }
34
+
35
+ /** Mapeia o uso do SDK de forma defensiva (campos opcionais por provider). */
36
+ export function mapUsage(raw: unknown): AiUsage {
37
+ const usage = (raw ?? {}) as Record<string, unknown>;
38
+ const input = numberOrZero(usage.inputTokens ?? usage.promptTokens);
39
+ const output = numberOrZero(usage.outputTokens ?? usage.completionTokens);
40
+ const total = numberOrZero(usage.totalTokens) || input + output;
41
+ return { inputTokens: input, outputTokens: output, totalTokens: total, calls: 1 };
42
+ }
43
+
44
+ function numberOrZero(value: unknown): number {
45
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
46
+ }
47
+
48
+ /** Provider de modelo apoiado no AI SDK. */
49
+ export class AiSdkModelProvider implements ModelProvider {
50
+ constructor(
51
+ readonly name: AiProviderKind,
52
+ readonly modelId: string,
53
+ private readonly model: LanguageModel,
54
+ ) {}
55
+
56
+ async generate<T>(request: ModelRequest<T>): Promise<ModelResponse<T>> {
57
+ const tools = request.tools ? toToolSet(request.tools) : undefined;
58
+ const hasTools = tools !== undefined && Object.keys(tools).length > 0;
59
+ const maxSteps = request.maxSteps ?? 1;
60
+
61
+ try {
62
+ const result = await generateText({
63
+ model: this.model,
64
+ system: request.instructions,
65
+ prompt: request.prompt,
66
+ ...(hasTools ? { tools, stopWhen: stepCountIs(maxSteps) } : {}),
67
+ ...(request.outputSchema
68
+ ? { output: Output.object({ schema: request.outputSchema }) }
69
+ : {}),
70
+ ...(request.maxOutputTokens
71
+ ? { maxOutputTokens: request.maxOutputTokens }
72
+ : {}),
73
+ ...(request.temperature !== undefined
74
+ ? { temperature: request.temperature }
75
+ : {}),
76
+ ...(request.signal ? { abortSignal: request.signal } : {}),
77
+ maxRetries: 0,
78
+ });
79
+
80
+ const output = request.outputSchema
81
+ ? (result.output as T)
82
+ : ((result.text ?? "") as unknown as T);
83
+ return {
84
+ output,
85
+ usage: mapUsage(result.usage),
86
+ steps: result.steps?.length ?? 1,
87
+ finishReason: result.finishReason ?? "unknown",
88
+ };
89
+ } catch (error) {
90
+ if (error instanceof Error && error.name === "AbortError") {
91
+ throw new AiProviderError("Chamada de IA cancelada ou expirada.", error);
92
+ }
93
+ throw new AiProviderError(
94
+ error instanceof Error ? error.message : String(error),
95
+ error,
96
+ );
97
+ }
98
+ }
99
+ }
100
+
101
+ export { emptyUsage };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Erros da camada de IA — mensagens claras e sem segredos.
3
+ */
4
+
5
+ /** Configuração ausente/inválida (provider, modelo ou chave). */
6
+ export class AiConfigurationError extends Error {
7
+ constructor(message: string) {
8
+ super(message);
9
+ this.name = "AiConfigurationError";
10
+ }
11
+ }
12
+
13
+ /** Teto de chamadas/tokens por review estourado. */
14
+ export class AiBudgetError extends Error {
15
+ constructor(message: string) {
16
+ super(message);
17
+ this.name = "AiBudgetError";
18
+ }
19
+ }
20
+
21
+ /** Falha do provider (rede, timeout, resposta inválida). */
22
+ export class AiProviderError extends Error {
23
+ constructor(
24
+ message: string,
25
+ readonly cause?: unknown,
26
+ ) {
27
+ super(message);
28
+ this.name = "AiProviderError";
29
+ }
30
+ }
31
+
32
+ /** Mensagem segura de um erro desconhecido. */
33
+ export function errorMessage(error: unknown): string {
34
+ if (error instanceof Error) return error.message;
35
+ return String(error);
36
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Enriquecimento de briefs via IA (`AiBriefEnricher`).
3
+ *
4
+ * Uma chamada por watchdog, concorrência limitada. A saída é um plano sugerido
5
+ * (`BriefPlan`) — nunca cria regra nem achado. Falha → brief inalterado.
6
+ */
7
+
8
+ import type { AiClient } from "../ai-client";
9
+ import { type BriefPromptInput, briefInstructions, briefPrompt } from "../prompts/brief";
10
+ import { type BriefPlan, BriefPlanSchema } from "../schemas";
11
+
12
+ /** Enriquecimento de briefs (fail-open por watchdog). */
13
+ export class AiBriefEnricher {
14
+ constructor(private readonly client: AiClient) {}
15
+
16
+ /** `true` quando a feature está ligada e há IA disponível. */
17
+ get enabled(): boolean {
18
+ return this.client.available && this.client.features.brief;
19
+ }
20
+
21
+ /** Plano de um watchdog; `null` em falha. */
22
+ async enrich(input: BriefPromptInput): Promise<BriefPlan | null> {
23
+ if (!this.enabled) return null;
24
+ const result = await this.client.generateObject<BriefPlan>(
25
+ "brief",
26
+ {
27
+ instructions: briefInstructions(),
28
+ prompt: briefPrompt(input),
29
+ outputSchema: BriefPlanSchema,
30
+ maxSteps: 1,
31
+ maxOutputTokens: 700,
32
+ },
33
+ this.client.projectConfig.aiModelFor(input.watchdogId),
34
+ );
35
+ if (!result.ok) return null;
36
+ return result.output;
37
+ }
38
+
39
+ /** Enriquece vários watchdogs com concorrência limitada. */
40
+ async enrichAll(
41
+ inputs: readonly BriefPromptInput[],
42
+ ): Promise<Map<string, BriefPlan>> {
43
+ const plans = new Map<string, BriefPlan>();
44
+ if (!this.enabled || inputs.length === 0) return plans;
45
+ const concurrency = Math.max(1, this.client.projectConfig.aiConcurrency);
46
+ let cursor = 0;
47
+
48
+ const worker = async (): Promise<void> => {
49
+ while (cursor < inputs.length) {
50
+ const input = inputs[cursor++];
51
+ if (!input) return;
52
+ const plan = await this.enrich(input);
53
+ if (plan) plans.set(input.watchdogId, plan);
54
+ }
55
+ };
56
+
57
+ await Promise.all(
58
+ Array.from({ length: Math.min(concurrency, inputs.length) }, () => worker()),
59
+ );
60
+ return plans;
61
+ }
62
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Classificação fallback por IA (arquivos que nenhum watchdog casou).
3
+ */
4
+
5
+ import type { AiClient } from "../ai-client";
6
+ import {
7
+ classificationInstructions,
8
+ classificationPrompt,
9
+ } from "../prompts/classification";
10
+ import { type AiClassification, ClassificationSchema } from "../schemas";
11
+
12
+ /** Atribui arquivos não classificados a watchdogs conhecidos. */
13
+ export class AiClassifier {
14
+ constructor(private readonly client: AiClient) {}
15
+
16
+ get enabled(): boolean {
17
+ return this.client.available && this.client.features.classification;
18
+ }
19
+
20
+ /**
21
+ * Devolve `arquivo → watchdogId` apenas para ids válidos. Arquivos sem
22
+ * atribuição simplesmente continuam não classificados.
23
+ */
24
+ async classify(
25
+ files: readonly string[],
26
+ watchdogs: readonly { id: string; name: string; scope: string }[],
27
+ ): Promise<Map<string, string>> {
28
+ const assignments = new Map<string, string>();
29
+ if (!this.enabled || files.length === 0 || watchdogs.length === 0) {
30
+ return assignments;
31
+ }
32
+ const known = new Set(watchdogs.map((item) => item.id));
33
+ const filesSet = new Set(files);
34
+ const result = await this.client.generateObject<AiClassification>(
35
+ "classification",
36
+ {
37
+ instructions: classificationInstructions(),
38
+ prompt: classificationPrompt({ files, watchdogs }),
39
+ outputSchema: ClassificationSchema,
40
+ maxSteps: 1,
41
+ },
42
+ );
43
+ if (!result.ok) return assignments;
44
+ for (const item of result.output.assignments) {
45
+ if (!known.has(item.watchdogId) || !filesSet.has(item.file)) continue;
46
+ assignments.set(item.file, item.watchdogId);
47
+ }
48
+ return assignments;
49
+ }
50
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Resumo do contexto da PR via IA (fallback ao subagente de contexto).
3
+ */
4
+
5
+ import { type ContextSummary, parseContextSummary } from "../../report-builder";
6
+ import type { AiClient } from "../ai-client";
7
+ import { contextInstructions, contextPrompt } from "../prompts/context";
8
+ import { type ContextSummaryPlan, ContextSummarySchema } from "../schemas";
9
+
10
+ /** Resume `context.json` em um `ContextSummary` curto. */
11
+ export class AiContextSummarizer {
12
+ constructor(private readonly client: AiClient) {}
13
+
14
+ /** `true` quando a feature está ligada e há IA disponível. */
15
+ get enabled(): boolean {
16
+ return this.client.available && this.client.features.context;
17
+ }
18
+
19
+ /** Falha-aberto: sem IA ou saída inválida → `null`. */
20
+ async summarize(raw: string): Promise<ContextSummary | null> {
21
+ if (!this.enabled) return null;
22
+ const result = await this.client.generateObject<ContextSummaryPlan>("context", {
23
+ instructions: contextInstructions(),
24
+ prompt: contextPrompt(raw),
25
+ outputSchema: ContextSummarySchema,
26
+ maxSteps: 1,
27
+ });
28
+ if (!result.ok) return null;
29
+ return parseContextSummary({ ...result.output, available: true });
30
+ }
31
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Pré-análise por IA (substitui o Jev quando a feature está ligada).
3
+ *
4
+ * Produz candidatos no mesmo shape do Jev — nunca bloqueiam a revisão.
5
+ */
6
+
7
+ import type { JevCandidate } from "../../jev";
8
+ import type { AiClient } from "../ai-client";
9
+ import {
10
+ type PreAnalysisPromptInput,
11
+ preAnalysisInstructions,
12
+ preAnalysisPrompt,
13
+ } from "../prompts/pre-analysis";
14
+ import { type AiCandidate, CandidateSchema } from "../schemas";
15
+
16
+ /** Gera candidatos de pré-análise para um watchdog. */
17
+ export class AiPreAnalyzer {
18
+ constructor(private readonly client: AiClient) {}
19
+
20
+ get enabled(): boolean {
21
+ return this.client.available && this.client.features.preAnalysis;
22
+ }
23
+
24
+ /** Candidatos validados contra as regras e arquivos fornecidos. */
25
+ async analyze(input: PreAnalysisPromptInput): Promise<JevCandidate[]> {
26
+ if (!this.enabled || input.files.length === 0) return [];
27
+ const rules = new Set(input.rules.map((rule) => rule.text.trim()));
28
+ const files = new Set(input.files.map((item) => item.file));
29
+ const result = await this.client.generateObject<{ candidates: AiCandidate[] }>(
30
+ "pre-analysis",
31
+ {
32
+ instructions: preAnalysisInstructions(),
33
+ prompt: preAnalysisPrompt(input),
34
+ outputSchema: CandidateSchema,
35
+ maxSteps: 1,
36
+ },
37
+ this.client.projectConfig.aiModelFor(input.watchdogId),
38
+ );
39
+ if (!result.ok) return [];
40
+ return result.output.candidates
41
+ .filter((item) => files.has(item.file) && rules.has(item.rule.trim()))
42
+ .map((item) => ({
43
+ watchdogId: input.watchdogId,
44
+ rule: item.rule,
45
+ severity: item.severity,
46
+ file: item.file,
47
+ confidence: item.confidence,
48
+ questionKey: "ai",
49
+ }));
50
+ }
51
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Verificação de findings por IA (casos ambíguos do `verify`).
3
+ *
4
+ * Retorna `id → confiança` para o `applyConfidence` do core. Falha-aberto:
5
+ * ids ausentes mantêm o provisório local.
6
+ */
7
+
8
+ import type { AiClient } from "../ai-client";
9
+ import {
10
+ type VerifyPromptInput,
11
+ verifyInstructions,
12
+ verifyPrompt,
13
+ } from "../prompts/verify";
14
+ import { type AiVerdict, VerifySchema } from "../schemas";
15
+
16
+ /** Confirma/refuta findings ambíguos. */
17
+ export class AiVerifier {
18
+ constructor(private readonly client: AiClient) {}
19
+
20
+ get enabled(): boolean {
21
+ return this.client.available && this.client.features.verify;
22
+ }
23
+
24
+ /** Mapa id → confiança (0–1) de persistência. */
25
+ async verify(input: VerifyPromptInput): Promise<Map<string, number>> {
26
+ const confidence = new Map<string, number>();
27
+ if (!this.enabled || input.findings.length === 0) return confidence;
28
+ const ids = new Set(input.findings.map((item) => item.id));
29
+ const result = await this.client.generateObject<{ verdicts: AiVerdict[] }>(
30
+ "verify",
31
+ {
32
+ instructions: verifyInstructions(),
33
+ prompt: verifyPrompt(input),
34
+ outputSchema: VerifySchema,
35
+ maxSteps: 1,
36
+ },
37
+ );
38
+ if (!result.ok) return confidence;
39
+ for (const verdict of result.output.verdicts) {
40
+ if (!ids.has(verdict.id)) continue;
41
+ confidence.set(verdict.id, verdict.persisting ? verdict.confidence : 0);
42
+ }
43
+ return confidence;
44
+ }
45
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Barrel da camada de IA (`core/ai`).
3
+ */
4
+
5
+ export type { AiProviderKind } from "../config";
6
+ export type {
7
+ AiClientOptions,
8
+ AiCredentialCheck,
9
+ ObjectRequest,
10
+ TextRequest,
11
+ } from "./ai-client";
12
+ export { AiClient } from "./ai-client";
13
+ export { AiSdkModelProvider, mapUsage } from "./ai-sdk-model-provider";
14
+ export {
15
+ AiBudgetError,
16
+ AiConfigurationError,
17
+ AiProviderError,
18
+ errorMessage,
19
+ } from "./errors";
20
+ export { AiBriefEnricher } from "./features/brief-enricher";
21
+ export { AiClassifier } from "./features/classifier";
22
+ export { AiContextSummarizer } from "./features/context-summarizer";
23
+ export { AiPreAnalyzer } from "./features/pre-analyzer";
24
+ export { AiVerifier } from "./features/verifier";
25
+ export { hasProviderCredential, ModelFactory } from "./model-factory";
26
+ export {
27
+ type AiCandidate,
28
+ type AiClassification,
29
+ type AiVerdict,
30
+ type AiWatchdogReport,
31
+ type BriefPlan,
32
+ BriefPlanSchema,
33
+ CandidateSchema,
34
+ ClassificationSchema,
35
+ type ContextSummaryPlan,
36
+ ContextSummarySchema,
37
+ SeveritySchema,
38
+ VerifySchema,
39
+ WatchdogReportSchema,
40
+ } from "./schemas";
41
+ export {
42
+ type AiResult,
43
+ type AiTool,
44
+ type AiUsage,
45
+ addUsage,
46
+ emptyUsage,
47
+ type ModelProvider,
48
+ type ModelRequest,
49
+ type ModelResponse,
50
+ } from "./types";
51
+ export { type AiBudgetLimits, AiUsageBudget } from "./usage-budget";
52
+ export {
53
+ type AiUsageEntry,
54
+ type AiUsageFile,
55
+ AiUsageRecorder,
56
+ } from "./usage-recorder";