@payconductor/houndsman 0.13.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 (106) 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/hunt-command.ts +20 -3
  7. package/src/cli/commands/review-command.ts +38 -0
  8. package/src/cli/commands/status-command.ts +3 -2
  9. package/src/cli/help.ts +3 -1
  10. package/src/cli/index.ts +3 -1
  11. package/src/cli/progress.ts +7 -2
  12. package/src/core/ai/ai-client.ts +38 -2
  13. package/src/core/ai/features/brief-enricher.ts +1 -1
  14. package/src/core/ai/features/business-analyzer.ts +61 -0
  15. package/src/core/ai/features/business-summarizer.ts +44 -0
  16. package/src/core/ai/index.ts +8 -0
  17. package/src/core/ai/prompts/brief.ts +33 -7
  18. package/src/core/ai/prompts/business.ts +76 -0
  19. package/src/core/ai/provider-capabilities.ts +12 -0
  20. package/src/core/ai/schemas.ts +70 -2
  21. package/src/core/ai/usage-recorder.ts +53 -1
  22. package/src/core/brief.ts +246 -12
  23. package/src/core/builtins.ts +108 -0
  24. package/src/core/business-summary.ts +583 -0
  25. package/src/core/commands/compare.ts +233 -0
  26. package/src/core/commands/context.ts +6 -2
  27. package/src/core/commands/findings.ts +38 -30
  28. package/src/core/commands/review.ts +6 -0
  29. package/src/core/commands/scaffold.ts +27 -0
  30. package/src/core/commands/status.ts +9 -4
  31. package/src/core/compare.ts +243 -0
  32. package/src/core/concurrency.ts +30 -0
  33. package/src/core/config.ts +31 -3
  34. package/src/core/defaults.ts +50 -4
  35. package/src/core/domain/budget.ts +62 -4
  36. package/src/core/domain/classification.ts +12 -2
  37. package/src/core/engine/carryover.ts +4 -0
  38. package/src/core/engine/finalize-pipeline.ts +347 -0
  39. package/src/core/engine/prepare-pipeline.ts +1294 -0
  40. package/src/core/engine/review-context.ts +51 -0
  41. package/src/core/engine/review-options.ts +47 -0
  42. package/src/core/engine/review-scope.ts +14 -1
  43. package/src/core/engine/review-session.ts +11 -13
  44. package/src/core/engine/run-artifacts.ts +196 -0
  45. package/src/core/engine/run-format.ts +4 -0
  46. package/src/core/findings-state.ts +13 -1
  47. package/src/core/github-review.ts +5 -2
  48. package/src/core/hunt/hunt-runner.ts +90 -0
  49. package/src/core/hunt/index.ts +2 -0
  50. package/src/core/hunt/maki-intent-prompt.ts +50 -0
  51. package/src/core/hunt/maki-prompt.ts +13 -1
  52. package/src/core/hunt/pool.ts +4 -26
  53. package/src/core/logging/index.ts +6 -1
  54. package/src/core/logging/progress.ts +85 -1
  55. package/src/core/orchestrator.ts +33 -1289
  56. package/src/core/paths.ts +6 -3
  57. package/src/core/pr-context.ts +2 -4
  58. package/src/core/prepare-report.ts +61 -0
  59. package/src/core/project-config.ts +8 -1
  60. package/src/core/propagation.ts +127 -0
  61. package/src/core/registry-check.ts +60 -21
  62. package/src/core/renderers/manifest-writer.ts +13 -0
  63. package/src/core/renderers/resume-renderer.ts +151 -11
  64. package/src/core/report-builder.ts +108 -278
  65. package/src/core/storage/file/file-storage.ts +7 -3
  66. package/src/core/storage/sqlite/mappers.ts +10 -0
  67. package/src/core/storage/sqlite/schema.ts +104 -23
  68. package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
  69. package/src/core/storage/types.ts +16 -4
  70. package/src/core/storage/work-area.ts +59 -1
  71. package/src/core/types.ts +31 -0
  72. package/src/core/watchdog-registry.ts +21 -3
  73. package/src/index.ts +29 -4
  74. package/src/server/client/ArtifactsPanel.tsx +4 -2
  75. package/src/server/client/FindingCard.tsx +21 -1
  76. package/src/server/client/RunsSection.tsx +5 -0
  77. package/src/server/client/utils/constants.ts +21 -11
  78. package/src/server/client/utils/markdown.ts +1 -1
  79. package/src/server/client/utils/types.ts +14 -2
  80. package/src/server/run-repository.ts +35 -14
  81. package/src/server/styles.css +2 -0
  82. package/src/skill/OUTPUT-FORMAT.md +36 -10
  83. package/src/skill/README.md +18 -14
  84. package/src/skill/SETUP.md +1 -0
  85. package/src/skill/SKILL.md +13 -4
  86. package/src/skill/example-resume.md +43 -0
  87. package/src/skill/hunt.md +145 -58
  88. package/src/tests/__snapshots__/baseline.test.ts.snap +187 -178
  89. package/src/tests/ai-core.test.ts +46 -0
  90. package/src/tests/ai-features.test.ts +95 -4
  91. package/src/tests/baseline.test.ts +29 -121
  92. package/src/tests/brief-scope.test.ts +250 -0
  93. package/src/tests/business-summary.test.ts +258 -0
  94. package/src/tests/compare.test.ts +233 -0
  95. package/src/tests/fixtures/fake-model-provider.ts +2 -1
  96. package/src/tests/hunt-maki.test.ts +58 -0
  97. package/src/tests/markdown.test.ts +3 -3
  98. package/src/tests/progress.test.ts +29 -1
  99. package/src/tests/propagation.test.ts +119 -0
  100. package/src/tests/review-resume.test.ts +60 -1
  101. package/src/tests/review-scope.test.ts +38 -29
  102. package/src/tests/review-scripts.test.ts +106 -218
  103. package/src/tests/server.test.ts +7 -4
  104. package/src/tests/storage.test.ts +194 -9
  105. package/src/core/renderers/report-renderer.ts +0 -66
  106. package/src/skill/example-review.md +0 -66
@@ -69,7 +69,7 @@ export class SQLiteStorageProvider implements StorageProvider {
69
69
  // -----------------------------------------------------------------------
70
70
 
71
71
  getOrCreateReview(input: CreateReviewInput): ReviewRecord {
72
- const existing = this.findReview(input.repo.key, input.branch);
72
+ const existing = this.findReview(input.repo.key, input.branch, input.variant);
73
73
  if (existing) {
74
74
  // O repo pode ter sido movido; a identidade continua a mesma.
75
75
  const moved =
@@ -102,6 +102,7 @@ export class SQLiteStorageProvider implements StorageProvider {
102
102
  repoRoot: input.repo.root,
103
103
  repoRemote: input.repo.remote,
104
104
  branch: input.branch,
105
+ variant: input.variant?.trim() || undefined,
105
106
  slug: input.slug,
106
107
  base: input.base,
107
108
  head: input.head,
@@ -113,15 +114,16 @@ export class SQLiteStorageProvider implements StorageProvider {
113
114
  };
114
115
  this.db.run(
115
116
  `INSERT INTO reviews
116
- (id, repo_key, repo_root, repo_remote, branch, slug, base, head,
117
+ (id, repo_key, repo_root, repo_remote, branch, variant, slug, base, head,
117
118
  worktree_path, pr_number, created_at, updated_at)
118
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
119
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
119
120
  [
120
121
  record.id,
121
122
  record.repoKey,
122
123
  record.repoRoot,
123
124
  record.repoRemote ?? null,
124
125
  record.branch,
126
+ record.variant ?? "",
125
127
  record.slug,
126
128
  record.base ?? null,
127
129
  record.head ?? null,
@@ -134,10 +136,12 @@ export class SQLiteStorageProvider implements StorageProvider {
134
136
  return record;
135
137
  }
136
138
 
137
- findReview(repoKey: string, branch: string): ReviewRecord | null {
139
+ findReview(repoKey: string, branch: string, variant?: string): ReviewRecord | null {
138
140
  const row = this.db
139
- .query("SELECT * FROM reviews WHERE repo_key = ? AND branch = ?")
140
- .get(repoKey, branch) as ReviewRow | null;
141
+ .query(
142
+ "SELECT * FROM reviews WHERE repo_key = ? AND branch = ? AND variant = ?",
143
+ )
144
+ .get(repoKey, branch, variant?.trim() ?? "") as ReviewRow | null;
141
145
  return row ? reviewFromRow(row) : null;
142
146
  }
143
147
 
@@ -253,9 +257,10 @@ export class SQLiteStorageProvider implements StorageProvider {
253
257
  const upsert = this.db.query(
254
258
  `INSERT INTO findings
255
259
  (id, review_id, short_id, fingerprint, watchdog_ids, rule, severity,
256
- file, line, message, action, evidence, anchor, found_at_sha, count,
257
- present, decision, comment, exec_state, github, created_at, updated_at)
258
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
260
+ file, line, message, action, evidence, scenario, impact, confidence, basis,
261
+ anchor, found_at_sha, count, present, decision, comment, exec_state, github,
262
+ created_at, updated_at)
263
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
259
264
  ON CONFLICT(id) DO UPDATE SET
260
265
  short_id = excluded.short_id,
261
266
  fingerprint = excluded.fingerprint,
@@ -267,6 +272,10 @@ export class SQLiteStorageProvider implements StorageProvider {
267
272
  message = excluded.message,
268
273
  action = excluded.action,
269
274
  evidence = excluded.evidence,
275
+ scenario = excluded.scenario,
276
+ impact = excluded.impact,
277
+ confidence = excluded.confidence,
278
+ basis = excluded.basis,
270
279
  anchor = excluded.anchor,
271
280
  found_at_sha = excluded.found_at_sha,
272
281
  count = excluded.count,
@@ -325,6 +334,10 @@ export class SQLiteStorageProvider implements StorageProvider {
325
334
  finding.message,
326
335
  finding.action,
327
336
  finding.evidence ?? null,
337
+ finding.scenario ?? null,
338
+ finding.impact ?? null,
339
+ finding.confidence ?? null,
340
+ finding.basis ?? null,
328
341
  finding.anchor ? JSON.stringify(finding.anchor) : null,
329
342
  finding.foundAtSha ?? null,
330
343
  finding.count,
@@ -28,6 +28,13 @@ export type ReviewRecord = {
28
28
  repoRoot: string;
29
29
  repoRemote?: string;
30
30
  branch: string;
31
+ /**
32
+ * Rótulo da variante A/B (ausente = run principal da branch).
33
+ *
34
+ * Faz parte da identidade junto com `(repoKey, branch)`: permite rodar a
35
+ * mesma PR com dois experimentos sem apagar a revisão anterior.
36
+ */
37
+ variant?: string;
31
38
  slug: string;
32
39
  base?: string;
33
40
  head?: string;
@@ -81,6 +88,8 @@ export type CreateReviewInput = {
81
88
  repo: RepoIdentity;
82
89
  branch: string;
83
90
  slug: string;
91
+ /** Variante A/B (ausente = run principal da branch). */
92
+ variant?: string;
84
93
  base?: string;
85
94
  head?: string;
86
95
  /** Worktree onde a revisão roda (ausente = clone principal). */
@@ -99,11 +108,11 @@ export type ArtifactKind =
99
108
  | "context"
100
109
  | "context-brief"
101
110
  | "context-summary"
111
+ | "business"
112
+ | "business-brief"
102
113
  | "finding-report"
103
- | "review"
104
114
  | "resume"
105
115
  | "technical"
106
- | "domain-review"
107
116
  | "verify"
108
117
  | "ast"
109
118
  | "ast-impact"
@@ -111,6 +120,8 @@ export type ArtifactKind =
111
120
  | "ai-usage"
112
121
  | "ai-brief"
113
122
  | "hunt"
123
+ | "prepare"
124
+ | "compare"
114
125
  | "other";
115
126
 
116
127
  /** Artefato persistido de uma review. */
@@ -142,9 +153,10 @@ export type WriteArtifactInput = {
142
153
  export interface StorageProvider {
143
154
  readonly kind: "sqlite" | "file";
144
155
 
145
- /** Recupera ou cria a review de uma branch (identidade = repo + branch). */
156
+ /** Busca por identidade `(repo, branch, variante)`; variante ausente = principal. */
157
+ findReview(repoKey: string, branch: string, variant?: string): ReviewRecord | null;
158
+ /** Run da branch + variante (cria quando não existe). */
146
159
  getOrCreateReview(input: CreateReviewInput): ReviewRecord;
147
- findReview(repoKey: string, branch: string): ReviewRecord | null;
148
160
  getReview(id: string): ReviewRecord | null;
149
161
  /** Reviews do repositório (mais recentes primeiro); sem chave = todas. */
150
162
  listReviews(repoKey?: string): ReviewRecord[];
@@ -9,7 +9,10 @@
9
9
 
10
10
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
+ import { type BusinessSummary, parseBusinessSummary } from "../business-summary";
12
13
  import {
14
+ BUSINESS_BRIEF_FILE,
15
+ BUSINESS_SUMMARY_FILE,
13
16
  CONTEXT_BRIEF_FILE,
14
17
  CONTEXT_FILE,
15
18
  CONTEXT_SUMMARY_FILE,
@@ -97,7 +100,6 @@ export class ReviewWorkArea {
97
100
  brief: this.file(CONTEXT_BRIEF_FILE),
98
101
  };
99
102
  }
100
-
101
103
  /** Garante os diretórios; `clean` recria a área do zero. */
102
104
  ensure(clean = false): void {
103
105
  if (clean) this.clean();
@@ -143,6 +145,62 @@ export class ReviewWorkArea {
143
145
  writeFileSync(this.contextPaths.summary, `${JSON.stringify(summary, null, 2)}\n`);
144
146
  }
145
147
 
148
+ /** Caminhos do modelo de intenção (JSON + brief do passo). */
149
+ get businessPaths(): { summary: string; brief: string } {
150
+ return {
151
+ summary: this.file(BUSINESS_SUMMARY_FILE),
152
+ brief: this.file(BUSINESS_BRIEF_FILE),
153
+ };
154
+ }
155
+
156
+ /** Grava o modelo de intenção (determinístico, IA ou revisor). */
157
+ writeBusinessSummary(summary: unknown): void {
158
+ mkdirSync(this.root, { recursive: true });
159
+ writeFileSync(
160
+ this.businessPaths.summary,
161
+ `${JSON.stringify(summary, null, 2)}\n`,
162
+ );
163
+ }
164
+
165
+ /** Grava o brief do passo de intenção. */
166
+ writeBusinessBrief(content: string): void {
167
+ mkdirSync(this.root, { recursive: true });
168
+ writeFileSync(this.businessPaths.brief, `${content}\n`);
169
+ }
170
+
171
+ /** Modelo de intenção gravado (subagente/IA) — null se ausente/inválido. */
172
+ readBusinessSummary(): BusinessSummary | null {
173
+ const raw = this.readBusinessSummaryRaw();
174
+ if (raw === null) return null;
175
+ try {
176
+ return parseBusinessSummary(JSON.parse(raw), "reviewer");
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ /** Conteúdo cru do modelo de intenção (para persistir como artefato). */
183
+ readBusinessSummaryRaw(): string | null {
184
+ const path = this.businessPaths.summary;
185
+ if (!existsSync(path)) return null;
186
+ try {
187
+ return readFileSync(path, "utf8");
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
193
+ /** Conteúdo cru do brief de intenção (para persistir como artefato). */
194
+ readBusinessBriefRaw(): string | null {
195
+ const path = this.businessPaths.brief;
196
+ if (!existsSync(path)) return null;
197
+ try {
198
+ return readFileSync(path, "utf8");
199
+ } catch {
200
+ return null;
201
+ }
202
+ }
203
+
146
204
  /** Finds brutos gravados pelos subagentes (um JSON por watchdog). */
147
205
  readFindings(activeIds: string[]): FindingsIngest {
148
206
  const reports: WatchdogReport[] = [];
package/src/core/types.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { BriefPlan } from "./ai/schemas";
10
10
  import type { AstFileBrief } from "./ast/types";
11
+ import type { BusinessSummary } from "./business-summary";
11
12
  import type { ChangedScope } from "./changed-scope";
12
13
  import type { JevRuleSpec } from "./jev/jev-types";
13
14
 
@@ -131,8 +132,38 @@ export type WatchdogRenderArgs = {
131
132
  ast?: readonly AstFileBrief[];
132
133
  /** Plano de revisão sugerido por IA (nunca substitui as regras). */
133
134
  aiPlan?: BriefPlan;
135
+ /** Modelo de intenção da PR (negócio) — insumo do revisor `general`. */
136
+ business?: BusinessSummary;
137
+ /** Caminho do `business-summary.json` no staging (versão canônica). */
138
+ businessSummaryPath?: string;
139
+ /** Trechos alterados (`diff` por arquivo) — só para o revisor de fluxo. */
140
+ snippets?: readonly { file: string; snippet: string }[];
141
+ /** `true` no revisor de fluxo: exige devolver/gravar o modelo de intenção. */
142
+ flowReview?: boolean;
134
143
  /** Escopo do diff por arquivo (símbolos alterados; hunks como fallback). */
135
144
  changedScope?: ChangedScope;
145
+ /** Micro-alterações iguais em muitos arquivos (propagação mecânica). */
146
+ propagation?: PropagationCluster | null;
147
+ /** Contratos alterados cujos importadores não mudaram (possível lacuna). */
148
+ propagationGaps?: readonly PropagationGap[];
149
+ };
150
+
151
+ /** Cluster de propagação mecânica (ex.: novo parâmetro em 100 call sites). */
152
+ export type PropagationCluster = {
153
+ /** Linhas adicionadas normalizadas (o padrão dominante). */
154
+ lines: string[];
155
+ /** Arquivos que seguem o padrão. */
156
+ files: string[];
157
+ /** Arquivos com micro-alteração que NÃO seguem o padrão. */
158
+ outliers: { file: string; lines: string[] }[];
159
+ };
160
+
161
+ /** Contrato que mudou e tem importadores fora do diff. */
162
+ export type PropagationGap = {
163
+ /** Arquivo do contrato (o que mudou). */
164
+ file: string;
165
+ /** Importadores que não aparecem no diff. */
166
+ importers: string[];
136
167
  };
137
168
 
138
169
  /** Definição completa de um watchdog — 1 por arquivo em `project/watchdogs/`. */
@@ -6,6 +6,7 @@
6
6
  * adaptadores para a API pública.
7
7
  */
8
8
 
9
+ import { builtinWatchdog, GENERAL_WATCHDOG_ID, mergeGeneralWatchdog } from "./builtins";
9
10
  import type { HoundsmanConfig } from "./config";
10
11
  import { Watchdog, type WatchdogContext } from "./domain/watchdog";
11
12
  import type { WatchdogDefinition } from "./types";
@@ -23,17 +24,34 @@ export class WatchdogRegistry<TId extends string = string> {
23
24
  return this.config.watchdogIds;
24
25
  }
25
26
 
26
- /** Definição de um watchdog (id string em runtime). */
27
+ /** Definição de um watchdog (config vence; senão, built-in). */
27
28
  get(watchdogId: string): WatchdogDefinition {
28
- return this.config.watchdogs[watchdogId as TId];
29
+ const definition = this.definitionOf(watchdogId);
30
+ if (!definition) {
31
+ throw new Error(`Watchdog '${watchdogId}' não encontrado.`);
32
+ }
33
+ return definition;
29
34
  }
30
35
 
31
36
  /** Busca null-safe por id. */
32
37
  find(watchdogId: string): Watchdog | null {
33
- const definition = this.config.watchdogs[watchdogId as TId];
38
+ const definition = this.definitionOf(watchdogId);
34
39
  return definition ? new Watchdog(definition) : null;
35
40
  }
36
41
 
42
+ /**
43
+ * Resolve a definição: config primeiro, built-in depois. O `general` é
44
+ * reservado e sempre inclui o piso de lógica do motor (mesmo se o consumidor
45
+ * declarar um watchdog com esse id).
46
+ */
47
+ private definitionOf(watchdogId: string): WatchdogDefinition | null {
48
+ const configured = this.config.watchdogs[watchdogId as TId];
49
+ if (watchdogId === GENERAL_WATCHDOG_ID) {
50
+ return mergeGeneralWatchdog(configured);
51
+ }
52
+ return configured ?? builtinWatchdog(watchdogId);
53
+ }
54
+
37
55
  /** Nome legível (default: o próprio id). */
38
56
  nameOf(watchdogId: string): string {
39
57
  return this.find(watchdogId)?.name ?? watchdogId;
package/src/index.ts CHANGED
@@ -50,6 +50,29 @@ export type {
50
50
  AstSymbolKind,
51
51
  } from "./core/ast/types";
52
52
  export { defaultRenderBrief, PROCEDURE, renderBrief } from "./core/brief";
53
+ export {
54
+ BUILTIN_WATCHDOGS,
55
+ builtinWatchdog,
56
+ GENERAL_WATCHDOG_ID,
57
+ generalWatchdog,
58
+ mergeGeneralWatchdog,
59
+ } from "./core/builtins";
60
+ export {
61
+ BUSINESS_SUMMARY_SCHEMA,
62
+ type BusinessBriefInput,
63
+ type BusinessContract,
64
+ type BusinessFlow,
65
+ type BusinessInput,
66
+ type BusinessState,
67
+ type BusinessSummary,
68
+ type BusinessSummarySource,
69
+ buildBusinessBrief,
70
+ buildBusinessPromptPayload,
71
+ buildBusinessSummary,
72
+ parseBusinessSummary,
73
+ renderBusinessLines,
74
+ summarizeBusiness,
75
+ } from "./core/business-summary";
53
76
  export type {
54
77
  BuildChangedScopeInput,
55
78
  ChangedScope,
@@ -67,7 +90,10 @@ export {
67
90
  mergeRanges,
68
91
  summarizeScope,
69
92
  } from "./core/changed-scope";
70
- export type { FindingsCommandResult } from "./core/commands/findings";
93
+ export type {
94
+ FindingsCommandResult,
95
+ FindingsContextOptions,
96
+ } from "./core/commands/findings";
71
97
  export { runFindingsCommand, runVerifyCommand } from "./core/commands/findings";
72
98
  export { ProjectScaffolder } from "./core/commands/scaffold";
73
99
  export type {
@@ -81,6 +107,7 @@ export type {
81
107
  HoundsmanConfig,
82
108
  HoundsmanConfigInput,
83
109
  HoundsmanHunt,
110
+ HoundsmanPriority,
84
111
  HoundsmanStorage,
85
112
  } from "./core/config";
86
113
  export {
@@ -242,9 +269,7 @@ export { ProjectConfig } from "./core/project-config";
242
269
  export { checkRegistry } from "./core/registry-check";
243
270
  export { BriefRenderer } from "./core/renderers/brief-renderer";
244
271
  export { ManifestWriter } from "./core/renderers/manifest-writer";
245
- export { ReportRenderer } from "./core/renderers/report-renderer";
246
- export type { DomainReport } from "./core/report-builder";
247
- export { renderDomainReport } from "./core/report-builder";
272
+ export { RESUME_MAX_CHARS, renderReviewResume } from "./core/renderers/resume-renderer";
248
273
  export type { Clock } from "./core/services/clock";
249
274
  export { systemClock } from "./core/services/clock";
250
275
  export { FileClassifier } from "./core/services/file-classifier";
@@ -19,9 +19,11 @@ import {
19
19
  import { formatJson, highlightJson } from "./utils/json-highlight";
20
20
  import type { ArtifactMeta } from "./utils/types";
21
21
 
22
- /** Prefere o relatório consolidado; senão, o primeiro artefato disponível. */
22
+ /** Prefere o resumo final; senão, o primeiro artefato disponível. */
23
23
  function pickDefault(items: ArtifactMeta[]): string {
24
- return (items.find((item) => item.name === "review.md") ?? items[0])?.name ?? "";
24
+ return (
25
+ (items.find((item) => item.name === "review-resume.md") ?? items[0])?.name ?? ""
26
+ );
25
27
  }
26
28
 
27
29
  /** JSON com destaque de sintaxe (tokens em spans, sem lib externa). */
@@ -157,7 +157,7 @@ export function FindingCard({
157
157
  </span>
158
158
  )}
159
159
  {!finding.present && (
160
- <span class="tag" title="Não aparece no último relatório consolidado">
160
+ <span class="tag" title="Não aparece na última revisão">
161
161
  ausente
162
162
  </span>
163
163
  )}
@@ -206,6 +206,26 @@ export function FindingCard({
206
206
  <div class="card-body">
207
207
  <div class="meta-row">{finding.rule}</div>
208
208
 
209
+ {finding.scenario && (
210
+ <div class="action-block">
211
+ <Sparkles size={16} class="action-icon" />
212
+ <div class="action-text">
213
+ <span class="action-label">Cenário:</span>
214
+ <MarkdownText text={finding.scenario} />
215
+ </div>
216
+ </div>
217
+ )}
218
+
219
+ {finding.impact && (
220
+ <div class="action-block">
221
+ <Sparkles size={16} class="action-icon" />
222
+ <div class="action-text">
223
+ <span class="action-label">Impacto:</span>
224
+ <MarkdownText text={finding.impact} />
225
+ </div>
226
+ </div>
227
+ )}
228
+
209
229
  <div class="action-block">
210
230
  <Sparkles size={16} class="action-icon" />
211
231
  <div class="action-text">
@@ -30,6 +30,11 @@ function RunItem({
30
30
  <span class="run-top">
31
31
  <BranchIcon size={14} class="run-icon" />
32
32
  <span class="run-branch">{run.branch}</span>
33
+ {run.variant ? (
34
+ <span class="run-variant" title="Variante A/B">
35
+ {run.variant}
36
+ </span>
37
+ ) : null}
33
38
  </span>
34
39
  <span class="run-meta">
35
40
  <span class="run-when">
@@ -83,13 +83,13 @@ export function describeRunStatus(
83
83
 
84
84
  /** Ordem canônica dos tipos de artefato na lista da UI. */
85
85
  const KIND_ORDER: ArtifactKind[] = [
86
- "review",
86
+ "compare",
87
87
  "resume",
88
88
  "technical",
89
- "domain-review",
90
89
  "verify",
91
90
  "brief",
92
91
  "manifest",
92
+ "prepare",
93
93
  "classification",
94
94
  "candidates",
95
95
  "ast",
@@ -100,6 +100,8 @@ const KIND_ORDER: ArtifactKind[] = [
100
100
  "context",
101
101
  "context-brief",
102
102
  "context-summary",
103
+ "business",
104
+ "business-brief",
103
105
  "finding-report",
104
106
  "other",
105
107
  ];
@@ -107,12 +109,12 @@ const KIND_ORDER: ArtifactKind[] = [
107
109
  /** Grupo visual de cada tipo (relatórios, briefs, núcleo, contexto…). */
108
110
  const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: number }> =
109
111
  {
110
- review: { key: "report", label: "Relatórios", order: 0 },
112
+ compare: { key: "report", label: "Relatórios", order: 0 },
111
113
  resume: { key: "report", label: "Relatórios", order: 0 },
112
114
  technical: { key: "core", label: "Núcleo da revisão", order: 3 },
113
- "domain-review": { key: "report", label: "Relatórios", order: 0 },
114
115
  verify: { key: "verify", label: "Verificação", order: 1 },
115
116
  brief: { key: "brief", label: "Briefs (auditoria)", order: 2 },
117
+ prepare: { key: "core", label: "Núcleo da revisão", order: 3 },
116
118
  manifest: { key: "core", label: "Núcleo da revisão", order: 3 },
117
119
  classification: { key: "core", label: "Núcleo da revisão", order: 3 },
118
120
  candidates: { key: "core", label: "Núcleo da revisão", order: 3 },
@@ -124,17 +126,19 @@ const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: n
124
126
  context: { key: "context", label: "Contexto da PR", order: 5 },
125
127
  "context-brief": { key: "context", label: "Contexto da PR", order: 5 },
126
128
  "context-summary": { key: "context", label: "Contexto da PR", order: 5 },
129
+ business: { key: "business", label: "Intenção da PR", order: 5 },
130
+ "business-brief": { key: "business", label: "Intenção da PR", order: 5 },
127
131
  "finding-report": { key: "other", label: "Outros", order: 6 },
128
132
  other: { key: "other", label: "Outros", order: 6 },
129
133
  };
130
134
 
131
135
  /** Rótulo curto do tipo (badge do item). */
132
136
  export const ARTIFACT_KIND_LABELS: Record<ArtifactKind, string> = {
133
- review: "relatório",
137
+ compare: "comparação",
134
138
  resume: "resumo",
135
139
  technical: "pontos técnicos",
136
- "domain-review": "domínio",
137
140
  verify: "verify",
141
+ prepare: "prepare",
138
142
  brief: "brief",
139
143
  manifest: "manifesto",
140
144
  classification: "classificação",
@@ -147,6 +151,8 @@ export const ARTIFACT_KIND_LABELS: Record<ArtifactKind, string> = {
147
151
  context: "contexto",
148
152
  "context-brief": "brief ctx",
149
153
  "context-summary": "resumo ctx",
154
+ business: "intenção",
155
+ "business-brief": "brief intenção",
150
156
  "finding-report": "findings",
151
157
  other: "outro",
152
158
  };
@@ -161,16 +167,16 @@ export function displayArtifactName(meta: ArtifactMeta): string {
161
167
  /** Descrição curta do artefato (cabeçalho do painel). */
162
168
  export function describeArtifact(meta: ArtifactMeta): string {
163
169
  switch (meta.kind) {
164
- case "review":
165
- return "Relatório consolidado da revisão — o entregável.";
170
+ case "compare":
171
+ return "Comparação A/B entre duas runs: achados novos/sumidos, cobertura, tempo e tokens.";
166
172
  case "resume":
167
- return "Resumo final da revisão: visão de negócio e estrutura técnica (leitura humana).";
173
+ return "Resumo final: negócio, pontos técnicos, cobertura por domínio e conclusão.";
168
174
  case "technical":
169
175
  return "Destaques estruturais da mudança (módulos, contratos, dependências) usados no resumo final.";
170
- case "domain-review":
171
- return "Relatório isolado do domínio: achados daquele watchdog.";
172
176
  case "verify":
173
177
  return "Resultado do `findings verify` (resolvidos, persistentes e incertos).";
178
+ case "prepare":
179
+ return "Duração por etapa do prepare e uso de IA (diagnóstico de performance).";
174
180
  case "brief":
175
181
  return "Instruções enviadas ao subagente (auditoria do prompt).";
176
182
  case "manifest":
@@ -189,6 +195,10 @@ export function describeArtifact(meta: ArtifactMeta): string {
189
195
  return "Brief do subagente que resume o contexto da PR.";
190
196
  case "context-summary":
191
197
  return "Resumo da PR (objetivo, motivação e alterações) gerado no Passo 0.";
198
+ case "business":
199
+ return "Modelo de intenção da PR: o que ela deve entregar (fluxos, invariantes, contratos e estados).";
200
+ case "business-brief":
201
+ return "Brief do passo de intenção (subagente na skill / pré-passe maki).";
192
202
  default:
193
203
  return meta.name;
194
204
  }
@@ -240,7 +240,7 @@ export function sectionBlocks(text: string, title: string): MarkdownBlock[] {
240
240
  const FILE_EXTENSION =
241
241
  /\.(?:[cm]?[jt]sx?|json|md|ya?ml|toml|css|scss|html?|py|go|rs|java|kt|cs|php|rb|sh|sql|txt|lock)$/i;
242
242
 
243
- /** Caminho/arquivo dentro de um texto corrido (`src/foo.ts`, `review.md`). */
243
+ /** Caminho/arquivo dentro de um texto corrido (`src/foo.ts`, `manifest.json`). */
244
244
  const PATH_TOKEN =
245
245
  /(?:[\w.-]+[\\/])*[\w-]+\.(?:[cm]?[jt]sx?|json|md|ya?ml|toml|css|scss|html?|py|go|rs|java|kt|cs|php|rb|sh|sql|txt|lock)\b/g;
246
246
 
@@ -32,6 +32,14 @@ export type Finding = {
32
32
  message: string;
33
33
  action: string;
34
34
  evidence?: string;
35
+ /** Gatilho concreto do gap (fora de regra). */
36
+ scenario?: string;
37
+ /** Consequência observável do gap. */
38
+ impact?: string;
39
+ /** Confiança do revisor (0–1). */
40
+ confidence?: number;
41
+ /** Origem do achado: regra, lógica ou domínio. */
42
+ basis?: "rule" | "logic" | "domain";
35
43
  count: number;
36
44
  present: boolean;
37
45
  decision: Decision;
@@ -59,17 +67,19 @@ export type ArtifactKind =
59
67
  | "context"
60
68
  | "context-brief"
61
69
  | "context-summary"
70
+ | "business"
71
+ | "business-brief"
62
72
  | "finding-report"
63
- | "review"
64
73
  | "resume"
65
74
  | "technical"
66
- | "domain-review"
67
75
  | "verify"
68
76
  | "ast"
69
77
  | "ast-impact"
70
78
  | "ai"
71
79
  | "ai-usage"
72
80
  | "ai-brief"
81
+ | "prepare"
82
+ | "compare"
73
83
  | "other";
74
84
 
75
85
  /** Metadados de artefato retornados pela API (sem conteúdo). */
@@ -83,6 +93,8 @@ export type ArtifactMeta = {
83
93
  export type RunSummary = {
84
94
  slug: string;
85
95
  branch: string;
96
+ /** Variante A/B (ausente = run principal da branch). */
97
+ variant?: string;
86
98
  /** Worktree onde a run foi executada (ausente = clone principal). */
87
99
  worktreePath?: string;
88
100
  prNumber?: number;