@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.
- package/README.md +35 -41
- 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/status-command.ts +46 -0
- package/src/cli/help.ts +18 -1
- package/src/cli/index.ts +13 -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 +120 -6
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/status.ts +63 -0
- 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 +77 -13
- 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/findings-state.ts +21 -4
- package/src/core/github-review.ts +620 -57
- 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 +454 -16
- package/src/core/paths.ts +3 -0
- 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/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/rule-text.ts +97 -0
- package/src/core/run-status.ts +39 -5
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/file/file-storage.ts +2 -0
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/sqlite/mappers.ts +2 -0
- package/src/core/storage/sqlite/schema.ts +9 -1
- package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
- package/src/core/storage/types.ts +15 -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 +26 -9
- package/src/server/client/ArtifactsPanel.tsx +12 -5
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +14 -10
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +92 -12
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +49 -6
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/{constants.ts → utils/constants.ts} +61 -10
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +64 -0
- package/src/server/index.ts +66 -7
- package/src/server/run-repository.ts +9 -0
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/example-review.md +22 -14
- package/src/skill/hunt.md +45 -19
- package/src/skill/verify.md +3 -2
- package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
- 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/baseline.test.ts +3 -0
- package/src/tests/code-highlight.test.ts +1 -1
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/findings-state.test.ts +50 -2
- 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 +409 -60
- 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/markdown.test.ts +61 -1
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +73 -0
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +53 -0
- package/src/tests/server.test.ts +76 -21
- package/src/tests/storage.test.ts +63 -0
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- package/src/server/client/markdown.ts +0 -153
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
- /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
|
@@ -6,9 +6,25 @@
|
|
|
6
6
|
* O contrato do consumidor (`houndsman.config.ts`) não muda.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
AiProviderKind,
|
|
11
|
+
HoundsmanAi,
|
|
12
|
+
HoundsmanAiFeatures,
|
|
13
|
+
HoundsmanAst,
|
|
14
|
+
HoundsmanConfig,
|
|
15
|
+
HoundsmanStorage,
|
|
16
|
+
} from "./config";
|
|
10
17
|
import {
|
|
18
|
+
DEFAULT_AI_CONCURRENCY,
|
|
19
|
+
DEFAULT_AI_MAX_CALLS_PER_REVIEW,
|
|
20
|
+
DEFAULT_AI_MAX_OUTPUT_TOKENS,
|
|
21
|
+
DEFAULT_AI_MAX_STEPS,
|
|
22
|
+
DEFAULT_AI_MODEL,
|
|
23
|
+
DEFAULT_AI_PROVIDER,
|
|
24
|
+
DEFAULT_AI_TIMEOUT_MS,
|
|
11
25
|
DEFAULT_ALWAYS_INCLUDE_DOCS,
|
|
26
|
+
DEFAULT_AST_MAX_FILE_BYTES,
|
|
27
|
+
DEFAULT_AST_MAX_IMPACT_FILES,
|
|
12
28
|
DEFAULT_JEV_CONCURRENCY,
|
|
13
29
|
DEFAULT_MAX_FILES_PER_BRIEF,
|
|
14
30
|
} from "./defaults";
|
|
@@ -32,6 +48,26 @@ export class ProjectConfig<TId extends string = string> {
|
|
|
32
48
|
return this.raw.storage ?? {};
|
|
33
49
|
}
|
|
34
50
|
|
|
51
|
+
/** Config do AST (objeto vazio quando ausente). */
|
|
52
|
+
get ast(): HoundsmanAst {
|
|
53
|
+
return this.raw.ast ?? {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** AST habilitado (default: ligado; falhas de grammar são fail-open). */
|
|
57
|
+
get astEnabled(): boolean {
|
|
58
|
+
return this.ast.enabled !== false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get astMaxFileBytes(): number {
|
|
62
|
+
const value = this.ast.maxFileBytes;
|
|
63
|
+
return value !== undefined && value > 0 ? value : DEFAULT_AST_MAX_FILE_BYTES;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get astMaxImpactFiles(): number {
|
|
67
|
+
const value = this.ast.maxImpactFiles;
|
|
68
|
+
return value !== undefined && value >= 0 ? value : DEFAULT_AST_MAX_IMPACT_FILES;
|
|
69
|
+
}
|
|
70
|
+
|
|
35
71
|
get maxFilesPerBrief(): number {
|
|
36
72
|
const value = this.raw.budgets?.maxFilesPerBrief;
|
|
37
73
|
return value !== undefined && value > 0 ? value : DEFAULT_MAX_FILES_PER_BRIEF;
|
|
@@ -53,4 +89,90 @@ export class ProjectConfig<TId extends string = string> {
|
|
|
53
89
|
get docMappings(): readonly (readonly [RegExp, readonly string[]])[] {
|
|
54
90
|
return this.raw.docMappings;
|
|
55
91
|
}
|
|
92
|
+
|
|
93
|
+
/** Config de IA (objeto vazio quando ausente). */
|
|
94
|
+
get ai(): HoundsmanAi {
|
|
95
|
+
return this.raw.ai ?? {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** IA habilitada (default: ligada; sem chave é fail-open). */
|
|
99
|
+
get aiEnabled(): boolean {
|
|
100
|
+
return this.ai.enabled !== false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get aiProvider(): AiProviderKind {
|
|
104
|
+
return this.ai.provider ?? DEFAULT_AI_PROVIDER;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get aiModel(): string {
|
|
108
|
+
const value = this.ai.model?.trim();
|
|
109
|
+
return value && value.length > 0 ? value : DEFAULT_AI_MODEL;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get aiConcurrency(): number {
|
|
113
|
+
return positive(this.ai.concurrency, DEFAULT_AI_CONCURRENCY);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get aiMaxSteps(): number {
|
|
117
|
+
return positive(this.ai.maxSteps, DEFAULT_AI_MAX_STEPS);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get aiMaxOutputTokens(): number {
|
|
121
|
+
return positive(this.ai.maxOutputTokens, DEFAULT_AI_MAX_OUTPUT_TOKENS);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get aiTemperature(): number {
|
|
125
|
+
const value = this.ai.temperature;
|
|
126
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get aiTimeoutMs(): number {
|
|
130
|
+
return positive(this.ai.timeoutMs, DEFAULT_AI_TIMEOUT_MS);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get aiMaxCallsPerReview(): number {
|
|
134
|
+
return positive(this.ai.maxCallsPerReview, DEFAULT_AI_MAX_CALLS_PER_REVIEW);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Teto de tokens por review (0/ausente = sem teto). */
|
|
138
|
+
get aiMaxTokensPerReview(): number | undefined {
|
|
139
|
+
const value = this.ai.maxTokensPerReview;
|
|
140
|
+
return value !== undefined && value > 0 ? value : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Features resolvidas com defaults (`context`/`brief` ligadas). */
|
|
144
|
+
get aiFeatures(): Required<HoundsmanAiFeatures> {
|
|
145
|
+
const features = this.ai.features ?? {};
|
|
146
|
+
return {
|
|
147
|
+
context: features.context !== false,
|
|
148
|
+
brief: features.brief !== false,
|
|
149
|
+
classification: features.classification === true,
|
|
150
|
+
preAnalysis: features.preAnalysis === true,
|
|
151
|
+
verify: features.verify === true,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Modelo efetivo de um watchdog (override por watchdog ou global). */
|
|
156
|
+
aiModelFor(watchdogId: string): string {
|
|
157
|
+
const override = this.ai.watchdogs?.[watchdogId]?.model?.trim();
|
|
158
|
+
return override && override.length > 0 ? override : this.aiModel;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Passos efetivos de um watchdog (override por watchdog ou global). */
|
|
162
|
+
aiMaxStepsFor(watchdogId: string): number {
|
|
163
|
+
return positive(this.ai.watchdogs?.[watchdogId]?.maxSteps, this.aiMaxSteps);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Tokens de saída efetivos de um watchdog. */
|
|
167
|
+
aiMaxOutputTokensFor(watchdogId: string): number {
|
|
168
|
+
return positive(
|
|
169
|
+
this.ai.watchdogs?.[watchdogId]?.maxOutputTokens,
|
|
170
|
+
this.aiMaxOutputTokens,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Inteiro positivo ou o default. */
|
|
176
|
+
function positive(value: number | undefined, fallback: number): number {
|
|
177
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
56
178
|
}
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { HoundsmanConfig } from "./config";
|
|
9
|
+
import { env } from "./env";
|
|
9
10
|
import { JevQuestionFactory } from "./jev";
|
|
10
11
|
import { renderReport, sanitizeBranch } from "./report-builder";
|
|
12
|
+
import { hasExplicitPolarity } from "./rule-text";
|
|
11
13
|
import type { WatchdogMeta } from "./types";
|
|
12
14
|
|
|
13
15
|
/** `id` da definição bate com a chave; `description` não vazio. */
|
|
@@ -27,17 +29,56 @@ function checkDefinitions(config: HoundsmanConfig, problems: string[]): void {
|
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
/** Todo watchdog precisa declarar ao menos um arquivo em `classify
|
|
32
|
+
/** Todo watchdog precisa declarar ao menos um arquivo em `classify` (fallback isento). */
|
|
31
33
|
function checkClassification(config: HoundsmanConfig, problems: string[]): void {
|
|
32
34
|
for (const id of config.watchdogIds) {
|
|
33
35
|
const def = config.watchdogs[id];
|
|
34
36
|
if (!def) continue;
|
|
37
|
+
if (def.fallback) continue;
|
|
35
38
|
if (!def.classify || def.classify.length === 0) {
|
|
36
39
|
problems.push(`Watchdog '${id}' não declara nenhum padrão em 'classify'`);
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
/** Fallback: no máximo um; `classify` é ignorado; precisa de regras. */
|
|
45
|
+
function checkFallbacks(config: HoundsmanConfig, warnings: string[]): void {
|
|
46
|
+
const fallbacks = config.watchdogIds.filter(
|
|
47
|
+
(id) => config.watchdogs[id]?.fallback === true,
|
|
48
|
+
);
|
|
49
|
+
if (fallbacks.length > 1) {
|
|
50
|
+
warnings.push(
|
|
51
|
+
`Mais de um watchdog fallback (${fallbacks.join(", ")}); o primeiro declarado vence.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
for (const id of fallbacks) {
|
|
55
|
+
const def = config.watchdogs[id];
|
|
56
|
+
if (!def) continue;
|
|
57
|
+
if (def.classify && def.classify.length > 0) {
|
|
58
|
+
warnings.push(`watchdogs['${id}'] é fallback — 'classify' é ignorado.`);
|
|
59
|
+
}
|
|
60
|
+
if (def.rules.length === 0) {
|
|
61
|
+
warnings.push(`Watchdog fallback '${id}' não declara nenhuma regra.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Findings emergentes exigem ao menos uma regra de lógica/domínio como piso. */
|
|
67
|
+
function checkEmergent(config: HoundsmanConfig, warnings: string[]): void {
|
|
68
|
+
for (const id of config.watchdogIds) {
|
|
69
|
+
const def = config.watchdogs[id];
|
|
70
|
+
if (!def?.allowEmergentFindings) continue;
|
|
71
|
+
const hasGeneric = def.rules.some((rule) =>
|
|
72
|
+
/l[oó]gica|dom[ií]nio/i.test(rule.text),
|
|
73
|
+
);
|
|
74
|
+
if (!hasGeneric) {
|
|
75
|
+
warnings.push(
|
|
76
|
+
`watchdogs['${id}'].allowEmergentFindings sem regra de lógica/domínio — o piso fica implícito.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
41
82
|
/** Smoke test dos helpers críticos de renderização. */
|
|
42
83
|
function checkRendering(config: HoundsmanConfig, problems: string[]): void {
|
|
43
84
|
if (sanitizeBranch("feature/x") !== "feature-x") {
|
|
@@ -106,9 +147,6 @@ function checkJevSpecs(config: HoundsmanConfig, problems: string[]): number {
|
|
|
106
147
|
return generated;
|
|
107
148
|
}
|
|
108
149
|
|
|
109
|
-
/** Prefixos de polaridade aceitos no texto da regra (§ Convenções de escrita). */
|
|
110
|
-
const POLARITY_PREFIX = /^(promover|evitar|não|nao|nunca)\b/i;
|
|
111
|
-
|
|
112
150
|
/**
|
|
113
151
|
* Aviso **não bloqueante** para regra sem polaridade explícita: o texto deve
|
|
114
152
|
* começar com `Promover:` (DEVE acontecer) ou `Evitar:`/`Não`/`Nunca` (NÃO DEVE).
|
|
@@ -119,7 +157,7 @@ function checkRulePolarity(config: HoundsmanConfig, warnings: string[]): void {
|
|
|
119
157
|
if (!def) continue;
|
|
120
158
|
def.rules.forEach((rule, index) => {
|
|
121
159
|
const text = rule.text.trim();
|
|
122
|
-
if (!text ||
|
|
160
|
+
if (!text || hasExplicitPolarity(text)) return;
|
|
123
161
|
warnings.push(
|
|
124
162
|
`Regra sem polaridade explícita em ${id} (rule-${index}): ${rule.text}`,
|
|
125
163
|
);
|
|
@@ -169,7 +207,7 @@ function checkMetadataPrecision(config: HoundsmanConfig, warnings: string[]): vo
|
|
|
169
207
|
const items = def[field];
|
|
170
208
|
if (!items) continue;
|
|
171
209
|
items.forEach((item, index) => {
|
|
172
|
-
if (!
|
|
210
|
+
if (!hasExplicitPolarity(item)) return;
|
|
173
211
|
warnings.push(
|
|
174
212
|
`Texto de regra em ${id}.${field}[${index}] — a regra vive só em 'rules': ${item}`,
|
|
175
213
|
);
|
|
@@ -192,6 +230,106 @@ function checkBudgets(config: HoundsmanConfig, warnings: string[]): void {
|
|
|
192
230
|
}
|
|
193
231
|
}
|
|
194
232
|
|
|
233
|
+
/** Valida a configuração do AST (a disponibilidade das grammars é checada no doctor). */
|
|
234
|
+
function checkAstConfig(
|
|
235
|
+
config: HoundsmanConfig,
|
|
236
|
+
problems: string[],
|
|
237
|
+
warnings: string[],
|
|
238
|
+
): void {
|
|
239
|
+
const ast = config.ast;
|
|
240
|
+
if (!ast) return;
|
|
241
|
+
|
|
242
|
+
if (ast.maxFileBytes !== undefined && ast.maxFileBytes <= 0) {
|
|
243
|
+
warnings.push(
|
|
244
|
+
`ast.maxFileBytes = ${ast.maxFileBytes} — não-positivo; usando o default.`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (ast.maxImpactFiles !== undefined && ast.maxImpactFiles < 0) {
|
|
248
|
+
warnings.push(
|
|
249
|
+
`ast.maxImpactFiles = ${ast.maxImpactFiles} — negativo; impacto será desabilitado.`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const valid = new Set(["typescript", "tsx", "javascript"]);
|
|
253
|
+
for (const [extension, language] of Object.entries(ast.languages ?? {})) {
|
|
254
|
+
if (!valid.has(language)) {
|
|
255
|
+
problems.push(
|
|
256
|
+
`ast.languages['${extension}'] = '${language}' (esperado typescript|tsx|javascript)`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const [alias, target] of Object.entries(ast.aliases ?? {})) {
|
|
261
|
+
if (!alias.trim() || !target.trim()) {
|
|
262
|
+
warnings.push(`ast.aliases['${alias}'] vazio — alias ignorado.`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Valida a configuração de IA (sem exigir chave — fail-open). */
|
|
268
|
+
function checkAiConfig(
|
|
269
|
+
config: HoundsmanConfig,
|
|
270
|
+
problems: string[],
|
|
271
|
+
warnings: string[],
|
|
272
|
+
): void {
|
|
273
|
+
const ai = config.ai;
|
|
274
|
+
if (!ai) return;
|
|
275
|
+
|
|
276
|
+
const validProviders = new Set(["deepseek", "openai", "anthropic"]);
|
|
277
|
+
if (ai.provider !== undefined && !validProviders.has(ai.provider)) {
|
|
278
|
+
problems.push(
|
|
279
|
+
`ai.provider = '${ai.provider}' (esperado deepseek|openai|anthropic)`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (ai.enabled !== false && ai.model !== undefined && !ai.model.trim()) {
|
|
283
|
+
warnings.push("ai.model vazio — usando o default.");
|
|
284
|
+
}
|
|
285
|
+
for (const key of [
|
|
286
|
+
"concurrency",
|
|
287
|
+
"maxSteps",
|
|
288
|
+
"maxOutputTokens",
|
|
289
|
+
"timeoutMs",
|
|
290
|
+
] as const) {
|
|
291
|
+
const value = ai[key];
|
|
292
|
+
if (value !== undefined && value <= 0) {
|
|
293
|
+
warnings.push(`ai.${key} = ${value} — não-positivo; usando o default.`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (ai.maxCallsPerReview !== undefined && ai.maxCallsPerReview <= 0) {
|
|
297
|
+
warnings.push(
|
|
298
|
+
`ai.maxCallsPerReview = ${ai.maxCallsPerReview} — não-positivo; usando o default.`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
if (ai.maxTokensPerReview !== undefined && ai.maxTokensPerReview < 0) {
|
|
302
|
+
warnings.push(
|
|
303
|
+
`ai.maxTokensPerReview = ${ai.maxTokensPerReview} — negativo; sem teto.`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
for (const [id, override] of Object.entries(ai.watchdogs ?? {})) {
|
|
307
|
+
if (!config.watchdogIds.includes(id)) {
|
|
308
|
+
warnings.push(`ai.watchdogs['${id}'] não corresponde a nenhum watchdog.`);
|
|
309
|
+
}
|
|
310
|
+
if (override.maxSteps !== undefined && override.maxSteps <= 0) {
|
|
311
|
+
warnings.push(
|
|
312
|
+
`ai.watchdogs['${id}'].maxSteps não-positivo — usando o default.`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const provider = ai.provider ?? "deepseek";
|
|
318
|
+
if (validProviders.has(provider) && ai.enabled !== false) {
|
|
319
|
+
const credential =
|
|
320
|
+
provider === "deepseek"
|
|
321
|
+
? env.deepSeekApiKey
|
|
322
|
+
: provider === "openai"
|
|
323
|
+
? env.openAiApiKey
|
|
324
|
+
: env.anthropicApiKey;
|
|
325
|
+
if (!credential) {
|
|
326
|
+
warnings.push(
|
|
327
|
+
`ai.provider '${provider}' sem credencial — features de IA ficam fail-open.`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
195
333
|
export function checkRegistry(config: HoundsmanConfig): {
|
|
196
334
|
problems: string[];
|
|
197
335
|
warnings: string[];
|
|
@@ -202,11 +340,15 @@ export function checkRegistry(config: HoundsmanConfig): {
|
|
|
202
340
|
|
|
203
341
|
checkDefinitions(config, problems);
|
|
204
342
|
checkClassification(config, problems);
|
|
343
|
+
checkFallbacks(config, warnings);
|
|
344
|
+
checkEmergent(config, warnings);
|
|
205
345
|
checkRendering(config, problems);
|
|
206
346
|
checkRulePolarity(config, warnings);
|
|
207
347
|
checkRuleDuplication(config, warnings);
|
|
208
348
|
checkMetadataPrecision(config, warnings);
|
|
209
349
|
checkBudgets(config, warnings);
|
|
350
|
+
checkAstConfig(config, problems, warnings);
|
|
351
|
+
checkAiConfig(config, problems, warnings);
|
|
210
352
|
const jevQuestions = checkJevSpecs(config, problems);
|
|
211
353
|
|
|
212
354
|
return { problems, warnings, jevQuestions };
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* findings apontam para o staging da review.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { AiUsage } from "../ai/types";
|
|
9
10
|
import { Classification } from "../domain/classification";
|
|
10
11
|
import type { ClassificationResult } from "../file-classifier";
|
|
11
12
|
import type { JevCandidatesFile } from "../jev";
|
|
@@ -22,6 +23,27 @@ export type ManifestOptions = {
|
|
|
22
23
|
head: string;
|
|
23
24
|
};
|
|
24
25
|
|
|
26
|
+
/** Resumo do AST no manifesto. */
|
|
27
|
+
export type ManifestAst = {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
languages: string[];
|
|
30
|
+
files: number;
|
|
31
|
+
impact: {
|
|
32
|
+
affectedFiles: number;
|
|
33
|
+
importers: number;
|
|
34
|
+
exportedChanges: number;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Resumo da IA no manifesto (só quando a IA foi usada). */
|
|
39
|
+
export type ManifestAi = {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
provider: string;
|
|
42
|
+
model: string;
|
|
43
|
+
briefPlans: number;
|
|
44
|
+
usage: AiUsage;
|
|
45
|
+
};
|
|
46
|
+
|
|
25
47
|
/** Classificação enriquecida gravada pelo prepare (modo + incremental). */
|
|
26
48
|
export type ManifestClassification = ClassificationResult & {
|
|
27
49
|
headSha?: string;
|
|
@@ -57,6 +79,8 @@ export class ManifestWriter {
|
|
|
57
79
|
activeIds: string[],
|
|
58
80
|
pending: string[],
|
|
59
81
|
jev: JevCandidatesFile,
|
|
82
|
+
ast?: ManifestAst,
|
|
83
|
+
ai?: ManifestAi,
|
|
60
84
|
): void {
|
|
61
85
|
const view = Classification.from(classification);
|
|
62
86
|
const maxFilesPerBrief = this.project.maxFilesPerBrief;
|
|
@@ -65,9 +89,11 @@ export class ManifestWriter {
|
|
|
65
89
|
const watchdogs = activeIds.map((id) => {
|
|
66
90
|
const selection = view.budgetFor(id, maxFilesPerBrief);
|
|
67
91
|
filesOverBudget += selection.overflow.length;
|
|
92
|
+
const domain = this.project.watchdogs.find(id);
|
|
68
93
|
return {
|
|
69
94
|
id,
|
|
70
95
|
name: this.project.watchdogs.nameOf(id),
|
|
96
|
+
...(domain?.definition.fallback ? { fallback: true } : {}),
|
|
71
97
|
files: selection.selected,
|
|
72
98
|
overflow: selection.overflow.length,
|
|
73
99
|
brief: this.work.briefFile(id),
|
|
@@ -109,6 +135,13 @@ export class ManifestWriter {
|
|
|
109
135
|
skipped: jev.skipped?.length ?? 0,
|
|
110
136
|
}
|
|
111
137
|
: { available: false, reason: jev.reason },
|
|
138
|
+
ast: ast ?? {
|
|
139
|
+
enabled: false,
|
|
140
|
+
languages: [],
|
|
141
|
+
files: 0,
|
|
142
|
+
impact: { affectedFiles: 0, importers: 0, exportedChanges: 0 },
|
|
143
|
+
},
|
|
144
|
+
ai,
|
|
112
145
|
deleted: classification.deleted,
|
|
113
146
|
unclassified: classification.unclassified,
|
|
114
147
|
excludedCount: classification.excludedCount,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resumo final da revisão (`review-resume.md`).
|
|
3
|
+
*
|
|
4
|
+
* Artefato somente-leitura para humanos: não entra em briefs, não é lido pelos
|
|
5
|
+
* watchdogs e não participa de nenhuma etapa seguinte. Une o contexto de
|
|
6
|
+
* negócio da PR aos números técnicos diretos da revisão, com limite de
|
|
7
|
+
* `RESUME_MAX_CHARS`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ContextSummary,
|
|
12
|
+
MergedFinding,
|
|
13
|
+
WatchdogNameResolver,
|
|
14
|
+
} from "../report-builder";
|
|
15
|
+
import { systemClock } from "../services/clock";
|
|
16
|
+
|
|
17
|
+
/** Teto de caracteres do artefato (inclui cabeçalho e quebras de linha). */
|
|
18
|
+
export const RESUME_MAX_CHARS = 2500;
|
|
19
|
+
|
|
20
|
+
/** Dados de classificação usados no resumo. */
|
|
21
|
+
export type ResumeClassification = {
|
|
22
|
+
files: Record<string, { status: string }>;
|
|
23
|
+
watchdogs: Record<string, string[]>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ReviewResumeInput = {
|
|
27
|
+
branch: string;
|
|
28
|
+
/** `YYYY-MM-DD`; default: data corrente do sistema. */
|
|
29
|
+
date?: string;
|
|
30
|
+
context?: ContextSummary | null;
|
|
31
|
+
classification: ResumeClassification;
|
|
32
|
+
findings: readonly MergedFinding[];
|
|
33
|
+
nameOf?: WatchdogNameResolver;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const identityName: WatchdogNameResolver = (id) => id;
|
|
37
|
+
|
|
38
|
+
const STATUS_BY_SEVERITY: Record<"blocking" | "attention" | "clean", string> = {
|
|
39
|
+
blocking: "❌ Mudanças necessárias",
|
|
40
|
+
attention: "⚠️ Aprovado com ressalvas",
|
|
41
|
+
clean: "✅ Aprovado",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Corta o texto em `max` caracteres, sinalizando com reticências. */
|
|
45
|
+
function truncate(value: string, max: number): string {
|
|
46
|
+
const text = value.trim().replace(/\s+/g, " ");
|
|
47
|
+
return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function plural(count: number, singular: string, plural: string): string {
|
|
51
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Resumo de negócio: contexto da PR; sem ele, o alcance da branch. */
|
|
55
|
+
function businessSection(input: ReviewResumeInput): string[] {
|
|
56
|
+
const context = input.context;
|
|
57
|
+
const hasContext =
|
|
58
|
+
context?.available === true &&
|
|
59
|
+
Boolean(
|
|
60
|
+
context.objective ||
|
|
61
|
+
context.motivation ||
|
|
62
|
+
(context.changeSummary && context.changeSummary.length > 0),
|
|
63
|
+
);
|
|
64
|
+
const lines = ["## Resumo de negócio", ""];
|
|
65
|
+
|
|
66
|
+
if (!hasContext) {
|
|
67
|
+
const domainCount = Object.keys(input.classification.watchdogs).length;
|
|
68
|
+
lines.push(
|
|
69
|
+
`A branch \`${input.branch}\` altera ${plural(
|
|
70
|
+
Object.keys(input.classification.files).length,
|
|
71
|
+
"arquivo",
|
|
72
|
+
"arquivos",
|
|
73
|
+
)} em ${plural(domainCount, "domínio", "domínios")} de revisão.`,
|
|
74
|
+
"Sem resumo de contexto da PR disponível.",
|
|
75
|
+
"",
|
|
76
|
+
);
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (context.objective) lines.push(context.objective, "");
|
|
81
|
+
if (context.motivation) lines.push(`**Por quê:** ${context.motivation}`, "");
|
|
82
|
+
if (context.changeSummary && context.changeSummary.length > 0) {
|
|
83
|
+
for (const item of context.changeSummary.slice(0, 4)) {
|
|
84
|
+
lines.push(`- ${truncate(item, 180)}`);
|
|
85
|
+
}
|
|
86
|
+
lines.push("");
|
|
87
|
+
}
|
|
88
|
+
return lines;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Achados que exigem ação, em formato direto (máximo 5). */
|
|
92
|
+
function technicalSection(
|
|
93
|
+
findings: readonly MergedFinding[],
|
|
94
|
+
nameOf: WatchdogNameResolver,
|
|
95
|
+
): string[] {
|
|
96
|
+
const relevant = findings
|
|
97
|
+
.filter(
|
|
98
|
+
(finding) =>
|
|
99
|
+
finding.severity === "blocking" || finding.severity === "attention",
|
|
100
|
+
)
|
|
101
|
+
.slice(0, 5);
|
|
102
|
+
const lines = ["## Pontos técnicos", ""];
|
|
103
|
+
if (relevant.length === 0) {
|
|
104
|
+
lines.push("- Nenhum bloqueio ou ponto de atenção.", "");
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
for (const finding of relevant) {
|
|
108
|
+
const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
|
|
109
|
+
const watchdogs = finding.watchdogs.map((id) => nameOf(id)).join(", ");
|
|
110
|
+
lines.push(
|
|
111
|
+
`- \`${location}\` — ${truncate(finding.message, 130)}${watchdogs ? ` _(${watchdogs})_` : ""}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
lines.push("");
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Garante o teto de caracteres cortando em uma quebra de linha. */
|
|
119
|
+
function clamp(markdown: string, max: number): string {
|
|
120
|
+
if (markdown.length <= max) return markdown;
|
|
121
|
+
const head = markdown.slice(0, max - 2);
|
|
122
|
+
const cut = head.lastIndexOf("\n");
|
|
123
|
+
return `${(cut > 0 ? head.slice(0, cut) : head).trimEnd()}\n…`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Renderiza o resumo final. Determinístico e sem chamadas externas: o texto
|
|
128
|
+
* nasce do contexto já disponível e dos findings consolidados.
|
|
129
|
+
*/
|
|
130
|
+
export function renderReviewResume(input: ReviewResumeInput): string {
|
|
131
|
+
const nameOf = input.nameOf ?? identityName;
|
|
132
|
+
const blockers = input.findings.filter((f) => f.severity === "blocking").length;
|
|
133
|
+
const attention = input.findings.filter((f) => f.severity === "attention").length;
|
|
134
|
+
const status =
|
|
135
|
+
blockers > 0
|
|
136
|
+
? STATUS_BY_SEVERITY.blocking
|
|
137
|
+
: attention > 0
|
|
138
|
+
? STATUS_BY_SEVERITY.attention
|
|
139
|
+
: STATUS_BY_SEVERITY.clean;
|
|
140
|
+
|
|
141
|
+
const lines = [
|
|
142
|
+
`# Resumo da revisão — ${input.branch}`,
|
|
143
|
+
"",
|
|
144
|
+
`**Data:** ${input.date ?? systemClock().slice(0, 10)} · **Status:** ${status}`,
|
|
145
|
+
"",
|
|
146
|
+
...businessSection(input),
|
|
147
|
+
...technicalSection(input.findings, nameOf),
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
return clamp(`${lines.join("\n").trimEnd()}\n`, RESUME_MAX_CHARS);
|
|
151
|
+
}
|
|
@@ -294,6 +294,7 @@ function renderConclusion(counts: Record<Severity, number>): string[] {
|
|
|
294
294
|
"",
|
|
295
295
|
`**Status:** ${status}`,
|
|
296
296
|
`**Resumo:** ${counts.blocking} violações bloqueantes, ${counts.attention} pontos de atenção, ${counts.suggestion} sugestões e ${counts.nit} nits.`,
|
|
297
|
+
"",
|
|
297
298
|
];
|
|
298
299
|
}
|
|
299
300
|
|
|
@@ -453,6 +454,12 @@ export function renderReport(
|
|
|
453
454
|
|
|
454
455
|
if (context) lines.push(...renderContext(context));
|
|
455
456
|
|
|
457
|
+
// Sumário primeiro: quem lê o review.md vê domínios, cobertura e conclusão
|
|
458
|
+
// antes dos pontos específicos.
|
|
459
|
+
lines.push(...renderDomainIndex(domains));
|
|
460
|
+
lines.push(...renderCoverage(reports, coverage, nameOf));
|
|
461
|
+
lines.push(...renderConclusion(counts));
|
|
462
|
+
|
|
456
463
|
lines.push(...renderFindingsSections(merged, nameOf));
|
|
457
464
|
|
|
458
465
|
if (merged.length === 0) {
|
|
@@ -463,9 +470,5 @@ export function renderReport(
|
|
|
463
470
|
lines.push(...renderUnconfirmedCandidates(candidates, merged));
|
|
464
471
|
}
|
|
465
472
|
|
|
466
|
-
lines.push(...renderDomainIndex(domains));
|
|
467
|
-
lines.push(...renderCoverage(reports, coverage, nameOf));
|
|
468
|
-
lines.push(...renderConclusion(counts));
|
|
469
|
-
|
|
470
473
|
return lines.join("\n");
|
|
471
474
|
}
|