@payconductor/houndsman 0.12.0 → 0.14.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 +23 -2
- package/package.json +1 -1
- package/src/cli/commands/compare-command.ts +112 -0
- package/src/cli/commands/context-command.ts +1 -0
- package/src/cli/commands/doctor-command.ts +22 -0
- package/src/cli/commands/findings-command.ts +11 -1
- package/src/cli/commands/hunt-command.ts +479 -0
- package/src/cli/commands/review-command.ts +58 -2
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +20 -3
- package/src/cli/index.ts +20 -3
- package/src/cli/progress.ts +50 -0
- package/src/core/ai/ai-client.ts +38 -2
- package/src/core/ai/ai-sdk-model-provider.ts +39 -0
- package/src/core/ai/features/brief-enricher.ts +1 -1
- package/src/core/ai/features/business-analyzer.ts +61 -0
- package/src/core/ai/features/business-summarizer.ts +44 -0
- package/src/core/ai/index.ts +14 -1
- package/src/core/ai/prompts/brief.ts +33 -7
- package/src/core/ai/prompts/business.ts +76 -0
- package/src/core/ai/provider-capabilities.ts +12 -0
- package/src/core/ai/schemas.ts +70 -2
- package/src/core/ai/types.ts +2 -0
- package/src/core/ai/usage-recorder.ts +53 -1
- package/src/core/brief.ts +246 -12
- package/src/core/builtins.ts +108 -0
- package/src/core/business-summary.ts +583 -0
- package/src/core/commands/compare.ts +233 -0
- package/src/core/commands/context.ts +6 -2
- package/src/core/commands/findings.ts +38 -30
- package/src/core/commands/review.ts +6 -0
- package/src/core/commands/scaffold.ts +38 -0
- package/src/core/commands/status.ts +9 -4
- package/src/core/compare.ts +243 -0
- package/src/core/concurrency.ts +30 -0
- package/src/core/config.ts +57 -1
- package/src/core/defaults.ts +63 -2
- package/src/core/domain/budget.ts +62 -4
- package/src/core/domain/classification.ts +12 -2
- package/src/core/engine/carryover.ts +4 -0
- package/src/core/engine/finalize-pipeline.ts +347 -0
- package/src/core/engine/prepare-pipeline.ts +1294 -0
- package/src/core/engine/review-context.ts +51 -0
- package/src/core/engine/review-options.ts +47 -0
- package/src/core/engine/review-scope.ts +106 -26
- package/src/core/engine/review-session.ts +18 -16
- package/src/core/engine/review-workspace.ts +14 -0
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/env.ts +14 -0
- package/src/core/findings-state.ts +13 -1
- package/src/core/github-review.ts +5 -2
- package/src/core/hunt/hunt-runner.ts +418 -0
- package/src/core/hunt/index.ts +61 -0
- package/src/core/hunt/maki-install.ts +331 -0
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-model.ts +101 -0
- package/src/core/hunt/maki-prompt.ts +83 -0
- package/src/core/hunt/maki-release.ts +104 -0
- package/src/core/hunt/maki-runner.ts +276 -0
- package/src/core/hunt/pool.ts +8 -0
- package/src/core/logging/index.ts +7 -0
- package/src/core/logging/logger.ts +19 -0
- package/src/core/logging/progress.ts +366 -0
- package/src/core/orchestrator.ts +33 -1180
- package/src/core/paths.ts +6 -3
- package/src/core/pr-context.ts +2 -4
- package/src/core/prepare-report.ts +61 -0
- package/src/core/project-config.ts +46 -1
- package/src/core/propagation.ts +127 -0
- package/src/core/registry-check.ts +60 -21
- package/src/core/renderers/manifest-writer.ts +13 -0
- package/src/core/renderers/resume-renderer.ts +151 -11
- package/src/core/report-builder.ts +108 -278
- package/src/core/storage/file/file-storage.ts +7 -3
- package/src/core/storage/sqlite/mappers.ts +10 -0
- package/src/core/storage/sqlite/schema.ts +104 -23
- package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
- package/src/core/storage/types.ts +17 -4
- package/src/core/storage/work-area.ts +59 -1
- package/src/core/types.ts +31 -0
- package/src/core/watchdog-registry.ts +21 -3
- package/src/index.ts +78 -4
- package/src/server/client/ArtifactsPanel.tsx +4 -2
- package/src/server/client/FindingCard.tsx +21 -1
- package/src/server/client/RunsSection.tsx +5 -0
- package/src/server/client/utils/constants.ts +21 -11
- package/src/server/client/utils/markdown.ts +1 -1
- package/src/server/client/utils/types.ts +14 -2
- package/src/server/run-repository.ts +35 -14
- package/src/server/styles.css +2 -0
- package/src/skill/OUTPUT-FORMAT.md +36 -10
- package/src/skill/README.md +30 -14
- package/src/skill/SETUP.md +22 -1
- package/src/skill/SKILL.md +17 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +181 -58
- package/src/tests/__snapshots__/baseline.test.ts.snap +206 -178
- package/src/tests/ai-core.test.ts +84 -1
- package/src/tests/ai-features.test.ts +95 -4
- package/src/tests/baseline.test.ts +29 -121
- package/src/tests/brief-scope.test.ts +250 -0
- package/src/tests/business-summary.test.ts +258 -0
- package/src/tests/cli.test.ts +1 -0
- package/src/tests/compare.test.ts +233 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/hunt-cli.test.ts +185 -0
- package/src/tests/hunt-install.test.ts +153 -0
- package/src/tests/hunt-maki.test.ts +525 -0
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +145 -0
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +90 -19
- package/src/tests/review-scripts.test.ts +106 -218
- package/src/tests/server.test.ts +7 -4
- package/src/tests/storage.test.ts +194 -9
- package/src/core/renderers/report-renderer.ts +0 -66
- package/src/skill/example-review.md +0 -66
package/src/core/paths.ts
CHANGED
|
@@ -47,12 +47,15 @@ export const CONTEXT_SUMMARY_FILE = "context-summary.json";
|
|
|
47
47
|
/** Brief do subagente que resume o contexto da PR. */
|
|
48
48
|
export const CONTEXT_BRIEF_FILE = "context-brief.md";
|
|
49
49
|
|
|
50
|
+
/** Modelo de intenção da PR (negócio) — insumo obrigatório do revisor `general`. */
|
|
51
|
+
export const BUSINESS_SUMMARY_FILE = "business-summary.json";
|
|
52
|
+
|
|
53
|
+
/** Brief do passo de intenção (subagente na skill / pré-passe no hunt). */
|
|
54
|
+
export const BUSINESS_BRIEF_FILE = "business-brief.md";
|
|
55
|
+
|
|
50
56
|
/** Relatório da verificação de findings (artefato `verify.md`). */
|
|
51
57
|
export const VERIFY_FILE = "verify.md";
|
|
52
58
|
|
|
53
|
-
/** Relatório consolidado da review (artefato `review.md`). */
|
|
54
|
-
export const REVIEW_FILE = "review.md";
|
|
55
|
-
|
|
56
59
|
/** Resumo final da review (artefato `review-resume.md`) — leitura humana apenas. */
|
|
57
60
|
export const REVIEW_RESUME_FILE = "review-resume.md";
|
|
58
61
|
|
package/src/core/pr-context.ts
CHANGED
|
@@ -203,7 +203,7 @@ export function renderContextBrief(
|
|
|
203
203
|
|
|
204
204
|
lines.push("## Objetivo", "");
|
|
205
205
|
lines.push(
|
|
206
|
-
"Resumir a PR para abrir o
|
|
206
|
+
"Resumir a PR para abrir o resumo final em **Objetivo**,",
|
|
207
207
|
"**Motivação** e **Resumo das alterações** (pt-BR).",
|
|
208
208
|
"",
|
|
209
209
|
);
|
|
@@ -228,9 +228,7 @@ export function renderContextBrief(
|
|
|
228
228
|
lines.push("```", "");
|
|
229
229
|
|
|
230
230
|
lines.push("## Regras", "");
|
|
231
|
-
lines.push(
|
|
232
|
-
"- O resumo abre o relatório e o resumo final; nos briefs ele é recortado.",
|
|
233
|
-
);
|
|
231
|
+
lines.push("- O resumo abre o resumo final; nos briefs ele é recortado.");
|
|
234
232
|
lines.push("- Baseie-se **somente** no `context.json` — não invente intenção.");
|
|
235
233
|
lines.push(
|
|
236
234
|
"- `objective`: até 2 frases (≤ 400 caracteres) sobre o que a PR entrega.",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relatório de duração do `prepare` (artefato `prepare-report.json`).
|
|
3
|
+
*
|
|
4
|
+
* O CLI já imprime cada etapa; o artefato permite comparar runs (antes/depois
|
|
5
|
+
* de tuning) sem poluir o `manifest.json`, que é determinístico nos snapshots.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AiUsage } from "./ai/types";
|
|
9
|
+
import type { PrepareStage } from "./orchestrator";
|
|
10
|
+
|
|
11
|
+
/** Duração de uma etapa do prepare. */
|
|
12
|
+
export type PrepareStageTiming = {
|
|
13
|
+
stage: PrepareStage;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Chamada de IA que falhou (timeout, truncamento, schema inválido). */
|
|
18
|
+
export type PrepareAiFailure = {
|
|
19
|
+
feature: string;
|
|
20
|
+
model: string;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
error: string;
|
|
23
|
+
finishReason?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Uso de IA do prepare: tokens das chamadas boas + tentativas/falhas. */
|
|
27
|
+
export type PrepareAiReport = AiUsage & {
|
|
28
|
+
attempts: number;
|
|
29
|
+
failures: readonly PrepareAiFailure[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Entrada do relatório. */
|
|
33
|
+
export type PrepareReportInput = {
|
|
34
|
+
/** Início do prepare (ISO). */
|
|
35
|
+
startedAt: string;
|
|
36
|
+
durationMs: number;
|
|
37
|
+
stages: readonly PrepareStageTiming[];
|
|
38
|
+
/** Uso agregado de IA e falhas (quando configurada). */
|
|
39
|
+
ai?: PrepareAiReport;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Monta o objeto persistido. */
|
|
43
|
+
export function buildPrepareReport(input: PrepareReportInput) {
|
|
44
|
+
return {
|
|
45
|
+
version: 1 as const,
|
|
46
|
+
startedAt: input.startedAt,
|
|
47
|
+
durationMs: input.durationMs,
|
|
48
|
+
stages: input.stages.map((item) => ({ ...item })),
|
|
49
|
+
ai: input.ai
|
|
50
|
+
? {
|
|
51
|
+
...input.ai,
|
|
52
|
+
failures: input.ai.failures.map((failure) => ({ ...failure })),
|
|
53
|
+
}
|
|
54
|
+
: null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Serializa o relatório (JSON indentado + newline). */
|
|
59
|
+
export function renderPrepareReport(input: PrepareReportInput): string {
|
|
60
|
+
return `${JSON.stringify(buildPrepareReport(input), null, 2)}\n`;
|
|
61
|
+
}
|
|
@@ -13,6 +13,8 @@ import type {
|
|
|
13
13
|
HoundsmanAiFeatures,
|
|
14
14
|
HoundsmanAst,
|
|
15
15
|
HoundsmanConfig,
|
|
16
|
+
HoundsmanHunt,
|
|
17
|
+
HoundsmanPriority,
|
|
16
18
|
HoundsmanStorage,
|
|
17
19
|
HoundsmanWorktrees,
|
|
18
20
|
} from "./config";
|
|
@@ -25,6 +27,9 @@ import {
|
|
|
25
27
|
DEFAULT_ALWAYS_INCLUDE_DOCS,
|
|
26
28
|
DEFAULT_AST_MAX_FILE_BYTES,
|
|
27
29
|
DEFAULT_AST_MAX_IMPACT_FILES,
|
|
30
|
+
DEFAULT_HUNT_CONCURRENCY,
|
|
31
|
+
DEFAULT_HUNT_RETRIES,
|
|
32
|
+
DEFAULT_HUNT_TIMEOUT_MS,
|
|
28
33
|
DEFAULT_JEV_CONCURRENCY,
|
|
29
34
|
DEFAULT_MAX_FILES_PER_BRIEF,
|
|
30
35
|
DEFAULT_WORKTREES_LOCATION,
|
|
@@ -101,6 +106,11 @@ export class ProjectConfig<TId extends string = string> {
|
|
|
101
106
|
return this.raw.docMappings;
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
/** Prioridade por caminho (vazio = churn puro, comportamento antigo). */
|
|
110
|
+
get priorities(): readonly HoundsmanPriority[] {
|
|
111
|
+
return this.raw.priorities ?? [];
|
|
112
|
+
}
|
|
113
|
+
|
|
104
114
|
/** Config de IA (objeto vazio quando ausente). */
|
|
105
115
|
get ai(): HoundsmanAi {
|
|
106
116
|
return this.raw.ai ?? {};
|
|
@@ -144,11 +154,12 @@ export class ProjectConfig<TId extends string = string> {
|
|
|
144
154
|
return value !== undefined && value > 0 ? value : undefined;
|
|
145
155
|
}
|
|
146
156
|
|
|
147
|
-
/** Features resolvidas com defaults (`context`/`brief`/`technical` ligadas). */
|
|
157
|
+
/** Features resolvidas com defaults (`context`/`business`/`brief`/`technical` ligadas). */
|
|
148
158
|
get aiFeatures(): Required<HoundsmanAiFeatures> {
|
|
149
159
|
const features = this.ai.features ?? {};
|
|
150
160
|
return {
|
|
151
161
|
context: features.context !== false,
|
|
162
|
+
business: features.business !== false,
|
|
152
163
|
brief: features.brief !== false,
|
|
153
164
|
technical: features.technical !== false,
|
|
154
165
|
classification: features.classification === true,
|
|
@@ -162,6 +173,40 @@ export class ProjectConfig<TId extends string = string> {
|
|
|
162
173
|
const override = this.ai.watchdogs?.[watchdogId]?.model?.trim();
|
|
163
174
|
return override && override.length > 0 ? override : this.aiModel;
|
|
164
175
|
}
|
|
176
|
+
|
|
177
|
+
/** Config do harness local (objeto vazio quando ausente). */
|
|
178
|
+
get hunt(): HoundsmanHunt {
|
|
179
|
+
return this.raw.hunt ?? {};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Harness local habilitado (default: ligado; sem binário é fail-closed no comando). */
|
|
183
|
+
get huntEnabled(): boolean {
|
|
184
|
+
return this.hunt.enabled !== false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Processos maki simultâneos. */
|
|
188
|
+
get huntConcurrency(): number {
|
|
189
|
+
return positive(this.hunt.concurrency, DEFAULT_HUNT_CONCURRENCY);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Timeout por watchdog (ms). */
|
|
193
|
+
get huntTimeoutMs(): number {
|
|
194
|
+
return positive(this.hunt.timeoutMs, DEFAULT_HUNT_TIMEOUT_MS);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Retentativas extras (0 desliga; valores negativos caem no default). */
|
|
198
|
+
get huntRetries(): number {
|
|
199
|
+
const value = this.hunt.retries;
|
|
200
|
+
return value !== undefined && Number.isInteger(value) && value >= 0
|
|
201
|
+
? value
|
|
202
|
+
: DEFAULT_HUNT_RETRIES;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Modelo base do harness local (sem prefixo de provider). */
|
|
206
|
+
get huntModel(): string {
|
|
207
|
+
const value = this.hunt.model?.trim();
|
|
208
|
+
return value && value.length > 0 ? value : this.aiModel;
|
|
209
|
+
}
|
|
165
210
|
}
|
|
166
211
|
|
|
167
212
|
/** Inteiro positivo ou o default. */
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Propagação mecânica — detecção determinística de mudanças em massa.
|
|
3
|
+
*
|
|
4
|
+
* Cenário típico: um contrato ganha um parâmetro obrigatório e N arquivos
|
|
5
|
+
* adicionam a mesma linha. Revisar N diffs de uma linha é desperdício; o risco
|
|
6
|
+
* está no padrão (um representante) e nos outliers (quem fugiu do padrão).
|
|
7
|
+
*
|
|
8
|
+
* Tudo aqui é função pura sobre dados já coletados no prepare (patch `-U0`,
|
|
9
|
+
* churn, delta/impacto do AST) — não há leitura extra de git nem chamada de IA.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { AstAnalysis, AstImpact } from "./ast/types";
|
|
13
|
+
import { churnOf } from "./domain/budget";
|
|
14
|
+
import type { ChurnMap, PropagationCluster, PropagationGap } from "./types";
|
|
15
|
+
|
|
16
|
+
/** Acima disso o arquivo não é "micro-alteração". */
|
|
17
|
+
export const PROPAGATION_MAX_CHURN = 3;
|
|
18
|
+
|
|
19
|
+
/** Mínimo de arquivos com o mesmo padrão para virar cluster. */
|
|
20
|
+
export const PROPAGATION_MIN_FILES = 5;
|
|
21
|
+
|
|
22
|
+
/** Teto de arquivos examinados (proteção para PRs gigantes). */
|
|
23
|
+
export const PROPAGATION_MAX_FILES = 500;
|
|
24
|
+
|
|
25
|
+
/** Outliers listados no brief (os demais são contados). */
|
|
26
|
+
export const PROPAGATION_MAX_OUTLIERS = 8;
|
|
27
|
+
|
|
28
|
+
/** Importadores listados por contrato no brief. */
|
|
29
|
+
export const PROPAGATION_MAX_IMPORTERS = 10;
|
|
30
|
+
|
|
31
|
+
/** Linhas adicionadas do patch (sem cabeçalho), normalizadas. */
|
|
32
|
+
export function addedLines(patch: string | undefined): string[] {
|
|
33
|
+
if (!patch) return [];
|
|
34
|
+
const lines: string[] = [];
|
|
35
|
+
for (const raw of patch.split(/\r?\n/)) {
|
|
36
|
+
if (!raw.startsWith("+") || raw.startsWith("+++")) continue;
|
|
37
|
+
const normalized = raw.slice(1).trim().replace(/\s+/g, " ");
|
|
38
|
+
if (normalized.length > 0) lines.push(normalized);
|
|
39
|
+
}
|
|
40
|
+
return lines;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Detecta o padrão dominante entre micro-alterações.
|
|
45
|
+
*
|
|
46
|
+
* Agrupa por linha adicionada **exata** (só normaliza espaços): agrupar por
|
|
47
|
+
* identificador esconderia justamente o outlier que precisa de revisão. Sem
|
|
48
|
+
* grupo dominante, devolve `null` e o brief segue como antes.
|
|
49
|
+
*/
|
|
50
|
+
export function detectPropagation(
|
|
51
|
+
files: readonly string[],
|
|
52
|
+
patch: ReadonlyMap<string, string> | undefined,
|
|
53
|
+
churn?: ChurnMap,
|
|
54
|
+
): PropagationCluster | null {
|
|
55
|
+
if (!patch) return null;
|
|
56
|
+
const micro: { file: string; lines: string[] }[] = [];
|
|
57
|
+
for (const file of files.slice(0, PROPAGATION_MAX_FILES)) {
|
|
58
|
+
if (churnOf(file, churn) > PROPAGATION_MAX_CHURN) continue;
|
|
59
|
+
const lines = addedLines(patch.get(file));
|
|
60
|
+
if (lines.length === 0) continue;
|
|
61
|
+
micro.push({ file, lines });
|
|
62
|
+
}
|
|
63
|
+
if (micro.length < PROPAGATION_MIN_FILES) return null;
|
|
64
|
+
|
|
65
|
+
const groups = new Map<string, { file: string; lines: string[] }[]>();
|
|
66
|
+
for (const entry of micro) {
|
|
67
|
+
const key = entry.lines.join("\n");
|
|
68
|
+
const list = groups.get(key);
|
|
69
|
+
if (list) list.push(entry);
|
|
70
|
+
else groups.set(key, [entry]);
|
|
71
|
+
}
|
|
72
|
+
const [patternKey, members] =
|
|
73
|
+
[...groups.entries()].sort(
|
|
74
|
+
(a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]),
|
|
75
|
+
)[0] ?? [];
|
|
76
|
+
if (!members || members.length < PROPAGATION_MIN_FILES) return null;
|
|
77
|
+
|
|
78
|
+
const dominant = new Set(members.map((entry) => entry.file));
|
|
79
|
+
const outliers = micro
|
|
80
|
+
.filter((entry) => !dominant.has(entry.file))
|
|
81
|
+
.slice(0, PROPAGATION_MAX_OUTLIERS);
|
|
82
|
+
return {
|
|
83
|
+
lines: (patternKey ?? "").split("\n"),
|
|
84
|
+
files: members.map((entry) => entry.file),
|
|
85
|
+
outliers,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `true` quando o delta mexeu em um contrato exportado (assinatura/export). */
|
|
90
|
+
function changesExportedContract(analysis: AstAnalysis | undefined): boolean {
|
|
91
|
+
const delta = analysis?.delta;
|
|
92
|
+
if (!delta) return false;
|
|
93
|
+
if (delta.changedExports.length > 0) return true;
|
|
94
|
+
if (delta.changedSignatures.some((change) => change.exported)) return true;
|
|
95
|
+
return delta.modifiedSymbols.some(
|
|
96
|
+
(symbol) =>
|
|
97
|
+
symbol.exported &&
|
|
98
|
+
(symbol.kind === "interface" ||
|
|
99
|
+
symbol.kind === "type" ||
|
|
100
|
+
symbol.kind === "enum" ||
|
|
101
|
+
symbol.kind === "class"),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Contratos alterados com importadores que **não** aparecem no diff.
|
|
107
|
+
*
|
|
108
|
+
* É o cheque de "atualizaram 100 e esqueceram 3": o impacto reverso do AST já
|
|
109
|
+
* lista quem importa o arquivo; quem não está no diff vira suspeito no brief.
|
|
110
|
+
*/
|
|
111
|
+
export function detectPropagationGaps(
|
|
112
|
+
files: readonly string[],
|
|
113
|
+
ast: ReadonlyMap<string, AstAnalysis> | undefined,
|
|
114
|
+
impact: AstImpact | undefined,
|
|
115
|
+
changedFiles: ReadonlySet<string>,
|
|
116
|
+
): PropagationGap[] {
|
|
117
|
+
if (!ast || !impact) return [];
|
|
118
|
+
const gaps: PropagationGap[] = [];
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
if (!changesExportedContract(ast.get(file))) continue;
|
|
121
|
+
const importers = [...new Set(impact.importers[file] ?? [])]
|
|
122
|
+
.filter((importer) => !changedFiles.has(importer))
|
|
123
|
+
.slice(0, PROPAGATION_MAX_IMPORTERS);
|
|
124
|
+
if (importers.length > 0) gaps.push({ file, importers });
|
|
125
|
+
}
|
|
126
|
+
return gaps;
|
|
127
|
+
}
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* `HoundsmanConfig` — não há caminhos de arquivo do projeto na config.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { GENERAL_WATCHDOG_ID } from "./builtins";
|
|
8
9
|
import type { HoundsmanConfig } from "./config";
|
|
9
10
|
import { env } from "./env";
|
|
10
11
|
import { JevQuestionFactory } from "./jev";
|
|
11
|
-
import {
|
|
12
|
+
import { renderReviewResume } from "./renderers/resume-renderer";
|
|
13
|
+
import { sanitizeBranch } from "./report-builder";
|
|
12
14
|
import { hasExplicitPolarity } from "./rule-text";
|
|
13
15
|
import type { WatchdogMeta } from "./types";
|
|
14
16
|
|
|
@@ -63,6 +65,15 @@ function checkFallbacks(config: HoundsmanConfig, warnings: string[]): void {
|
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
/** O id `general` é reservado: o motor sempre injeta o revisor de fluxo. */
|
|
69
|
+
function checkReservedIds(config: HoundsmanConfig, warnings: string[]): void {
|
|
70
|
+
if (config.watchdogIds.includes(GENERAL_WATCHDOG_ID)) {
|
|
71
|
+
warnings.push(
|
|
72
|
+
`watchdogs['${GENERAL_WATCHDOG_ID}'] usa um id reservado — os metadados são mesclados ao piso de lógica do motor.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
/** Findings emergentes exigem ao menos uma regra de lógica/domínio como piso. */
|
|
67
78
|
function checkEmergent(config: HoundsmanConfig, warnings: string[]): void {
|
|
68
79
|
for (const id of config.watchdogIds) {
|
|
@@ -79,35 +90,61 @@ function checkEmergent(config: HoundsmanConfig, warnings: string[]): void {
|
|
|
79
90
|
}
|
|
80
91
|
}
|
|
81
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Prioridades por caminho: RegExp, valores finitos (`bonus` ≥ 0, teto > 0) e ao
|
|
95
|
+
* menos um efeito — uma regra sem `bonus` nem `maxRiskFromChurn` é inócua.
|
|
96
|
+
*/
|
|
97
|
+
function checkPriorities(config: HoundsmanConfig, problems: string[]): void {
|
|
98
|
+
for (const [index, rule] of (config.priorities ?? []).entries()) {
|
|
99
|
+
if (!(rule.pattern instanceof RegExp)) {
|
|
100
|
+
problems.push(`priorities[${index}].pattern precisa ser uma RegExp`);
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
rule.bonus !== undefined &&
|
|
104
|
+
(!Number.isFinite(rule.bonus) || rule.bonus < 0)
|
|
105
|
+
) {
|
|
106
|
+
problems.push(`priorities[${index}].bonus precisa ser número ≥ 0`);
|
|
107
|
+
}
|
|
108
|
+
if (
|
|
109
|
+
rule.maxRiskFromChurn !== undefined &&
|
|
110
|
+
(!Number.isFinite(rule.maxRiskFromChurn) || rule.maxRiskFromChurn <= 0)
|
|
111
|
+
) {
|
|
112
|
+
problems.push(`priorities[${index}].maxRiskFromChurn precisa ser número > 0`);
|
|
113
|
+
}
|
|
114
|
+
if (rule.bonus === undefined && rule.maxRiskFromChurn === undefined) {
|
|
115
|
+
problems.push(
|
|
116
|
+
`priorities[${index}] não tem bonus nem maxRiskFromChurn (regra inócua)`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
82
122
|
/** Smoke test dos helpers críticos de renderização. */
|
|
83
|
-
function checkRendering(
|
|
123
|
+
function checkRendering(problems: string[]): void {
|
|
84
124
|
if (sanitizeBranch("feature/x") !== "feature-x") {
|
|
85
125
|
problems.push("sanitizeBranch não está normalizando branches");
|
|
86
126
|
}
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
skipped: [],
|
|
127
|
+
const base = { branch: "main", date: "2026-01-01" } as const;
|
|
128
|
+
const empty = renderReviewResume({
|
|
129
|
+
...base,
|
|
130
|
+
classification: { files: {}, watchdogs: {} },
|
|
92
131
|
findings: [],
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
problems.push("renderReport não marcou status aprovado sem achados");
|
|
132
|
+
});
|
|
133
|
+
if (!empty.includes("✅ Aprovado")) {
|
|
134
|
+
problems.push("renderReviewResume não marcou status aprovado sem achados");
|
|
97
135
|
}
|
|
98
|
-
const withContext =
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
[],
|
|
102
|
-
{
|
|
103
|
-
{
|
|
136
|
+
const withContext = renderReviewResume({
|
|
137
|
+
...base,
|
|
138
|
+
classification: { files: {}, watchdogs: {} },
|
|
139
|
+
findings: [],
|
|
140
|
+
context: {
|
|
104
141
|
available: true,
|
|
105
142
|
objective: "Entregar X",
|
|
106
143
|
changeSummary: ["Faz A"],
|
|
107
144
|
},
|
|
108
|
-
);
|
|
109
|
-
if (!withContext.includes("
|
|
110
|
-
problems.push("
|
|
145
|
+
});
|
|
146
|
+
if (!withContext.includes("Entregar X")) {
|
|
147
|
+
problems.push("renderReviewResume não incluiu o contexto da PR");
|
|
111
148
|
}
|
|
112
149
|
}
|
|
113
150
|
|
|
@@ -331,12 +368,14 @@ export function checkRegistry(config: HoundsmanConfig): {
|
|
|
331
368
|
checkDefinitions(config, problems);
|
|
332
369
|
checkClassification(config, problems);
|
|
333
370
|
checkFallbacks(config, warnings);
|
|
371
|
+
checkReservedIds(config, warnings);
|
|
334
372
|
checkEmergent(config, warnings);
|
|
335
|
-
checkRendering(
|
|
373
|
+
checkRendering(problems);
|
|
336
374
|
checkRulePolarity(config, warnings);
|
|
337
375
|
checkRuleDuplication(config, warnings);
|
|
338
376
|
checkMetadataPrecision(config, warnings);
|
|
339
377
|
checkBudgets(config, warnings);
|
|
378
|
+
checkPriorities(config, problems);
|
|
340
379
|
checkAstConfig(config, problems, warnings);
|
|
341
380
|
checkAiConfig(config, problems, warnings);
|
|
342
381
|
const jevQuestions = checkJevSpecs(config, problems);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { AiUsage } from "../ai/types";
|
|
10
|
+
import type { BusinessSummary } from "../business-summary";
|
|
10
11
|
import { Classification } from "../domain/classification";
|
|
11
12
|
import type { ClassificationResult } from "../file-classifier";
|
|
12
13
|
import type { JevCandidatesFile } from "../jev";
|
|
@@ -44,6 +45,16 @@ export type ManifestAi = {
|
|
|
44
45
|
usage: AiUsage;
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
/** Resumo do modelo de intenção no manifesto. */
|
|
49
|
+
export type ManifestBusiness = {
|
|
50
|
+
source: BusinessSummary["source"];
|
|
51
|
+
model?: string;
|
|
52
|
+
flows: number;
|
|
53
|
+
invariants: number;
|
|
54
|
+
contracts: number;
|
|
55
|
+
states: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
47
58
|
/** Classificação enriquecida gravada pelo prepare (modo + incremental). */
|
|
48
59
|
export type ManifestClassification = ClassificationResult & {
|
|
49
60
|
headSha?: string;
|
|
@@ -81,6 +92,7 @@ export class ManifestWriter {
|
|
|
81
92
|
jev: JevCandidatesFile,
|
|
82
93
|
ast?: ManifestAst,
|
|
83
94
|
ai?: ManifestAi,
|
|
95
|
+
business?: ManifestBusiness,
|
|
84
96
|
): void {
|
|
85
97
|
const view = Classification.from(classification);
|
|
86
98
|
const maxFilesPerBrief = this.project.maxFilesPerBrief;
|
|
@@ -125,6 +137,7 @@ export class ManifestWriter {
|
|
|
125
137
|
context: summary
|
|
126
138
|
? { summarized: true, source: summary.source ?? null }
|
|
127
139
|
: { summarized: false, source: null },
|
|
140
|
+
business: business ?? null,
|
|
128
141
|
jev: jev.available
|
|
129
142
|
? {
|
|
130
143
|
available: true,
|