@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
@@ -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 {
@@ -187,6 +214,9 @@ export {
187
214
  } from "./core/findings-state";
188
215
  export type { GitResult, GitRunner } from "./core/git-runner";
189
216
  export type {
217
+ BusinessDecision,
218
+ CheckCoverage,
219
+ CheckVerdict,
190
220
  HuntEvent,
191
221
  HuntReport,
192
222
  HuntRunnerOptions,
@@ -203,32 +233,51 @@ export type {
203
233
  MakiRunRequest,
204
234
  MakiRunResult,
205
235
  MakiUsage,
236
+ ReviewCheck,
237
+ ReviewRule,
238
+ RuleCheck,
206
239
  } from "./core/hunt";
207
240
  export {
241
+ briefRules,
242
+ businessScore,
243
+ chooseBusinessSummary,
244
+ expectedReviewItems,
208
245
  extractJsonObject,
209
246
  HuntRunner,
210
247
  houndsmanKeyEnvFor,
211
248
  installMaki,
212
249
  MAKI_API_KEY_ENV,
213
250
  MAKI_DISALLOWED_TOOLS,
251
+ MAKI_REVIEW_SYSTEM_PROMPT,
214
252
  makiArgs,
215
253
  makiCurrentFile,
216
254
  makiInstallDir,
217
255
  makiSourceLabel,
218
256
  makiToolsRoot,
219
257
  mapPool,
258
+ mergeFindings,
259
+ mergeRuleChecks,
220
260
  metricsFrom,
261
+ missingReviewItems,
262
+ missingRuleChecks,
221
263
  parseMakiOutput,
264
+ parseReviewChecks,
265
+ parseRuleChecks,
222
266
  probeMakiVersion,
223
267
  readMakiInstall,
268
+ renderChecklistSuffix,
224
269
  renderHuntPrompt,
270
+ renderResumeSuffix,
225
271
  renderRetrySuffix,
226
272
  requireMakiBinary,
227
273
  requireMakiModel,
228
274
  resolveMakiBinary,
229
275
  resolveMakiModel,
230
276
  SpawnMakiRunner,
277
+ summarizeChecks,
278
+ summarizeRuleChecks,
231
279
  uninstallMaki,
280
+ withInferredRule,
232
281
  } from "./core/hunt";
233
282
  export {
234
283
  CORE_DIR,
@@ -242,9 +291,7 @@ export { ProjectConfig } from "./core/project-config";
242
291
  export { checkRegistry } from "./core/registry-check";
243
292
  export { BriefRenderer } from "./core/renderers/brief-renderer";
244
293
  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";
294
+ export { RESUME_MAX_CHARS, renderReviewResume } from "./core/renderers/resume-renderer";
248
295
  export type { Clock } from "./core/services/clock";
249
296
  export { systemClock } from "./core/services/clock";
250
297
  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;
@@ -48,6 +48,8 @@ import { fetchPrComments, type PrCommentsResponse } from "./pr-comments";
48
48
  export type RunListItem = {
49
49
  slug: string;
50
50
  branch: string;
51
+ /** Variante A/B (ausente = run principal da branch). */
52
+ variant?: string;
51
53
  /** Worktree onde a run foi executada (ausente = clone principal). */
52
54
  worktreePath?: string;
53
55
  /** PR revisada (quando iniciada via `--pr`). */
@@ -63,6 +65,8 @@ export type RunListItem = {
63
65
  export type RunDetail = {
64
66
  slug: string;
65
67
  branch: string;
68
+ /** Variante A/B (ausente = run principal da branch). */
69
+ variant?: string;
66
70
  /** Worktree onde a run foi executada (ausente = clone principal). */
67
71
  worktreePath?: string;
68
72
  /** PR revisada (quando iniciada via `--pr`). */
@@ -129,6 +133,7 @@ export class RunRepository {
129
133
  return this.storage.listReviews(this.repo.key).map((review) => {
130
134
  const state = this.storage.readFindings(review.id);
131
135
  return {
136
+ variant: review.variant,
132
137
  slug: review.slug,
133
138
  branch: review.branch,
134
139
  worktreePath: review.worktreePath,
@@ -144,11 +149,11 @@ export class RunRepository {
144
149
 
145
150
  /** Mapa branch → slug das runs (usado para casar PRs com execuções). */
146
151
  branchIndex(): Map<string, string> {
147
- return new Map(
148
- this.storage
149
- .listReviews(this.repo.key)
150
- .map((review) => [review.branch, review.slug]),
151
- );
152
+ const index = new Map<string, string>();
153
+ for (const review of this.reviewsForBranchLinks()) {
154
+ if (!index.has(review.branch)) index.set(review.branch, review.slug);
155
+ }
156
+ return index;
152
157
  }
153
158
 
154
159
  /** Mapa branch → run + worktree (sidebar de PRs). */
@@ -156,15 +161,30 @@ export class RunRepository {
156
161
  string,
157
162
  { slug: string; worktreePath?: string; prNumber?: number }
158
163
  > {
159
- return new Map(
160
- this.storage.listReviews(this.repo.key).map((review) => [
161
- review.branch,
162
- {
163
- slug: review.slug,
164
- worktreePath: review.worktreePath,
165
- prNumber: review.prNumber,
166
- },
167
- ]),
164
+ const index = new Map<
165
+ string,
166
+ { slug: string; worktreePath?: string; prNumber?: number }
167
+ >();
168
+ for (const review of this.reviewsForBranchLinks()) {
169
+ if (index.has(review.branch)) continue;
170
+ index.set(review.branch, {
171
+ slug: review.slug,
172
+ worktreePath: review.worktreePath,
173
+ prNumber: review.prNumber,
174
+ });
175
+ }
176
+ return index;
177
+ }
178
+
179
+ /**
180
+ * Runs ordenadas para links de PR: a principal da branch primeiro (uma
181
+ * variante A/B não deve "roubar" o atalho da PR), depois as mais recentes.
182
+ */
183
+ private reviewsForBranchLinks(): ReviewRecord[] {
184
+ return [...this.storage.listReviews(this.repo.key)].sort(
185
+ (a, b) =>
186
+ Number(Boolean(a.variant)) - Number(Boolean(b.variant)) ||
187
+ b.updatedAt.localeCompare(a.updatedAt),
168
188
  );
169
189
  }
170
190
 
@@ -180,6 +200,7 @@ export class RunRepository {
180
200
  }>(review, "classification.json");
181
201
  return {
182
202
  slug: review.slug,
203
+ variant: review.variant,
183
204
  branch: review.branch,
184
205
  worktreePath: review.worktreePath,
185
206
  prNumber: review.prNumber,
@@ -77,6 +77,8 @@ body.resizing-sidebar { cursor:col-resize; user-select:none; }
77
77
  .run-icon { flex:none; margin-top:2px; color:var(--muted-2); }
78
78
  .run.active .run-icon { color:var(--accent); }
79
79
  .run-branch { font-size:12.5px; font-weight:600; line-height:1.35; word-break:break-word; }
80
+ .run-variant { flex:none; align-self:flex-start; padding:1px 6px; border-radius:999px; font-family:var(--font-mono);
81
+ font-size:9.5px; color:var(--accent); background:var(--accent-soft); border:1px solid var(--accent-line); }
80
82
  .run-meta { display:flex; align-items:center; gap:10px; font-family:var(--font-mono); font-size:10.5px; color:var(--muted); }
81
83
  .run-when { display:inline-flex; align-items:center; gap:6px; }
82
84
  .run-count { margin-left:auto; color:var(--muted-2); }