@payconductor/houndsman 0.4.0 → 0.5.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.
- package/README.md +12 -38
- package/package.json +1 -1
- package/src/cli/commands/status-command.ts +46 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +3 -0
- package/src/core/commands/findings.ts +61 -5
- package/src/core/commands/status.ts +63 -0
- package/src/core/engine/review-scope.ts +26 -8
- package/src/core/findings-state.ts +21 -4
- package/src/core/github-review.ts +620 -57
- package/src/core/orchestrator.ts +98 -8
- package/src/core/paths.ts +3 -0
- package/src/core/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/run-status.ts +39 -5
- package/src/core/storage/file/file-storage.ts +2 -0
- package/src/core/storage/sqlite/mappers.ts +2 -0
- package/src/core/storage/sqlite/schema.ts +9 -1
- package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
- package/src/core/storage/types.ts +9 -0
- package/src/server/client/App.tsx +26 -9
- package/src/server/client/ArtifactsPanel.tsx +12 -5
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +14 -10
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +92 -12
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +49 -6
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/{constants.ts → utils/constants.ts} +34 -5
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +58 -0
- package/src/server/index.ts +63 -5
- package/src/server/run-repository.ts +9 -0
- package/src/skill/example-review.md +22 -14
- package/src/skill/hunt.md +31 -17
- package/src/skill/verify.md +2 -1
- package/src/tests/__snapshots__/baseline.test.ts.snap +32 -30
- package/src/tests/baseline.test.ts +3 -0
- package/src/tests/code-highlight.test.ts +1 -1
- package/src/tests/findings-state.test.ts +50 -2
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/markdown.test.ts +61 -1
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +73 -0
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +53 -0
- package/src/tests/server.test.ts +76 -21
- package/src/tests/storage.test.ts +63 -0
- package/src/server/client/markdown.ts +0 -153
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
- /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
package/README.md
CHANGED
|
@@ -26,8 +26,8 @@ diferente das PRs. O motor só coordena a caçada — as regras são problema se
|
|
|
26
26
|
|
|
27
27
|
- **Bun** — runtime obrigatório.
|
|
28
28
|
- **git** — o diff usa `git diff <base>...<head>`.
|
|
29
|
-
-
|
|
30
|
-
|
|
29
|
+
- **gh** (Opcional) — Obtém contexto da PR e dispara comentários para o GitHub
|
|
30
|
+
- **`TYPESAFE_API_KEY`** (Opcional) — Env a ser informada na raiz, usada em pré-analise com JEV
|
|
31
31
|
|
|
32
32
|
### 2. Instale o CLI (global)
|
|
33
33
|
|
|
@@ -95,62 +95,38 @@ nenhum script à mão.
|
|
|
95
95
|
|
|
96
96
|
O skill tem **4 capabilities**, todas em `/houndsman`:
|
|
97
97
|
|
|
98
|
-
- **`hunt`** — revisa a PR
|
|
98
|
+
- **`hunt`** — revisa a PR.
|
|
99
99
|
- **`train`** — mantém/atualiza as regras dos watchdogs.
|
|
100
|
-
- **`fix`** — corrige os findings marcados
|
|
101
|
-
- **`verify`** — confere se as violações foram resolvidas
|
|
100
|
+
- **`fix`** — corrige os findings marcados.
|
|
101
|
+
- **`verify`** — confere se as violações foram resolvidas, sem re-revisar.
|
|
102
102
|
|
|
103
|
-
- Revisão normal: `/houndsman` ou `/houndsman hunt` (a branch é detectada; base default `main`).
|
|
104
103
|
- Revisar outra branch: `/houndsman hunt feature/minha-branch`.
|
|
105
|
-
- Validar
|
|
106
|
-
- Manual (sem skill): `houndsman context` → `houndsman --base main` → *(subagentes)* →
|
|
107
|
-
`houndsman --finalize`.
|
|
108
|
-
- CI: `houndsman --finalize --fail-on-blocking` (exit 1 se houver bloqueantes).
|
|
104
|
+
- Validar na UI: `houndsman server --open` — marque os findings a executar (`execute`).
|
|
109
105
|
|
|
110
106
|
---
|
|
111
107
|
|
|
112
|
-
##
|
|
108
|
+
## Validando os findings (opcional)
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
no storage global. Você decide o que fazer com cada um — pela
|
|
116
|
-
**UI local** ou pelo **agente** (auto-fix) — sem abrir o JSON à mão:
|
|
110
|
+
Você decide o que fazer com cada finding pela **UI local**, informando se quer `Executar` ou `Ignorar`
|
|
117
111
|
|
|
118
112
|
```bash
|
|
119
113
|
bun run houndsman server --open # UI: triar, comentar, marcar feito, ler artefatos
|
|
120
114
|
```
|
|
121
115
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
**Auto-fix pelo agente** — as linhas mudam depois do fix, e o motor reancora por conteúdo:
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
bun run houndsman findings batch --json # grupo atual (agrupamento automático) + outros
|
|
129
|
-
# ... o agente corrige apenas esse grupo ...
|
|
130
|
-
bun run houndsman findings sync # reancora por conteúdo
|
|
131
|
-
bun run houndsman findings done <id>
|
|
132
|
-
bun run houndsman findings verify # confere o que resolveu (barato, sem re-revisar)
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
> A partir da 2ª revisão o `hunt` é **incremental**: revisa só os arquivos que mudaram desde
|
|
136
|
-
> o último `--finalize` e **carrega** os findings dos arquivos inalterados. `--full` força
|
|
137
|
-
> o review completo.
|
|
138
|
-
|
|
139
|
-
> A identidade do finding é o **conteúdo** (fingerprint), não a linha: itens deslocados
|
|
140
|
-
> pelo fix não se perdem. O estado mora no storage global (`<HOUNDSMAN_HOME>/houndsman.db`).
|
|
116
|
+
> A partir da 2ª revisão o `hunt` é **incremental**: revisa apenas os arquivos que mudaram desde
|
|
117
|
+
> a última revisão. `--full` força o review completo.
|
|
141
118
|
|
|
142
119
|
---
|
|
143
120
|
|
|
144
121
|
## Mantendo a configuração
|
|
145
122
|
|
|
146
123
|
- **Persistência** → por default, SQLite global em `<HOUNDSMAN_HOME>/houndsman.db`
|
|
147
|
-
(`HOUNDSMAN_HOME` sobrescreve o diretório de dados).
|
|
148
|
-
troque só o provider — a interface `StorageProvider` é a mesma:
|
|
124
|
+
(`HOUNDSMAN_HOME` sobrescreve o diretório de dados). Caso queira storage base em arquivos, na config, troque provider para `file`:
|
|
149
125
|
|
|
150
126
|
```ts
|
|
151
127
|
export const houndsmanConfig = {
|
|
152
128
|
// ...
|
|
153
|
-
storage: { provider: "file", path: "./houndsman
|
|
129
|
+
storage: { provider: "file", path: "./houndsman/runs" },
|
|
154
130
|
};
|
|
155
131
|
```
|
|
156
132
|
|
|
@@ -165,8 +141,6 @@ bun run houndsman findings verify # confere o que resolveu (barato, se
|
|
|
165
141
|
|
|
166
142
|
O `update` reescreve **só** `.houndsman/core/**` e `.houndsman/.gitignore`. Seus arquivos
|
|
167
143
|
(`watchdogs/`, `exclusions.ts`, `doc-mappings.ts`, `houndsman.config.ts`) nunca são tocados.
|
|
168
|
-
- **Recomeçar a camada do zero** → `houndsman setup --force` (sobrescreve só os arquivos do
|
|
169
|
-
usuário; sem `--force` o `setup` é idempotente).
|
|
170
144
|
|
|
171
145
|
---
|
|
172
146
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payconductor/houndsman",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Orquestrador de revisão de Pull Requests: classifica o diff, despacha watchdogs especializados por domínio e consolida o relatório.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Comando `status` — mostra o status da run ou renova o heartbeat (`status ping`). */
|
|
2
|
+
|
|
3
|
+
import { pingStatus, readStatus } from "../../core/commands/status";
|
|
4
|
+
import { ArgParser } from "../arg-parser";
|
|
5
|
+
import type { CliContext, Command } from "./command";
|
|
6
|
+
|
|
7
|
+
export class StatusCommand implements Command {
|
|
8
|
+
readonly name = "status";
|
|
9
|
+
|
|
10
|
+
async run(args: string[], context: CliContext): Promise<number> {
|
|
11
|
+
const parser = ArgParser.from(args);
|
|
12
|
+
const action = parser.command();
|
|
13
|
+
const branch = parser.value("--branch");
|
|
14
|
+
|
|
15
|
+
if (action && action !== "ping" && action !== "show") {
|
|
16
|
+
context.err(
|
|
17
|
+
`Ação desconhecida em 'status': '${action}'. Use 'status' ou 'status ping'.`,
|
|
18
|
+
);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const engine = await context.engine();
|
|
23
|
+
const result =
|
|
24
|
+
action === "ping"
|
|
25
|
+
? pingStatus(engine.storage, engine.repo, branch)
|
|
26
|
+
: readStatus(engine.storage, engine.repo, branch);
|
|
27
|
+
|
|
28
|
+
if (action === "ping") {
|
|
29
|
+
context.out(
|
|
30
|
+
result.heartbeatAt
|
|
31
|
+
? `Heartbeat atualizado — ${result.branch} · ${result.status ?? "sem status"} · ${result.heartbeatAt}`
|
|
32
|
+
: `Sem execução ativa — ${result.branch} · ${result.status ?? "sem status"}.`,
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
context.out(
|
|
36
|
+
[
|
|
37
|
+
`Branch: ${result.branch}`,
|
|
38
|
+
`Status: ${result.status ?? "—"}`,
|
|
39
|
+
`Heartbeat: ${result.heartbeatAt ?? "—"}`,
|
|
40
|
+
`Atualizado: ${result.updatedAt}`,
|
|
41
|
+
].join("\n"),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/cli/help.ts
CHANGED
|
@@ -6,6 +6,7 @@ Comandos:
|
|
|
6
6
|
--finalize Consolida os findings em review.md
|
|
7
7
|
context Coleta o contexto da PR via gh (Passo 0)
|
|
8
8
|
findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · submit · path)
|
|
9
|
+
status [ping] Mostra o status da run (ping renova o heartbeat da execução)
|
|
9
10
|
server Sobe a UI local de triagem (--port, --host, --open)
|
|
10
11
|
setup Cria a configuração inicial do projeto (config + skills)
|
|
11
12
|
update Sincroniza os arquivos auto-gerados (.houndsman/core)
|
package/src/cli/index.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* --finalize Passo 3 — consolidar os findings em review.md
|
|
16
16
|
* context Passo 0 — coletar o contexto da PR via `gh`
|
|
17
17
|
* findings <sub> Triagem/correção dos findings (list/set/done/sync/batch/verify/submit/...)
|
|
18
|
+
* status [ping] Mostra o status da run; `ping` renova o heartbeat da execução
|
|
18
19
|
* server Sobe a UI local de triagem (Bun.serve)
|
|
19
20
|
* setup Cria a configuração inicial do projeto (config + skills)
|
|
20
21
|
* update Sincroniza os arquivos auto-gerados (.houndsman/core)
|
|
@@ -38,6 +39,7 @@ import { CommandRegistry } from "./commands/registry";
|
|
|
38
39
|
import { ReviewCommand } from "./commands/review-command";
|
|
39
40
|
import { ServerCommand } from "./commands/server-command";
|
|
40
41
|
import { SetupCommand, TestCommand, UpdateCommand } from "./commands/setup-command";
|
|
42
|
+
import { StatusCommand } from "./commands/status-command";
|
|
41
43
|
import { WatchdogCommand } from "./commands/watchdog-command";
|
|
42
44
|
import { HELP } from "./help";
|
|
43
45
|
|
|
@@ -53,6 +55,7 @@ function buildRegistry(): CommandRegistry {
|
|
|
53
55
|
.register(new HelpCommand())
|
|
54
56
|
.register(new ContextCommand())
|
|
55
57
|
.register(new FindingsCommand())
|
|
58
|
+
.register(new StatusCommand())
|
|
56
59
|
.register(new WatchdogCommand())
|
|
57
60
|
.register(new ServerCommand())
|
|
58
61
|
.register(new SetupCommand())
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
type SubmitPreview,
|
|
37
37
|
type SubmitSelection,
|
|
38
38
|
submitFindings,
|
|
39
|
+
syncResolvedThreads,
|
|
39
40
|
} from "../github-review";
|
|
40
41
|
import { VERIFY_FILE } from "../paths";
|
|
41
42
|
import { currentBranch } from "../pr-context";
|
|
@@ -214,6 +215,25 @@ function pingStatus(ctx: CommandContext, state: FindingsStateFile): void {
|
|
|
214
215
|
pingFindingsStatus(ctx.storage, ctx.review.id, state);
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Resolve no GitHub as threads dos findings recém-resolvidos localmente.
|
|
220
|
+
* Fail-open (`gh` ausente/sem PR ⇒ só a mensagem). Devolve a linha para o
|
|
221
|
+
* output ou `null` quando não há vínculo remoto pendente.
|
|
222
|
+
*/
|
|
223
|
+
function syncThreadsOutput(ctx: CommandContext, ids: readonly string[]): string | null {
|
|
224
|
+
if (ids.length === 0) return null;
|
|
225
|
+
const state = loadState(ctx);
|
|
226
|
+
const outcome = syncResolvedThreads({ state, ids });
|
|
227
|
+
if (outcome.checked === 0) return null;
|
|
228
|
+
if (outcome.updated.length > 0) ctx.storage.writeFindings(ctx.review.id, state);
|
|
229
|
+
if (!outcome.available) {
|
|
230
|
+
return `GitHub: threads não sincronizadas (${outcome.reason}).`;
|
|
231
|
+
}
|
|
232
|
+
const failures =
|
|
233
|
+
outcome.failed.length > 0 ? `, ${outcome.failed.length} falha(s)` : "";
|
|
234
|
+
return `GitHub: ${outcome.resolved.length} thread(s) resolvida(s), ${outcome.skipped.length} já resolvida(s)/pulada(s)${failures}.`;
|
|
235
|
+
}
|
|
236
|
+
|
|
217
237
|
/** Aplica uma atualização a um finding e persiste. */
|
|
218
238
|
function updateOne(
|
|
219
239
|
ctx: CommandContext,
|
|
@@ -309,10 +329,17 @@ function execute(
|
|
|
309
329
|
}
|
|
310
330
|
const finding = updateOne(ctx, input, update);
|
|
311
331
|
pingStatus(ctx, loadState(ctx));
|
|
332
|
+
const threads =
|
|
333
|
+
update.execState === "done" ? syncThreadsOutput(ctx, [finding.id]) : null;
|
|
312
334
|
return {
|
|
313
335
|
output: parsed.bools.has("--json")
|
|
314
336
|
? JSON.stringify(finding, null, 2)
|
|
315
|
-
:
|
|
337
|
+
: [
|
|
338
|
+
`Atualizado: \`${finding.id}\` → ${finding.decision}/${finding.execState}.`,
|
|
339
|
+
threads,
|
|
340
|
+
]
|
|
341
|
+
.filter(Boolean)
|
|
342
|
+
.join("\n"),
|
|
316
343
|
exitCode: 0,
|
|
317
344
|
};
|
|
318
345
|
}
|
|
@@ -320,8 +347,17 @@ function execute(
|
|
|
320
347
|
const ids = requireIds(parsed, "done");
|
|
321
348
|
const updated = updateMany(ctx, ids, { execState: "done" });
|
|
322
349
|
pingStatus(ctx, loadState(ctx));
|
|
350
|
+
const threads = syncThreadsOutput(
|
|
351
|
+
ctx,
|
|
352
|
+
updated.map((finding) => finding.id),
|
|
353
|
+
);
|
|
323
354
|
return {
|
|
324
|
-
output:
|
|
355
|
+
output: [
|
|
356
|
+
`Marcados como done: ${updated.map((f) => f.id).join(", ")}.`,
|
|
357
|
+
threads,
|
|
358
|
+
]
|
|
359
|
+
.filter(Boolean)
|
|
360
|
+
.join("\n"),
|
|
325
361
|
exitCode: 0,
|
|
326
362
|
};
|
|
327
363
|
}
|
|
@@ -511,6 +547,9 @@ async function executeVerify(
|
|
|
511
547
|
pingStatus(ctx, current);
|
|
512
548
|
}
|
|
513
549
|
|
|
550
|
+
// Resolvidos localmente ⇒ resolve as conversas correspondentes no PR.
|
|
551
|
+
const threads = syncThreadsOutput(ctx, applied);
|
|
552
|
+
|
|
514
553
|
const output = parsed.bools.has("--json")
|
|
515
554
|
? JSON.stringify(
|
|
516
555
|
{
|
|
@@ -534,6 +573,7 @@ async function executeVerify(
|
|
|
534
573
|
parsed.bools.has("--apply")
|
|
535
574
|
? `Aplicados (done): ${applied.length > 0 ? applied.join(", ") : "—"}.`
|
|
536
575
|
: "",
|
|
576
|
+
threads,
|
|
537
577
|
]
|
|
538
578
|
.filter(Boolean)
|
|
539
579
|
.join("\n");
|
|
@@ -603,6 +643,21 @@ function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
|
|
|
603
643
|
"",
|
|
604
644
|
`Enviados: ${outcome.posted.length} · Falhas: ${outcome.failed.length} · Pulados: ${outcome.skipped.length}`,
|
|
605
645
|
];
|
|
646
|
+
const review = outcome.review;
|
|
647
|
+
if (review?.id !== undefined) {
|
|
648
|
+
const target = review.url ? ` — ${review.url}` : "";
|
|
649
|
+
if (review.created) {
|
|
650
|
+
const status = review.finalized
|
|
651
|
+
? `finalizada (${review.event})`
|
|
652
|
+
: "criada, mas não finalizada";
|
|
653
|
+
lines.push(`Revisão #${review.id} ${status}${target}`);
|
|
654
|
+
} else {
|
|
655
|
+
lines.push(
|
|
656
|
+
`Comentários adicionados à revisão pendente #${review.id}${target}`,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (review?.error) lines.push(`Revisão: ❌ ${review.error}`);
|
|
606
661
|
if (outcome.general) {
|
|
607
662
|
lines.push(
|
|
608
663
|
`Comentário geral: ${outcome.general.url ?? outcome.general.commentId} (${outcome.general.ids.length})`,
|
|
@@ -615,8 +670,9 @@ function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
|
|
|
615
670
|
}
|
|
616
671
|
|
|
617
672
|
/**
|
|
618
|
-
* Subcomando `findings submit` — envia os findings selecionados
|
|
619
|
-
*
|
|
673
|
+
* Subcomando `findings submit` — envia os findings selecionados numa revisão da
|
|
674
|
+
* PR (via `gh`): reaproveita a revisão pendente ou cria/finaliza uma nova.
|
|
675
|
+
* `--dry-run` mostra a prévia sem postar. Assíncrono (gh).
|
|
620
676
|
*/
|
|
621
677
|
export async function runSubmitCommand(
|
|
622
678
|
config: HoundsmanConfig,
|
|
@@ -664,6 +720,6 @@ async function executeSubmit(
|
|
|
664
720
|
|
|
665
721
|
return {
|
|
666
722
|
output: renderSubmitOutcome(outcome, json),
|
|
667
|
-
exitCode: outcome.failed.length > 0 ? 1 : 0,
|
|
723
|
+
exitCode: outcome.failed.length > 0 || Boolean(outcome.review?.error) ? 1 : 0,
|
|
668
724
|
};
|
|
669
725
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status da run (biblioteca) — leitura e heartbeat para a UI.
|
|
3
|
+
*
|
|
4
|
+
* `status ping` é o sinal de vida de uma skill em execução (revisão ou
|
|
5
|
+
* correção). O heartbeat é separado de `updatedAt`: triagem/sync atualizam o
|
|
6
|
+
* segundo, mas não fingem que existe uma execução em andamento.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { currentBranch } from "../pr-context";
|
|
10
|
+
import { type RunStatus, touchRunStatus } from "../run-status";
|
|
11
|
+
import type { RepoIdentity, ReviewRecord, StorageProvider } from "../storage/types";
|
|
12
|
+
|
|
13
|
+
export type StatusResult = {
|
|
14
|
+
branch: string;
|
|
15
|
+
status: RunStatus | null;
|
|
16
|
+
startedAt?: string;
|
|
17
|
+
heartbeatAt?: string;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function toResult(review: ReviewRecord): StatusResult {
|
|
22
|
+
return {
|
|
23
|
+
branch: review.branch,
|
|
24
|
+
status: review.status,
|
|
25
|
+
startedAt: review.startedAt,
|
|
26
|
+
heartbeatAt: review.heartbeatAt,
|
|
27
|
+
updatedAt: review.updatedAt,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Resolve a review da branch (explícita ou a atual do git). */
|
|
32
|
+
function findReview(
|
|
33
|
+
storage: StorageProvider,
|
|
34
|
+
repo: RepoIdentity,
|
|
35
|
+
branch?: string,
|
|
36
|
+
): ReviewRecord {
|
|
37
|
+
const target = branch ?? currentBranch();
|
|
38
|
+
const review = storage.findReview(repo.key, target);
|
|
39
|
+
if (!review) {
|
|
40
|
+
throw new Error(`Nenhuma revisão para '${target}'.`);
|
|
41
|
+
}
|
|
42
|
+
return review;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Lê o status atual da run. */
|
|
46
|
+
export function readStatus(
|
|
47
|
+
storage: StorageProvider,
|
|
48
|
+
repo: RepoIdentity,
|
|
49
|
+
branch?: string,
|
|
50
|
+
): StatusResult {
|
|
51
|
+
return toResult(findReview(storage, repo, branch));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Renova o heartbeat de uma execução ativa (`reviewing`/`fixing`). */
|
|
55
|
+
export function pingStatus(
|
|
56
|
+
storage: StorageProvider,
|
|
57
|
+
repo: RepoIdentity,
|
|
58
|
+
branch?: string,
|
|
59
|
+
now?: string,
|
|
60
|
+
): StatusResult {
|
|
61
|
+
const review = findReview(storage, repo, branch);
|
|
62
|
+
return toResult(touchRunStatus(storage, review.id, now));
|
|
63
|
+
}
|
|
@@ -34,12 +34,21 @@ export type RunClassification = ClassificationResult & {
|
|
|
34
34
|
carryover?: MergedFinding[];
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
/** Resultado da resolução de escopo. */
|
|
38
|
-
export type ReviewScope =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
/** Resultado da resolução de escopo: revisar ou nada a fazer. */
|
|
38
|
+
export type ReviewScope =
|
|
39
|
+
| {
|
|
40
|
+
/** O `HEAD` é o mesmo commit já revisado — não há o que re-revisar. */
|
|
41
|
+
kind: "noop";
|
|
42
|
+
headSha: string;
|
|
43
|
+
/** Commit já coberto pelo review anterior. */
|
|
44
|
+
reviewedFrom: string;
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
kind: "review";
|
|
48
|
+
classification: RunClassification;
|
|
49
|
+
diff: DiffSource;
|
|
50
|
+
reviewedFrom?: string;
|
|
51
|
+
};
|
|
43
52
|
|
|
44
53
|
/** Decide entre review completo e incremental. */
|
|
45
54
|
export class ReviewScopeResolver {
|
|
@@ -52,20 +61,29 @@ export class ReviewScopeResolver {
|
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
/**
|
|
64
|
+
* No-op quando o `HEAD` é exatamente o commit já revisado (sem
|
|
65
|
+
* `--full`/`--clean`): re-revisar a PR inteira seria desperdício, e o
|
|
66
|
+
* relatório anterior continua válido.
|
|
67
|
+
*
|
|
55
68
|
* Incremental só quando há um `reviewed.headSha` anterior, o `HEAD` atual o
|
|
56
69
|
* sucede (sem rebase) e não veio `--full`/`--clean`.
|
|
57
70
|
*/
|
|
58
71
|
resolve(previous: FindingsStateFile | null, headSha: string): ReviewScope {
|
|
59
72
|
const prevSha = previous?.reviewed?.headSha;
|
|
73
|
+
|
|
74
|
+
if (!this.options.full && !this.options.clean && prevSha === headSha) {
|
|
75
|
+
return { kind: "noop", headSha, reviewedFrom: headSha };
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
if (
|
|
61
79
|
this.options.full ||
|
|
62
80
|
this.options.clean ||
|
|
63
81
|
!prevSha ||
|
|
64
|
-
prevSha === headSha ||
|
|
65
82
|
!isAncestor(prevSha, headSha)
|
|
66
83
|
) {
|
|
67
84
|
const diff = collectDiff(this.options.base, this.options.head);
|
|
68
85
|
return {
|
|
86
|
+
kind: "review",
|
|
69
87
|
diff,
|
|
70
88
|
classification: {
|
|
71
89
|
...this.classifier
|
|
@@ -90,6 +108,6 @@ export class ReviewScopeResolver {
|
|
|
90
108
|
reviewedFrom: prevSha,
|
|
91
109
|
carryover: buildCarryover(previous, changedPaths),
|
|
92
110
|
};
|
|
93
|
-
return { classification, diff, reviewedFrom: prevSha };
|
|
111
|
+
return { kind: "review", classification, diff, reviewedFrom: prevSha };
|
|
94
112
|
}
|
|
95
113
|
}
|
|
@@ -76,6 +76,10 @@ export type GitHubLink = {
|
|
|
76
76
|
pr: number;
|
|
77
77
|
/** Quando foi enviado (ISO). */
|
|
78
78
|
at: string;
|
|
79
|
+
/** Node ID da thread de revisão (GraphQL) — base para resolver a conversa. */
|
|
80
|
+
threadId?: string;
|
|
81
|
+
/** Quando a thread foi marcada como resolvida no GitHub (ISO). */
|
|
82
|
+
resolvedAt?: string;
|
|
79
83
|
};
|
|
80
84
|
|
|
81
85
|
/** Marco da revisão que originou o estado: commit revisado + quando. */
|
|
@@ -283,6 +287,12 @@ export type BuildStateOptions = {
|
|
|
283
287
|
reviewed?: FindingsReviewed;
|
|
284
288
|
/** SHA usado para carimbar `foundAtSha` de findings novos/re-reportados. */
|
|
285
289
|
headSha?: string;
|
|
290
|
+
/**
|
|
291
|
+
* Arquivos efetivamente revisados neste ciclo (união de `reviewedFiles`).
|
|
292
|
+
* Quando presente, só findings ausentes desses arquivos viram `done` — o que
|
|
293
|
+
* ficou fora do orçamento/skipped não pode ser dado como resolvido.
|
|
294
|
+
*/
|
|
295
|
+
reviewedPaths?: ReadonlySet<string>;
|
|
286
296
|
now?: string;
|
|
287
297
|
};
|
|
288
298
|
|
|
@@ -303,14 +313,21 @@ export function buildStateFromFindings(
|
|
|
303
313
|
|
|
304
314
|
const result: Record<string, FindingState> = {};
|
|
305
315
|
// 1. Preserva o estado anterior (marca como ausente até provar o contrário).
|
|
316
|
+
const reviewedPaths = options.reviewedPaths;
|
|
306
317
|
for (const prev of Object.values(previous?.findings ?? {})) {
|
|
318
|
+
// Ausente do novo relatório ⇒ corrigido manualmente entre revisões (ou
|
|
319
|
+
// some junto com o trecho): vira `done`. `ignore` é decisão explícita de
|
|
320
|
+
// não corrigir — preserva o estado original. Arquivo fora da revisão
|
|
321
|
+
// (orçamento/skipped) também preserva: ausência não prova resolução.
|
|
322
|
+
const wasReviewed = !reviewedPaths || reviewedPaths.has(prev.file);
|
|
323
|
+
const resolvedByReview =
|
|
324
|
+
wasReviewed && prev.present && prev.decision !== "ignore";
|
|
325
|
+
const queuedAndGone =
|
|
326
|
+
wasReviewed && prev.decision === "execute" && prev.execState === "pending";
|
|
307
327
|
result[prev.id] = {
|
|
308
328
|
...prev,
|
|
309
329
|
present: false,
|
|
310
|
-
execState:
|
|
311
|
-
prev.execState === "pending" && prev.decision === "execute"
|
|
312
|
-
? "done"
|
|
313
|
-
: prev.execState,
|
|
330
|
+
execState: resolvedByReview || queuedAndGone ? "done" : prev.execState,
|
|
314
331
|
updatedAt: now,
|
|
315
332
|
};
|
|
316
333
|
}
|