@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
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Contexto de uma run (engine) — storage + review + staging + raiz operacional.
3
+ *
4
+ * É criado uma vez por `ReviewSession` e injetado nas pipelines. Não existe
5
+ * contexto ambiente: duas runs concorrentes no mesmo processo mantêm contextos
6
+ * isolados (um singleton mutável trocaria root/git/storage entre `await`s).
7
+ */
8
+
9
+ import { GitRepository } from "../services/git-repository";
10
+ import { ProjectFiles } from "../services/project-files";
11
+ import type { ReviewRecord, StorageProvider } from "../storage/types";
12
+ import { ReviewWorkArea } from "../storage/work-area";
13
+
14
+ /** Workspace onde os arquivos da review são lidos. */
15
+ export type ReviewWorkspaceContext = {
16
+ /** Raiz operacional (worktree ou clone principal). */
17
+ root?: string;
18
+ /** Raiz do clone principal (identidade/config). */
19
+ mainRoot?: string;
20
+ git?: GitRepository;
21
+ };
22
+
23
+ /** Contexto de uma run: storage + review + staging + raiz operacional. */
24
+ export type ReviewContext = {
25
+ storage: StorageProvider;
26
+ review: ReviewRecord;
27
+ work: ReviewWorkArea;
28
+ root: string;
29
+ mainRoot: string;
30
+ git: GitRepository;
31
+ /** Leitor relativo à raiz operacional (evita repetir `fileReader(root)`). */
32
+ files: ProjectFiles;
33
+ };
34
+
35
+ /** Cria o contexto de uma review existente. */
36
+ export function reviewContext(
37
+ storage: StorageProvider,
38
+ review: ReviewRecord,
39
+ workspace: ReviewWorkspaceContext = {},
40
+ ): ReviewContext {
41
+ const root = workspace.root ?? process.cwd();
42
+ return {
43
+ storage,
44
+ review,
45
+ work: new ReviewWorkArea(review),
46
+ root,
47
+ mainRoot: workspace.mainRoot ?? root,
48
+ git: workspace.git ?? new GitRepository(),
49
+ files: new ProjectFiles(root),
50
+ };
51
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Opções de uma run (flags do CLI já resolvidas).
3
+ *
4
+ * Fica fora do `orchestrator` para as pipelines importarem o tipo sem ciclo.
5
+ */
6
+
7
+ export type ReviewOptions = {
8
+ base: string;
9
+ head: string;
10
+ branch?: string;
11
+ /**
12
+ * Rótulo da variante A/B. Cada variante vira uma run própria
13
+ * (`repoKey`, `branch`, `variant`) — permite comparar sem apagar a anterior.
14
+ */
15
+ variant?: string;
16
+ finalize: boolean;
17
+ failOnBlocking: boolean;
18
+ clean: boolean;
19
+ noJev: boolean;
20
+ /** Desativa a análise estrutural (AST) nesta run. */
21
+ noAst: boolean;
22
+ /** Força review completo (ignora o modo incremental). */
23
+ full: boolean;
24
+ /** Desativa as features de IA do fluxo normal. */
25
+ noAi: boolean;
26
+ /** PR de origem (resolvida via `--pr`). */
27
+ pr?: number;
28
+ /** Worktree informado via flag (`--worktree`). */
29
+ worktree?: string;
30
+ /** Worktree resolvido (absoluto; ausente = clone principal). */
31
+ worktreePath?: string;
32
+ };
33
+
34
+ /** Valores default das opções (o CLI sobrepõe com as flags). */
35
+ export function defaultOptions(): ReviewOptions {
36
+ return {
37
+ base: "main",
38
+ head: "HEAD",
39
+ finalize: false,
40
+ failOnBlocking: false,
41
+ clean: false,
42
+ noJev: false,
43
+ noAst: false,
44
+ full: false,
45
+ noAi: false,
46
+ };
47
+ }
@@ -52,6 +52,11 @@ export type ReviewScope =
52
52
  kind: "review";
53
53
  classification: RunClassification;
54
54
  diff: DiffSource;
55
+ /**
56
+ * Patch `-U0` sem truncar (path → hunks). É o insumo dos briefs: cada
57
+ * watchdog recebe os trechos alterados em vez de reler arquivos inteiros.
58
+ */
59
+ patch: Map<string, string>;
55
60
  reviewedFrom?: string;
56
61
  /** Fatos/delta por arquivo alterado (quando o AST está habilitado). */
57
62
  ast?: Map<string, AstAnalysis>;
@@ -144,6 +149,7 @@ export class ReviewScopeResolver {
144
149
  return {
145
150
  kind: "review",
146
151
  diff,
152
+ patch: scopeDiff.byFile,
147
153
  ast,
148
154
  classification: {
149
155
  ...result,
@@ -192,7 +198,14 @@ export class ReviewScopeResolver {
192
198
  ast,
193
199
  }),
194
200
  };
195
- return { kind: "review", classification, diff, reviewedFrom: prevSha, ast };
201
+ return {
202
+ kind: "review",
203
+ classification,
204
+ diff,
205
+ patch: scopeDiff.byFile,
206
+ reviewedFrom: prevSha,
207
+ ast,
208
+ };
196
209
  }
197
210
 
198
211
  /**
@@ -20,7 +20,7 @@ import {
20
20
  resolveBranch,
21
21
  reviewContext,
22
22
  } from "../orchestrator";
23
- import { sanitizeBranch } from "../report-builder";
23
+ import { reviewSlug } from "../report-builder";
24
24
  import { GitRepository } from "../services/git-repository";
25
25
  import type { RepoIdentity, StorageProvider } from "../storage/types";
26
26
 
@@ -48,12 +48,14 @@ export class ReviewSession {
48
48
  this.branch = branch;
49
49
  this.ast = ast;
50
50
  this.ai = ai;
51
+ const variant = options.variant?.trim() || undefined;
51
52
  const review = options.finalize
52
- ? storage.findReview(repo.key, branch)
53
+ ? storage.findReview(repo.key, branch, variant)
53
54
  : storage.getOrCreateReview({
54
55
  repo,
55
56
  branch,
56
- slug: sanitizeBranch(branch),
57
+ variant,
58
+ slug: reviewSlug(branch, variant),
57
59
  base: options.base,
58
60
  head: options.head,
59
61
  worktreePath: options.worktreePath,
@@ -61,7 +63,7 @@ export class ReviewSession {
61
63
  });
62
64
  if (!review) {
63
65
  throw new Error(
64
- `Nenhuma revisão para '${branch}'. Rode \`houndsman\` (preparar) primeiro.`,
66
+ `Nenhuma revisão para '${branch}'${variant ? ` (variante '${variant}')` : ""}. Rode \`houndsman\` (preparar) primeiro.`,
65
67
  );
66
68
  }
67
69
  // Um prepare no clone principal invalida um worktree usado antes; o finalize
@@ -106,20 +108,16 @@ export class ReviewSession {
106
108
 
107
109
  /** Passo 1 — preparar. */
108
110
  prepare(onProgress?: PrepareProgress): Promise<PrepareResult> {
109
- return prepare(
110
- this.options,
111
- this.branch,
112
- this.config,
113
- this.context,
114
- this.ast,
115
- this.ai,
111
+ return prepare(this.context, this.config, this.options, {
112
+ ast: this.ast,
113
+ ai: this.ai,
116
114
  onProgress,
117
- );
115
+ });
118
116
  }
119
117
 
120
118
  /** Passo 3 — finalizar. */
121
119
  finalize(): FinalizeResult {
122
- return finalize(this.options, this.branch, this.config, this.context);
120
+ return finalize(this.context, this.config, this.options);
123
121
  }
124
122
 
125
123
  /** Executa o passo escolhido em `--finalize`. */
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Artefatos persistidos de uma run (engine) — leitura/escrita tipada.
3
+ *
4
+ * Concentra `classification`, `candidates`, `manifest`, resumos e contexto para
5
+ * que as pipelines não repitam `storage.readArtifact(...)`.
6
+ */
7
+
8
+ import type { AstAnalysis, AstImpact } from "../ast/types";
9
+ import { GENERAL_WATCHDOG_ID } from "../builtins";
10
+ import { type BusinessSummary, parseBusinessSummary } from "../business-summary";
11
+ import type { HoundsmanConfig } from "../config";
12
+ import { type JevCandidatesFile, JevCandidatesStore, parseCandidates } from "../jev";
13
+ import {
14
+ BUSINESS_BRIEF_FILE,
15
+ BUSINESS_SUMMARY_FILE,
16
+ TECHNICAL_SUMMARY_FILE,
17
+ } from "../paths";
18
+ import type {
19
+ ManifestAi,
20
+ ManifestAst,
21
+ ManifestBusiness,
22
+ } from "../renderers/manifest-writer";
23
+ import { type ContextSummary, parseContextSummary } from "../report-builder";
24
+ import type { ReviewRecord, StorageProvider } from "../storage/types";
25
+ import type { ReviewWorkArea } from "../storage/work-area";
26
+ import {
27
+ buildTechnicalSummary,
28
+ parseTechnicalSummary,
29
+ type TechnicalSummary,
30
+ } from "../technical-summary";
31
+ import type { RunClassification } from "./review-scope";
32
+
33
+ /** Acesso tipado aos artefatos de uma review. */
34
+ export class RunArtifacts {
35
+ constructor(
36
+ private readonly storage: StorageProvider,
37
+ private readonly review: ReviewRecord,
38
+ private readonly work: ReviewWorkArea,
39
+ ) {}
40
+
41
+ /** JSON persistido, ou `null` quando ausente/inválido. */
42
+ json<T>(name: string): T | null {
43
+ const artifact = this.storage.readArtifact(this.review.id, name);
44
+ if (!artifact) return null;
45
+ try {
46
+ return JSON.parse(artifact.content) as T;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ /** Reaproveita `ast`/`ai`/`business` do manifesto anterior. */
53
+ manifestExtras(): {
54
+ ast?: ManifestAst;
55
+ ai?: ManifestAi;
56
+ business?: ManifestBusiness;
57
+ } {
58
+ const artifact = this.storage.readArtifact(this.review.id, "manifest.json");
59
+ if (!artifact) return {};
60
+ try {
61
+ const parsed = JSON.parse(artifact.content) as {
62
+ ast?: ManifestAst;
63
+ ai?: ManifestAi;
64
+ business?: ManifestBusiness;
65
+ };
66
+ return {
67
+ ast: parsed.ast,
68
+ ai: parsed.ai,
69
+ business: parsed.business ?? undefined,
70
+ };
71
+ } catch {
72
+ return {};
73
+ }
74
+ }
75
+
76
+ /** Lê o `classification.json` persistido pelo prepare (lança se ausente). */
77
+ classification(): RunClassification {
78
+ const artifact = this.storage.readArtifact(this.review.id, "classification.json");
79
+ if (!artifact) {
80
+ throw new Error(
81
+ `Não encontrei a classificação da review. Rode \`bun run houndsman\` (preparar) primeiro.`,
82
+ );
83
+ }
84
+ return JSON.parse(artifact.content) as RunClassification;
85
+ }
86
+
87
+ /** Lê os candidatos Jev persistidos (unavailable se ausente/inválido). */
88
+ candidates(config: HoundsmanConfig): JevCandidatesFile {
89
+ const artifact = this.storage.readArtifact(this.review.id, "candidates.json");
90
+ if (!artifact) return JevCandidatesStore.unavailable("candidates.json ausente");
91
+ try {
92
+ return (
93
+ parseCandidates(JSON.parse(artifact.content), [
94
+ ...config.watchdogIds,
95
+ GENERAL_WATCHDOG_ID,
96
+ ]) ?? JevCandidatesStore.unavailable("candidates.json inválido")
97
+ );
98
+ } catch {
99
+ return JevCandidatesStore.unavailable("candidates.json inválido");
100
+ }
101
+ }
102
+
103
+ /** Resumo do contexto persistido (usado quando o staging já foi limpo). */
104
+ contextSummary(): ContextSummary | null {
105
+ const artifact = this.storage.readArtifact(
106
+ this.review.id,
107
+ "context-summary.json",
108
+ );
109
+ if (!artifact) return null;
110
+ try {
111
+ return parseContextSummary(JSON.parse(artifact.content));
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ /** Modelo de intenção persistido pelo prepare/revisor; `null` quando ausente. */
118
+ businessSummary(): BusinessSummary | null {
119
+ const raw = this.json<unknown>(BUSINESS_SUMMARY_FILE);
120
+ return raw ? parseBusinessSummary(raw) : null;
121
+ }
122
+
123
+ /** Pontos técnicos persistidos pelo `prepare`; `null` quando ausentes. */
124
+ technicalSummary(): TechnicalSummary | null {
125
+ const raw = this.json<unknown>(TECHNICAL_SUMMARY_FILE);
126
+ return raw ? parseTechnicalSummary(raw) : null;
127
+ }
128
+
129
+ /** Fallback para runs antigas: reconstrói os destaques dos artefatos de AST. */
130
+ technicalFallback(classification: RunClassification): TechnicalSummary {
131
+ const ast = this.json<{ files?: Record<string, AstAnalysis> }>(
132
+ "ast-summary.json",
133
+ );
134
+ return buildTechnicalSummary({
135
+ classification,
136
+ ast: ast?.files ? new Map(Object.entries(ast.files)) : undefined,
137
+ impact: this.json<AstImpact>("ast-impact.json") ?? undefined,
138
+ headSha: classification.headSha,
139
+ });
140
+ }
141
+
142
+ /** Persiste o contexto da PR (bruto + resumo) como artefatos, quando existirem. */
143
+ persistContext(): void {
144
+ const raw = this.work.readContextRaw();
145
+ if (raw !== null) {
146
+ this.storage.writeArtifact(this.review.id, {
147
+ name: "context.json",
148
+ kind: "context",
149
+ content: raw,
150
+ });
151
+ }
152
+ const summary = this.work.readContextSummaryRaw();
153
+ if (summary !== null) {
154
+ this.storage.writeArtifact(this.review.id, {
155
+ name: "context-summary.json",
156
+ kind: "context-summary",
157
+ content: summary,
158
+ });
159
+ }
160
+ const brief = this.work.readContextBriefRaw();
161
+ if (brief !== null) {
162
+ this.storage.writeArtifact(this.review.id, {
163
+ name: "context-brief.md",
164
+ kind: "context-brief",
165
+ content: brief,
166
+ });
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Persiste o `business-summary.json` final: o revisor `general` pode ter
172
+ * reescrito o modelo no staging durante a revisão.
173
+ */
174
+ persistBusiness(): BusinessSummary | null {
175
+ const raw = this.work.readBusinessSummaryRaw();
176
+ if (raw === null) return this.businessSummary();
177
+ this.storage.writeArtifact(this.review.id, {
178
+ name: BUSINESS_SUMMARY_FILE,
179
+ kind: "business",
180
+ content: raw.endsWith("\n") ? raw : `${raw}\n`,
181
+ });
182
+ const brief = this.work.readBusinessBriefRaw();
183
+ if (brief !== null) {
184
+ this.storage.writeArtifact(this.review.id, {
185
+ name: BUSINESS_BRIEF_FILE,
186
+ kind: "business-brief",
187
+ content: brief.endsWith("\n") ? brief : `${brief}\n`,
188
+ });
189
+ }
190
+ try {
191
+ return parseBusinessSummary(JSON.parse(raw), "reviewer");
192
+ } catch {
193
+ return this.businessSummary();
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,4 @@
1
+ /** SHA curto para logs (7 chars, como o git). */
2
+ export function shortSha(sha: string): string {
3
+ return sha.slice(0, 7);
4
+ }
@@ -18,9 +18,10 @@ import {
18
18
  locateAnchor,
19
19
  reanchor,
20
20
  } from "./finding-anchor";
21
- import { type MergedFinding, sanitizeBranch } from "./report-builder";
21
+ import { type FindingBasis, type MergedFinding, sanitizeBranch } from "./report-builder";
22
22
  import { systemClock } from "./services/clock";
23
23
  import { type FileReader, ProjectFiles } from "./services/project-files";
24
+ import { isImpactClass } from "./severity";
24
25
  import { SEVERITY_ORDER, type Severity, severityRank } from "./types";
25
26
 
26
27
  export type { FileReader };
@@ -101,6 +102,14 @@ export type FindingState = {
101
102
  message: string;
102
103
  action: string;
103
104
  evidence?: string;
105
+ /** Gatilho concreto do gap (fora de regra). */
106
+ scenario?: string;
107
+ /** Consequência observável do gap. */
108
+ impact?: string;
109
+ /** Confiança do revisor (0–1). */
110
+ confidence?: number;
111
+ /** Origem do achado: regra, lógica ou domínio. */
112
+ basis?: FindingBasis;
104
113
  anchor: FindingAnchor | null;
105
114
  /** Commit em que o achado foi observado (base do diff focado do verify). */
106
115
  foundAtSha?: string;
@@ -359,17 +368,28 @@ export function buildStateFromFindings(
359
368
  const prev = previous
360
369
  ? Object.values(previous.findings).find((f) => f.fingerprint === fingerprint)
361
370
  : undefined;
371
+ // Emergente sem `impactClass` (relatório legado) mantém a severidade já triada
372
+ // para o mesmo fingerprint: o código é o mesmo, então o valor não pode oscilar
373
+ // entre reviews. Regra e `impactClass` produzem valor determinístico e vencem.
374
+ const sticky =
375
+ (finding.basis ?? "rule") !== "rule" &&
376
+ !isImpactClass(finding.impactClass) &&
377
+ prev !== undefined;
362
378
  const created: FindingState = {
363
379
  id: prev?.id ?? newFindingId(),
364
380
  fingerprint,
365
381
  watchdogIds: [...finding.watchdogs],
366
382
  rule: finding.rule,
367
- severity: finding.severity,
383
+ severity: sticky && prev ? prev.severity : finding.severity,
368
384
  file: finding.file,
369
385
  line: anchor ? anchor.line : null,
370
386
  message: finding.message,
371
387
  action: finding.action,
372
388
  evidence: finding.evidence,
389
+ scenario: finding.scenario,
390
+ impact: finding.impact,
391
+ confidence: finding.confidence,
392
+ basis: finding.basis,
373
393
  anchor,
374
394
  // Carry-over preserva o `foundAtSha` original; novos/re-reportados usam o head atual.
375
395
  foundAtSha: finding.foundAtSha ?? options.headSha ?? prev?.foundAtSha,
@@ -225,9 +225,10 @@ export function buildCommentBody(finding: FindingState): string {
225
225
  `${SEVERITY_EMOJI[finding.severity]} **${finding.rule}** · _${finding.severity}_`,
226
226
  "",
227
227
  finding.message,
228
- "",
229
- `**Ação:** ${finding.action}`,
230
228
  ];
229
+ if (finding.scenario) lines.push("", `**Cenário:** ${finding.scenario}`);
230
+ if (finding.impact) lines.push("", `**Impacto:** ${finding.impact}`);
231
+ lines.push("", `**Ação:** ${finding.action}`);
231
232
  if (finding.evidence) lines.push("", "```", finding.evidence, "```");
232
233
  lines.push("", "_— houndsman_");
233
234
  return lines.join("\n");
@@ -247,6 +248,8 @@ export function renderGeneralBody(findings: readonly FindingState[]): string {
247
248
  `- ${SEVERITY_EMOJI[finding.severity]} **${finding.severity}** \`${location}\` — ${finding.rule}`,
248
249
  ` ${finding.message}`,
249
250
  );
251
+ if (finding.scenario) lines.push(` **Cenário:** ${finding.scenario}`);
252
+ if (finding.impact) lines.push(` **Impacto:** ${finding.impact}`);
250
253
  }
251
254
  return lines.join("\n");
252
255
  }