@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.
Files changed (119) hide show
  1. package/README.md +23 -2
  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/doctor-command.ts +22 -0
  6. package/src/cli/commands/findings-command.ts +11 -1
  7. package/src/cli/commands/hunt-command.ts +479 -0
  8. package/src/cli/commands/review-command.ts +58 -2
  9. package/src/cli/commands/status-command.ts +3 -2
  10. package/src/cli/help.ts +20 -3
  11. package/src/cli/index.ts +20 -3
  12. package/src/cli/progress.ts +50 -0
  13. package/src/core/ai/ai-client.ts +38 -2
  14. package/src/core/ai/ai-sdk-model-provider.ts +39 -0
  15. package/src/core/ai/features/brief-enricher.ts +1 -1
  16. package/src/core/ai/features/business-analyzer.ts +61 -0
  17. package/src/core/ai/features/business-summarizer.ts +44 -0
  18. package/src/core/ai/index.ts +14 -1
  19. package/src/core/ai/prompts/brief.ts +33 -7
  20. package/src/core/ai/prompts/business.ts +76 -0
  21. package/src/core/ai/provider-capabilities.ts +12 -0
  22. package/src/core/ai/schemas.ts +70 -2
  23. package/src/core/ai/types.ts +2 -0
  24. package/src/core/ai/usage-recorder.ts +53 -1
  25. package/src/core/brief.ts +246 -12
  26. package/src/core/builtins.ts +108 -0
  27. package/src/core/business-summary.ts +583 -0
  28. package/src/core/commands/compare.ts +233 -0
  29. package/src/core/commands/context.ts +6 -2
  30. package/src/core/commands/findings.ts +38 -30
  31. package/src/core/commands/review.ts +6 -0
  32. package/src/core/commands/scaffold.ts +38 -0
  33. package/src/core/commands/status.ts +9 -4
  34. package/src/core/compare.ts +243 -0
  35. package/src/core/concurrency.ts +30 -0
  36. package/src/core/config.ts +57 -1
  37. package/src/core/defaults.ts +63 -2
  38. package/src/core/domain/budget.ts +62 -4
  39. package/src/core/domain/classification.ts +12 -2
  40. package/src/core/engine/carryover.ts +4 -0
  41. package/src/core/engine/finalize-pipeline.ts +347 -0
  42. package/src/core/engine/prepare-pipeline.ts +1294 -0
  43. package/src/core/engine/review-context.ts +51 -0
  44. package/src/core/engine/review-options.ts +47 -0
  45. package/src/core/engine/review-scope.ts +106 -26
  46. package/src/core/engine/review-session.ts +18 -16
  47. package/src/core/engine/review-workspace.ts +14 -0
  48. package/src/core/engine/run-artifacts.ts +196 -0
  49. package/src/core/engine/run-format.ts +4 -0
  50. package/src/core/env.ts +14 -0
  51. package/src/core/findings-state.ts +13 -1
  52. package/src/core/github-review.ts +5 -2
  53. package/src/core/hunt/hunt-runner.ts +418 -0
  54. package/src/core/hunt/index.ts +61 -0
  55. package/src/core/hunt/maki-install.ts +331 -0
  56. package/src/core/hunt/maki-intent-prompt.ts +50 -0
  57. package/src/core/hunt/maki-model.ts +101 -0
  58. package/src/core/hunt/maki-prompt.ts +83 -0
  59. package/src/core/hunt/maki-release.ts +104 -0
  60. package/src/core/hunt/maki-runner.ts +276 -0
  61. package/src/core/hunt/pool.ts +8 -0
  62. package/src/core/logging/index.ts +7 -0
  63. package/src/core/logging/logger.ts +19 -0
  64. package/src/core/logging/progress.ts +366 -0
  65. package/src/core/orchestrator.ts +33 -1180
  66. package/src/core/paths.ts +6 -3
  67. package/src/core/pr-context.ts +2 -4
  68. package/src/core/prepare-report.ts +61 -0
  69. package/src/core/project-config.ts +46 -1
  70. package/src/core/propagation.ts +127 -0
  71. package/src/core/registry-check.ts +60 -21
  72. package/src/core/renderers/manifest-writer.ts +13 -0
  73. package/src/core/renderers/resume-renderer.ts +151 -11
  74. package/src/core/report-builder.ts +108 -278
  75. package/src/core/storage/file/file-storage.ts +7 -3
  76. package/src/core/storage/sqlite/mappers.ts +10 -0
  77. package/src/core/storage/sqlite/schema.ts +104 -23
  78. package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
  79. package/src/core/storage/types.ts +17 -4
  80. package/src/core/storage/work-area.ts +59 -1
  81. package/src/core/types.ts +31 -0
  82. package/src/core/watchdog-registry.ts +21 -3
  83. package/src/index.ts +78 -4
  84. package/src/server/client/ArtifactsPanel.tsx +4 -2
  85. package/src/server/client/FindingCard.tsx +21 -1
  86. package/src/server/client/RunsSection.tsx +5 -0
  87. package/src/server/client/utils/constants.ts +21 -11
  88. package/src/server/client/utils/markdown.ts +1 -1
  89. package/src/server/client/utils/types.ts +14 -2
  90. package/src/server/run-repository.ts +35 -14
  91. package/src/server/styles.css +2 -0
  92. package/src/skill/OUTPUT-FORMAT.md +36 -10
  93. package/src/skill/README.md +30 -14
  94. package/src/skill/SETUP.md +22 -1
  95. package/src/skill/SKILL.md +17 -4
  96. package/src/skill/example-resume.md +43 -0
  97. package/src/skill/hunt.md +181 -58
  98. package/src/tests/__snapshots__/baseline.test.ts.snap +206 -178
  99. package/src/tests/ai-core.test.ts +84 -1
  100. package/src/tests/ai-features.test.ts +95 -4
  101. package/src/tests/baseline.test.ts +29 -121
  102. package/src/tests/brief-scope.test.ts +250 -0
  103. package/src/tests/business-summary.test.ts +258 -0
  104. package/src/tests/cli.test.ts +1 -0
  105. package/src/tests/compare.test.ts +233 -0
  106. package/src/tests/fixtures/fake-model-provider.ts +2 -1
  107. package/src/tests/hunt-cli.test.ts +185 -0
  108. package/src/tests/hunt-install.test.ts +153 -0
  109. package/src/tests/hunt-maki.test.ts +525 -0
  110. package/src/tests/markdown.test.ts +3 -3
  111. package/src/tests/progress.test.ts +145 -0
  112. package/src/tests/propagation.test.ts +119 -0
  113. package/src/tests/review-resume.test.ts +60 -1
  114. package/src/tests/review-scope.test.ts +90 -19
  115. package/src/tests/review-scripts.test.ts +106 -218
  116. package/src/tests/server.test.ts +7 -4
  117. package/src/tests/storage.test.ts +194 -9
  118. package/src/core/renderers/report-renderer.ts +0 -66
  119. 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
+ }
@@ -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 { 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
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(config: HoundsmanConfig, problems: string[]): void {
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 sampleId = config.watchdogIds[0] ?? "watchdog";
88
- const report = {
89
- watchdogId: sampleId,
90
- reviewedFiles: [],
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
- const md = renderReport("main", [report], [], {});
95
- if (!md.includes("✅ Aprovado")) {
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 = renderReport(
99
- "main",
100
- [report],
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("## Contexto da PR")) {
110
- problems.push("renderReport não incluiu a seção de contexto");
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(config, problems);
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,