@payconductor/houndsman 0.13.0 → 0.16.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 (116) hide show
  1. package/README.md +6 -1
  2. package/package.json +1 -1
  3. package/src/cli/commands/compare-command.ts +112 -0
  4. package/src/cli/commands/context-command.ts +1 -0
  5. package/src/cli/commands/findings-command.ts +11 -1
  6. package/src/cli/commands/golden-command.ts +92 -0
  7. package/src/cli/commands/hunt-command.ts +105 -4
  8. package/src/cli/commands/review-command.ts +38 -0
  9. package/src/cli/commands/status-command.ts +3 -2
  10. package/src/cli/help.ts +6 -1
  11. package/src/cli/index.ts +5 -1
  12. package/src/cli/progress.ts +7 -2
  13. package/src/core/ai/ai-client.ts +38 -2
  14. package/src/core/ai/features/brief-enricher.ts +1 -1
  15. package/src/core/ai/features/business-analyzer.ts +61 -0
  16. package/src/core/ai/features/business-summarizer.ts +44 -0
  17. package/src/core/ai/index.ts +8 -0
  18. package/src/core/ai/prompts/brief.ts +33 -7
  19. package/src/core/ai/prompts/business.ts +76 -0
  20. package/src/core/ai/provider-capabilities.ts +12 -0
  21. package/src/core/ai/schemas.ts +70 -2
  22. package/src/core/ai/usage-recorder.ts +53 -1
  23. package/src/core/brief.ts +278 -16
  24. package/src/core/builtins.ts +108 -0
  25. package/src/core/business-summary.ts +583 -0
  26. package/src/core/commands/compare.ts +266 -0
  27. package/src/core/commands/context.ts +6 -2
  28. package/src/core/commands/findings.ts +38 -30
  29. package/src/core/commands/golden.ts +176 -0
  30. package/src/core/commands/review.ts +6 -0
  31. package/src/core/commands/scaffold.ts +29 -0
  32. package/src/core/commands/status.ts +9 -4
  33. package/src/core/compare.ts +310 -0
  34. package/src/core/concurrency.ts +30 -0
  35. package/src/core/config.ts +41 -3
  36. package/src/core/defaults.ts +50 -4
  37. package/src/core/domain/budget.ts +62 -4
  38. package/src/core/domain/classification.ts +12 -2
  39. package/src/core/engine/carryover.ts +4 -0
  40. package/src/core/engine/finalize-pipeline.ts +371 -0
  41. package/src/core/engine/prepare-pipeline.ts +1294 -0
  42. package/src/core/engine/review-context.ts +51 -0
  43. package/src/core/engine/review-options.ts +47 -0
  44. package/src/core/engine/review-scope.ts +14 -1
  45. package/src/core/engine/review-session.ts +11 -13
  46. package/src/core/engine/run-artifacts.ts +196 -0
  47. package/src/core/engine/run-format.ts +4 -0
  48. package/src/core/findings-state.ts +22 -2
  49. package/src/core/github-review.ts +5 -2
  50. package/src/core/golden.ts +346 -0
  51. package/src/core/hunt/hunt-runner.ts +621 -34
  52. package/src/core/hunt/index.ts +40 -1
  53. package/src/core/hunt/maki-intent-prompt.ts +50 -0
  54. package/src/core/hunt/maki-prompt.ts +298 -5
  55. package/src/core/hunt/maki-runner.ts +93 -16
  56. package/src/core/hunt/pool.ts +4 -26
  57. package/src/core/hunt/review-checks.ts +460 -0
  58. package/src/core/logging/index.ts +6 -1
  59. package/src/core/logging/progress.ts +85 -1
  60. package/src/core/orchestrator.ts +33 -1289
  61. package/src/core/paths.ts +6 -3
  62. package/src/core/pr-context.ts +2 -4
  63. package/src/core/prepare-report.ts +61 -0
  64. package/src/core/project-config.ts +18 -1
  65. package/src/core/propagation.ts +127 -0
  66. package/src/core/registry-check.ts +90 -27
  67. package/src/core/renderers/manifest-writer.ts +13 -0
  68. package/src/core/renderers/resume-renderer.ts +151 -11
  69. package/src/core/report-builder.ts +116 -278
  70. package/src/core/severity.ts +245 -0
  71. package/src/core/storage/file/file-storage.ts +7 -3
  72. package/src/core/storage/sqlite/mappers.ts +10 -0
  73. package/src/core/storage/sqlite/schema.ts +104 -23
  74. package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
  75. package/src/core/storage/types.ts +17 -4
  76. package/src/core/storage/work-area.ts +59 -1
  77. package/src/core/types.ts +31 -0
  78. package/src/core/watchdog-registry.ts +21 -3
  79. package/src/index.ts +51 -4
  80. package/src/server/client/ArtifactsPanel.tsx +4 -2
  81. package/src/server/client/FindingCard.tsx +21 -1
  82. package/src/server/client/RunsSection.tsx +5 -0
  83. package/src/server/client/utils/constants.ts +21 -11
  84. package/src/server/client/utils/markdown.ts +1 -1
  85. package/src/server/client/utils/types.ts +14 -2
  86. package/src/server/run-repository.ts +35 -14
  87. package/src/server/styles.css +2 -0
  88. package/src/skill/OUTPUT-FORMAT.md +55 -10
  89. package/src/skill/README.md +19 -14
  90. package/src/skill/SETUP.md +1 -0
  91. package/src/skill/SKILL.md +13 -4
  92. package/src/skill/example-resume.md +43 -0
  93. package/src/skill/hunt.md +205 -58
  94. package/src/skill/train.md +3 -0
  95. package/src/tests/__snapshots__/baseline.test.ts.snap +208 -184
  96. package/src/tests/ai-core.test.ts +46 -0
  97. package/src/tests/ai-features.test.ts +95 -4
  98. package/src/tests/baseline.test.ts +29 -121
  99. package/src/tests/brief-scope.test.ts +250 -0
  100. package/src/tests/business-summary.test.ts +258 -0
  101. package/src/tests/compare.test.ts +256 -0
  102. package/src/tests/findings-state.test.ts +82 -0
  103. package/src/tests/fixtures/fake-model-provider.ts +2 -1
  104. package/src/tests/golden.test.ts +195 -0
  105. package/src/tests/hunt-maki.test.ts +710 -2
  106. package/src/tests/markdown.test.ts +3 -3
  107. package/src/tests/progress.test.ts +29 -1
  108. package/src/tests/propagation.test.ts +119 -0
  109. package/src/tests/review-resume.test.ts +60 -1
  110. package/src/tests/review-scope.test.ts +38 -29
  111. package/src/tests/review-scripts.test.ts +125 -218
  112. package/src/tests/server.test.ts +7 -4
  113. package/src/tests/severity.test.ts +235 -0
  114. package/src/tests/storage.test.ts +194 -9
  115. package/src/core/renderers/report-renderer.ts +0 -66
  116. 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
 
@@ -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 relatório de revisão em **Objetivo**,",
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
+ }
@@ -14,6 +14,7 @@ import type {
14
14
  HoundsmanAst,
15
15
  HoundsmanConfig,
16
16
  HoundsmanHunt,
17
+ HoundsmanPriority,
17
18
  HoundsmanStorage,
18
19
  HoundsmanWorktrees,
19
20
  } from "./config";
@@ -105,6 +106,11 @@ export class ProjectConfig<TId extends string = string> {
105
106
  return this.raw.docMappings;
106
107
  }
107
108
 
109
+ /** Prioridade por caminho (vazio = churn puro, comportamento antigo). */
110
+ get priorities(): readonly HoundsmanPriority[] {
111
+ return this.raw.priorities ?? [];
112
+ }
113
+
108
114
  /** Config de IA (objeto vazio quando ausente). */
109
115
  get ai(): HoundsmanAi {
110
116
  return this.raw.ai ?? {};
@@ -148,11 +154,12 @@ export class ProjectConfig<TId extends string = string> {
148
154
  return value !== undefined && value > 0 ? value : undefined;
149
155
  }
150
156
 
151
- /** Features resolvidas com defaults (`context`/`brief`/`technical` ligadas). */
157
+ /** Features resolvidas com defaults (`context`/`business`/`brief`/`technical` ligadas). */
152
158
  get aiFeatures(): Required<HoundsmanAiFeatures> {
153
159
  const features = this.ai.features ?? {};
154
160
  return {
155
161
  context: features.context !== false,
162
+ business: features.business !== false,
156
163
  brief: features.brief !== false,
157
164
  technical: features.technical !== false,
158
165
  classification: features.classification === true,
@@ -200,6 +207,16 @@ export class ProjectConfig<TId extends string = string> {
200
207
  const value = this.hunt.model?.trim();
201
208
  return value && value.length > 0 ? value : this.aiModel;
202
209
  }
210
+
211
+ /** Passada de checklist do harness (default: ligada). */
212
+ get huntChecklist(): boolean {
213
+ return this.hunt.checklist !== false;
214
+ }
215
+
216
+ /** Passada adversarial do `general` (default: ligada). */
217
+ get huntSecondOpinion(): boolean {
218
+ return this.hunt.secondOpinion !== false;
219
+ }
203
220
  }
204
221
 
205
222
  /** 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,12 +5,15 @@
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 { renderReport, sanitizeBranch } from "./report-builder";
12
+ import { renderReviewResume } from "./renderers/resume-renderer";
13
+ import { sanitizeBranch } from "./report-builder";
12
14
  import { hasExplicitPolarity } from "./rule-text";
13
- import type { WatchdogMeta } from "./types";
15
+ import { normalizeRuleText } from "./severity";
16
+ import type { Severity, WatchdogMeta } from "./types";
14
17
 
15
18
  /** `id` da definição bate com a chave; `description` não vazio. */
16
19
  function checkDefinitions(config: HoundsmanConfig, problems: string[]): void {
@@ -63,6 +66,15 @@ function checkFallbacks(config: HoundsmanConfig, warnings: string[]): void {
63
66
  }
64
67
  }
65
68
 
69
+ /** O id `general` é reservado: o motor sempre injeta o revisor de fluxo. */
70
+ function checkReservedIds(config: HoundsmanConfig, warnings: string[]): void {
71
+ if (config.watchdogIds.includes(GENERAL_WATCHDOG_ID)) {
72
+ warnings.push(
73
+ `watchdogs['${GENERAL_WATCHDOG_ID}'] usa um id reservado — os metadados são mesclados ao piso de lógica do motor.`,
74
+ );
75
+ }
76
+ }
77
+
66
78
  /** Findings emergentes exigem ao menos uma regra de lógica/domínio como piso. */
67
79
  function checkEmergent(config: HoundsmanConfig, warnings: string[]): void {
68
80
  for (const id of config.watchdogIds) {
@@ -79,35 +91,61 @@ function checkEmergent(config: HoundsmanConfig, warnings: string[]): void {
79
91
  }
80
92
  }
81
93
 
94
+ /**
95
+ * Prioridades por caminho: RegExp, valores finitos (`bonus` ≥ 0, teto > 0) e ao
96
+ * menos um efeito — uma regra sem `bonus` nem `maxRiskFromChurn` é inócua.
97
+ */
98
+ function checkPriorities(config: HoundsmanConfig, problems: string[]): void {
99
+ for (const [index, rule] of (config.priorities ?? []).entries()) {
100
+ if (!(rule.pattern instanceof RegExp)) {
101
+ problems.push(`priorities[${index}].pattern precisa ser uma RegExp`);
102
+ }
103
+ if (
104
+ rule.bonus !== undefined &&
105
+ (!Number.isFinite(rule.bonus) || rule.bonus < 0)
106
+ ) {
107
+ problems.push(`priorities[${index}].bonus precisa ser número ≥ 0`);
108
+ }
109
+ if (
110
+ rule.maxRiskFromChurn !== undefined &&
111
+ (!Number.isFinite(rule.maxRiskFromChurn) || rule.maxRiskFromChurn <= 0)
112
+ ) {
113
+ problems.push(`priorities[${index}].maxRiskFromChurn precisa ser número > 0`);
114
+ }
115
+ if (rule.bonus === undefined && rule.maxRiskFromChurn === undefined) {
116
+ problems.push(
117
+ `priorities[${index}] não tem bonus nem maxRiskFromChurn (regra inócua)`,
118
+ );
119
+ }
120
+ }
121
+ }
122
+
82
123
  /** Smoke test dos helpers críticos de renderização. */
83
- function checkRendering(config: HoundsmanConfig, problems: string[]): void {
124
+ function checkRendering(problems: string[]): void {
84
125
  if (sanitizeBranch("feature/x") !== "feature-x") {
85
126
  problems.push("sanitizeBranch não está normalizando branches");
86
127
  }
87
- const sampleId = config.watchdogIds[0] ?? "watchdog";
88
- const report = {
89
- watchdogId: sampleId,
90
- reviewedFiles: [],
91
- skipped: [],
128
+ const base = { branch: "main", date: "2026-01-01" } as const;
129
+ const empty = renderReviewResume({
130
+ ...base,
131
+ classification: { files: {}, watchdogs: {} },
92
132
  findings: [],
93
- };
94
- const md = renderReport("main", [report], [], {});
95
- if (!md.includes("✅ Aprovado")) {
96
- problems.push("renderReport não marcou status aprovado sem achados");
133
+ });
134
+ if (!empty.includes("✅ Aprovado")) {
135
+ problems.push("renderReviewResume não marcou status aprovado sem achados");
97
136
  }
98
- const withContext = renderReport(
99
- "main",
100
- [report],
101
- [],
102
- {},
103
- {
137
+ const withContext = renderReviewResume({
138
+ ...base,
139
+ classification: { files: {}, watchdogs: {} },
140
+ findings: [],
141
+ context: {
104
142
  available: true,
105
143
  objective: "Entregar X",
106
144
  changeSummary: ["Faz A"],
107
145
  },
108
- );
109
- if (!withContext.includes("## Contexto da PR")) {
110
- problems.push("renderReport não incluiu a seção de contexto");
146
+ });
147
+ if (!withContext.includes("Entregar X")) {
148
+ problems.push("renderReviewResume não incluiu o contexto da PR");
111
149
  }
112
150
  }
113
151
 
@@ -165,11 +203,6 @@ function checkRulePolarity(config: HoundsmanConfig, warnings: string[]): void {
165
203
  }
166
204
  }
167
205
 
168
- /** Normaliza o texto da regra para detectar duplicatas (case/espaços). */
169
- function normalizeRuleText(text: string): string {
170
- return text.trim().toLowerCase().replace(/\s+/g, " ");
171
- }
172
-
173
206
  /**
174
207
  * Aviso para regra repetida (texto normalizado) no mesmo watchdog — a regra deve
175
208
  * existir uma única vez em `rules` (§ Convenções de escrita, item 10).
@@ -191,6 +224,33 @@ function checkRuleDuplication(config: HoundsmanConfig, warnings: string[]): void
191
224
  }
192
225
  }
193
226
 
227
+ /**
228
+ * Aviso quando o mesmo texto de regra aparece em watchdogs diferentes com
229
+ * severidades diferentes: a ancoragem usaria a mais grave e o resultado
230
+ * dependeria de qual watchdog reportou.
231
+ */
232
+ function checkRuleSeverityConflicts(config: HoundsmanConfig, warnings: string[]): void {
233
+ const byText = new Map<string, { id: string; severity: Severity }[]>();
234
+ for (const id of config.watchdogIds) {
235
+ const def = config.watchdogs[id];
236
+ if (!def) continue;
237
+ for (const rule of def.rules) {
238
+ const key = normalizeRuleText(rule.text);
239
+ if (!key) continue;
240
+ const entries = byText.get(key) ?? [];
241
+ entries.push({ id, severity: rule.severity });
242
+ byText.set(key, entries);
243
+ }
244
+ }
245
+ for (const entries of byText.values()) {
246
+ if (entries.length < 2) continue;
247
+ const severities = new Set(entries.map((entry) => entry.severity));
248
+ if (severities.size < 2) continue;
249
+ const detail = entries.map((entry) => `${entry.id}=${entry.severity}`).join(", ");
250
+ warnings.push(`Regra com severidades divergentes entre watchdogs: ${detail}.`);
251
+ }
252
+ }
253
+
194
254
  /** Campos de metadados que NÃO devem conter texto de regra. */
195
255
  const RULE_BEARING_FIELDS = ["reviewFocus", "contextFiles", "exclude"] as const;
196
256
 
@@ -331,12 +391,15 @@ export function checkRegistry(config: HoundsmanConfig): {
331
391
  checkDefinitions(config, problems);
332
392
  checkClassification(config, problems);
333
393
  checkFallbacks(config, warnings);
394
+ checkReservedIds(config, warnings);
334
395
  checkEmergent(config, warnings);
335
- checkRendering(config, problems);
396
+ checkRendering(problems);
336
397
  checkRulePolarity(config, warnings);
337
398
  checkRuleDuplication(config, warnings);
399
+ checkRuleSeverityConflicts(config, warnings);
338
400
  checkMetadataPrecision(config, warnings);
339
401
  checkBudgets(config, warnings);
402
+ checkPriorities(config, problems);
340
403
  checkAstConfig(config, problems, warnings);
341
404
  checkAiConfig(config, problems, warnings);
342
405
  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,