@payconductor/houndsman 0.1.3 → 0.2.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/package.json +1 -1
- package/src/cli/index.ts +31 -21
- package/src/core/commands/findings.ts +137 -11
- package/src/core/findings-state.ts +19 -0
- package/src/core/github-review.ts +452 -0
- package/src/core/registry-check.ts +53 -0
- package/src/server/client/App.tsx +174 -49
- package/src/server/client/ArtifactsPanel.tsx +2 -2
- package/src/server/client/AutoRefresh.tsx +38 -0
- package/src/server/client/Filters.tsx +72 -53
- package/src/server/client/FindingCard.tsx +212 -71
- package/src/server/client/FindingsList.tsx +8 -6
- package/src/server/client/RunList.tsx +30 -7
- package/src/server/client/SubmitModal.tsx +418 -0
- package/src/server/client/SummaryBar.tsx +65 -12
- package/src/server/client/constants.ts +9 -12
- package/src/server/client/format.ts +40 -0
- package/src/server/client/icons.tsx +143 -0
- package/src/server/client/types.ts +69 -0
- package/src/server/index.ts +352 -88
- package/src/skill/AUDIT-PROTOCOL.md +5 -0
- package/src/skill/README.md +3 -1
- package/src/skill/SETUP.md +0 -1
- package/src/skill/hunt.md +2 -1
- package/src/skill/train.md +29 -8
- package/src/tests/__snapshots__/baseline.test.ts.snap +0 -0
- package/src/tests/baseline.test.ts +493 -0
- package/src/tests/cli.test.ts +167 -0
- package/src/tests/github-review.test.ts +320 -0
- package/src/tests/review-scripts.test.ts +36 -0
- package/src/tests/server.test.ts +117 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payconductor/houndsman",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
package/src/cli/index.ts
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
* cwd) e orquestra funções/classes de `core/`. Não importa nenhum caminho do
|
|
8
8
|
* projeto consumidor estaticamente.
|
|
9
9
|
*
|
|
10
|
+
* Cada comando carrega seus módulos por import dinâmico (lazy) — o startup não
|
|
11
|
+
* paga por `server`, Jev (axios) ou `orchestrator` que o comando não usa.
|
|
12
|
+
*
|
|
10
13
|
* Comandos:
|
|
11
14
|
* (default) Passo 1 — preparar (classificação + briefs + manifesto)
|
|
12
15
|
* --finalize Passo 3 — consolidar os findings em review.md
|
|
13
16
|
* context Passo 0 — coletar o contexto da PR via `gh`
|
|
14
|
-
* findings <sub> Triagem/correção dos findings (list/set/done/sync/batch/verify/...)
|
|
17
|
+
* findings <sub> Triagem/correção dos findings (list/set/done/sync/batch/verify/submit/...)
|
|
15
18
|
* server Sobe a UI local de triagem (Bun.serve)
|
|
16
19
|
* setup Cria a configuração inicial do projeto (config + skills)
|
|
17
20
|
* update Sincroniza os arquivos auto-gerados (.houndsman/core)
|
|
@@ -23,20 +26,9 @@
|
|
|
23
26
|
* `--config <path>` · `--clean` · `--full` · `--no-jev` · `--fail-on-blocking`.
|
|
24
27
|
*/
|
|
25
28
|
|
|
26
|
-
import { runContextCommand } from "../core/commands/context";
|
|
27
|
-
import { runFindingsCommand, runVerifyCommand } from "../core/commands/findings";
|
|
28
|
-
import { parseReviewArgs } from "../core/commands/review";
|
|
29
|
-
import { runSetupCommand } from "../core/commands/setup";
|
|
30
|
-
import { runLibTests } from "../core/commands/test";
|
|
31
|
-
import { runUpdateCommand } from "../core/commands/update";
|
|
32
|
-
import { renderWatchdogContext } from "../core/commands/watchdog";
|
|
33
29
|
import { type HoundsmanConfig, resolveArtifactDir } from "../core/config";
|
|
34
30
|
import { loadConfig } from "../core/config-loader";
|
|
35
|
-
import { JevClient } from "../core/jev";
|
|
36
|
-
import { finalize, prepare, resolveBranch } from "../core/orchestrator";
|
|
37
31
|
import { relFromRoot } from "../core/paths";
|
|
38
|
-
import { checkRegistry } from "../core/registry-check";
|
|
39
|
-
import { startServer } from "../server";
|
|
40
32
|
|
|
41
33
|
const HELP = `Uso: houndsman [comando] [flags]
|
|
42
34
|
|
|
@@ -44,7 +36,7 @@ Comandos:
|
|
|
44
36
|
(nenhum) Prepara a revisão (classification + briefs + manifesto)
|
|
45
37
|
--finalize Consolida os findings em review.md
|
|
46
38
|
context Coleta o contexto da PR via gh (Passo 0)
|
|
47
|
-
findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · path)
|
|
39
|
+
findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · submit · path)
|
|
48
40
|
server Sobe a UI local de triagem (--port, --host, --open)
|
|
49
41
|
setup Cria a configuração inicial do projeto (config + skills)
|
|
50
42
|
update Sincroniza os arquivos auto-gerados (.houndsman/core)
|
|
@@ -77,7 +69,9 @@ Flags de findings:
|
|
|
77
69
|
--offset <n> Paginação dentro do grupo (batch)
|
|
78
70
|
--include-done Inclui já concluídos no batch
|
|
79
71
|
--apply (verify) Marca os resolvidos como done
|
|
80
|
-
--fail-on-persisting (verify) Exit 1 se algum blocking persistir
|
|
72
|
+
--fail-on-persisting (verify) Exit 1 se algum blocking persistir
|
|
73
|
+
--include-sent (submit) Reenvia findings já enviados à PR
|
|
74
|
+
--dry-run (submit) Só a prévia do envio, sem postar`;
|
|
81
75
|
|
|
82
76
|
function readFlag(args: string[], flag: string): string | undefined {
|
|
83
77
|
const index = args.indexOf(flag);
|
|
@@ -85,7 +79,8 @@ function readFlag(args: string[], flag: string): string | undefined {
|
|
|
85
79
|
}
|
|
86
80
|
|
|
87
81
|
/** Passo 0 — contexto da PR via `gh`. */
|
|
88
|
-
function runContext(config: HoundsmanConfig, args: string[]): void {
|
|
82
|
+
async function runContext(config: HoundsmanConfig, args: string[]): Promise<void> {
|
|
83
|
+
const { runContextCommand } = await import("../core/commands/context");
|
|
89
84
|
const { paths, context } = runContextCommand(config, {
|
|
90
85
|
branch: readFlag(args, "--branch"),
|
|
91
86
|
dir: readFlag(args, "--dir"),
|
|
@@ -114,6 +109,7 @@ function runContext(config: HoundsmanConfig, args: string[]): void {
|
|
|
114
109
|
|
|
115
110
|
/** Valida a configuração do projeto e o ambiente. */
|
|
116
111
|
async function runDoctor(config: HoundsmanConfig): Promise<void> {
|
|
112
|
+
const { checkRegistry } = await import("../core/registry-check");
|
|
117
113
|
console.log("→ Validando configuração do projeto...");
|
|
118
114
|
const { problems, warnings, jevQuestions } = checkRegistry(config);
|
|
119
115
|
|
|
@@ -142,6 +138,7 @@ async function runDoctor(config: HoundsmanConfig): Promise<void> {
|
|
|
142
138
|
* Fail-open — nunca altera o exit code (`doctor` é diagnóstico).
|
|
143
139
|
*/
|
|
144
140
|
async function reportJevCredential(): Promise<void> {
|
|
141
|
+
const { JevClient } = await import("../core/jev");
|
|
145
142
|
const client = new JevClient();
|
|
146
143
|
if (!client.isConfigured()) {
|
|
147
144
|
console.log(" ⚠️ TYPESAFE_API_KEY (opcional — pré-análise Jev)");
|
|
@@ -158,7 +155,8 @@ async function reportJevCredential(): Promise<void> {
|
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
/** Contexto de um watchdog. */
|
|
161
|
-
function runWatchdog(config: HoundsmanConfig, args: string[]): void {
|
|
158
|
+
async function runWatchdog(config: HoundsmanConfig, args: string[]): Promise<void> {
|
|
159
|
+
const { renderWatchdogContext } = await import("../core/commands/watchdog");
|
|
162
160
|
const id = args.find((arg) => !arg.startsWith("-"));
|
|
163
161
|
|
|
164
162
|
if (!id || renderWatchdogContext(config, id) === null) {
|
|
@@ -185,11 +183,16 @@ function runWatchdog(config: HoundsmanConfig, args: string[]): void {
|
|
|
185
183
|
* o JSON. O `verify` é assíncrono (consulta opcional ao Jev) — roteado à parte.
|
|
186
184
|
*/
|
|
187
185
|
async function runFindings(config: HoundsmanConfig, args: string[]): Promise<void> {
|
|
186
|
+
const { runFindingsCommand, runSubmitCommand, runVerifyCommand } = await import(
|
|
187
|
+
"../core/commands/findings"
|
|
188
|
+
);
|
|
188
189
|
const sub = args.find((arg) => !arg.startsWith("-"));
|
|
189
190
|
const result =
|
|
190
191
|
sub === "verify"
|
|
191
192
|
? await runVerifyCommand(config, args)
|
|
192
|
-
:
|
|
193
|
+
: sub === "submit"
|
|
194
|
+
? await runSubmitCommand(config, args)
|
|
195
|
+
: runFindingsCommand(config, args);
|
|
193
196
|
if (result.exitCode !== 0) {
|
|
194
197
|
console.error(result.output);
|
|
195
198
|
process.exit(result.exitCode);
|
|
@@ -213,7 +216,8 @@ function openBrowser(url: string): void {
|
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
/** Subcomando `server` — sobe a UI local de triagem. */
|
|
216
|
-
function runServer(config: HoundsmanConfig, args: string[]): void {
|
|
219
|
+
async function runServer(config: HoundsmanConfig, args: string[]): Promise<void> {
|
|
220
|
+
const { startServer } = await import("../server");
|
|
217
221
|
const port = Number(readFlag(args, "--port")) || 7331;
|
|
218
222
|
const host = readFlag(args, "--host") ?? "127.0.0.1";
|
|
219
223
|
const { url } = startServer({ config, port, host });
|
|
@@ -224,6 +228,9 @@ function runServer(config: HoundsmanConfig, args: string[]): void {
|
|
|
224
228
|
|
|
225
229
|
/** Passo 1/3 — preparar ou finalizar. */
|
|
226
230
|
async function runReview(config: HoundsmanConfig, args: string[]): Promise<void> {
|
|
231
|
+
const [{ parseReviewArgs }, { finalize, prepare, resolveBranch }] = await Promise.all(
|
|
232
|
+
[import("../core/commands/review"), import("../core/orchestrator")],
|
|
233
|
+
);
|
|
227
234
|
const opts = parseReviewArgs(args, resolveArtifactDir(config));
|
|
228
235
|
const branch = resolveBranch(opts);
|
|
229
236
|
|
|
@@ -246,13 +253,16 @@ export async function main(): Promise<void> {
|
|
|
246
253
|
const rest = command ? argv.slice(1) : argv;
|
|
247
254
|
|
|
248
255
|
if (command === "test") {
|
|
256
|
+
const { runLibTests } = await import("../core/commands/test");
|
|
249
257
|
process.exit(runLibTests());
|
|
250
258
|
}
|
|
251
259
|
if (command === "setup") {
|
|
260
|
+
const { runSetupCommand } = await import("../core/commands/setup");
|
|
252
261
|
await runSetupCommand({ cwd: process.cwd(), args: rest });
|
|
253
262
|
return;
|
|
254
263
|
}
|
|
255
264
|
if (command === "update") {
|
|
265
|
+
const { runUpdateCommand } = await import("../core/commands/update");
|
|
256
266
|
await runUpdateCommand({ cwd: process.cwd(), args: rest });
|
|
257
267
|
return;
|
|
258
268
|
}
|
|
@@ -268,20 +278,20 @@ export async function main(): Promise<void> {
|
|
|
268
278
|
|
|
269
279
|
switch (command) {
|
|
270
280
|
case "context":
|
|
271
|
-
runContext(config, rest);
|
|
281
|
+
await runContext(config, rest);
|
|
272
282
|
return;
|
|
273
283
|
case "doctor":
|
|
274
284
|
case "check":
|
|
275
285
|
await runDoctor(config);
|
|
276
286
|
return;
|
|
277
287
|
case "watchdog":
|
|
278
|
-
runWatchdog(config, rest);
|
|
288
|
+
await runWatchdog(config, rest);
|
|
279
289
|
return;
|
|
280
290
|
case "findings":
|
|
281
291
|
await runFindings(config, rest);
|
|
282
292
|
return;
|
|
283
293
|
case "server":
|
|
284
|
-
runServer(config, rest);
|
|
294
|
+
await runServer(config, rest);
|
|
285
295
|
return;
|
|
286
296
|
case undefined:
|
|
287
297
|
await runReview(config, rest);
|
|
@@ -36,21 +36,22 @@ import {
|
|
|
36
36
|
syncState,
|
|
37
37
|
} from "../findings-state";
|
|
38
38
|
import { buildFixBatch, type FixBatchResult } from "../fix-batch";
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
buildSubmitPreview,
|
|
41
|
+
defaultGhRunner,
|
|
42
|
+
resolvePrTarget,
|
|
43
|
+
type SubmitOutcome,
|
|
44
|
+
type SubmitPreview,
|
|
45
|
+
type SubmitSelection,
|
|
46
|
+
submitFindings,
|
|
47
|
+
} from "../github-review";
|
|
40
48
|
import { relFromRoot, VERIFY_FILE } from "../paths";
|
|
41
49
|
import { currentBranch } from "../pr-context";
|
|
42
50
|
import { pingFindingsStatus } from "../run-status";
|
|
43
51
|
import { SEVERITY_EMOJI, SEVERITY_ORDER, type Severity } from "../types";
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
selectVerifiable,
|
|
48
|
-
summarizeVerify,
|
|
49
|
-
type VerifyEntry,
|
|
50
|
-
type VerifyStatus,
|
|
51
|
-
type VerifySummary,
|
|
52
|
-
verifyLocal,
|
|
53
|
-
} from "../verify";
|
|
52
|
+
// Tipos são apagados no transpile (custo zero). Os **valores** de `../verify` e de
|
|
53
|
+
// `../jev` (axios) entram por import dinâmico — só o subcomando `verify` os usa.
|
|
54
|
+
import type { VerifyEntry, VerifyStatus, VerifySummary } from "../verify";
|
|
54
55
|
|
|
55
56
|
export type FindingsCommandResult = {
|
|
56
57
|
output: string;
|
|
@@ -543,6 +544,12 @@ async function executeVerify(
|
|
|
543
544
|
const store = FindingsStateStore.at(dir, branch);
|
|
544
545
|
const state = loadState(config, parsed);
|
|
545
546
|
|
|
547
|
+
// Lazy: `../verify` (leve) e `../jev` (axios) só entram no caminho do verify.
|
|
548
|
+
const [
|
|
549
|
+
{ selectVerifiable, verifyLocal, summarizeVerify, applyJev, applyResolutions },
|
|
550
|
+
{ JevCandidatesStore, JevClient, JevVerifier },
|
|
551
|
+
] = await Promise.all([import("../verify"), import("../jev")]);
|
|
552
|
+
|
|
546
553
|
const findings = selectVerifiable(state, filterFrom(parsed));
|
|
547
554
|
const entries = verifyLocal(findings, fileReader(process.cwd()));
|
|
548
555
|
|
|
@@ -616,3 +623,122 @@ async function executeVerify(
|
|
|
616
623
|
|
|
617
624
|
return { output, exitCode: failOnPersisting ? 1 : 0 };
|
|
618
625
|
}
|
|
626
|
+
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
// Envio (findings submit)
|
|
629
|
+
// ---------------------------------------------------------------------------
|
|
630
|
+
|
|
631
|
+
/** Flag de lista `a,b,c` → itens não vazios. */
|
|
632
|
+
function splitList(raw: string | undefined): string[] {
|
|
633
|
+
return (raw ?? "")
|
|
634
|
+
.split(",")
|
|
635
|
+
.map((value) => value.trim())
|
|
636
|
+
.filter(Boolean);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Seleção do envio a partir das flags (`--severity`, `--exec`, `--include-sent`). */
|
|
640
|
+
function submitSelection(parsed: ParsedArgs): SubmitSelection {
|
|
641
|
+
const severities = splitList(parsed.values["--severity"]).filter(
|
|
642
|
+
(value): value is Severity =>
|
|
643
|
+
(SEVERITY_ORDER as readonly string[]).includes(value),
|
|
644
|
+
);
|
|
645
|
+
const execStates = splitList(parsed.values["--exec"]).filter(
|
|
646
|
+
(value): value is FindingExecState =>
|
|
647
|
+
(FINDING_EXEC_STATES as readonly string[]).includes(value),
|
|
648
|
+
);
|
|
649
|
+
return {
|
|
650
|
+
severities: severities.length > 0 ? severities : undefined,
|
|
651
|
+
execStates: execStates.length > 0 ? execStates : undefined,
|
|
652
|
+
includeSent: parsed.bools.has("--include-sent"),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Render da prévia do envio (markdown ou JSON). */
|
|
657
|
+
function renderSubmitPreview(preview: SubmitPreview, json: boolean): string {
|
|
658
|
+
if (json) return JSON.stringify(preview, null, 2);
|
|
659
|
+
const totals = preview.totals;
|
|
660
|
+
if (!preview.available) {
|
|
661
|
+
return [
|
|
662
|
+
`PR indisponível para envio: ${preview.reason}`,
|
|
663
|
+
`Prévia local: ${totals.candidates} candidato(s) — ${totals.line} na linha · ${totals.file} no arquivo · ${totals.sent} já enviado(s).`,
|
|
664
|
+
].join("\n");
|
|
665
|
+
}
|
|
666
|
+
const lines = [
|
|
667
|
+
`# Envio da revisão — PR #${preview.target?.number} (${preview.target?.repo})`,
|
|
668
|
+
"",
|
|
669
|
+
`A enviar: ${totals.toSend} (${totals.line} na linha · ${totals.file} no arquivo) · já enviados: ${totals.sent}`,
|
|
670
|
+
];
|
|
671
|
+
if (totals.toSend === 0) lines.push("", "Nada a enviar.");
|
|
672
|
+
return lines.join("\n");
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Render do resultado do envio (markdown ou JSON). */
|
|
676
|
+
function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
|
|
677
|
+
if (json) return JSON.stringify(outcome, null, 2);
|
|
678
|
+
const lines = [
|
|
679
|
+
"# Envio da revisão",
|
|
680
|
+
"",
|
|
681
|
+
`Enviados: ${outcome.posted.length} · Falhas: ${outcome.failed.length} · Pulados: ${outcome.skipped.length}`,
|
|
682
|
+
];
|
|
683
|
+
if (outcome.general) {
|
|
684
|
+
lines.push(
|
|
685
|
+
`Comentário geral: ${outcome.general.url ?? outcome.general.commentId} (${outcome.general.ids.length})`,
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
for (const failure of outcome.failed) {
|
|
689
|
+
lines.push(`- ❌ \`${failure.id}\`: ${failure.reason}`);
|
|
690
|
+
}
|
|
691
|
+
return lines.join("\n");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Subcomando `findings submit` — envia os findings selecionados como comentários
|
|
696
|
+
* na PR (via `gh`). `--dry-run` mostra a prévia sem postar. Assíncrono (gh).
|
|
697
|
+
*/
|
|
698
|
+
export async function runSubmitCommand(
|
|
699
|
+
config: HoundsmanConfig,
|
|
700
|
+
args: string[],
|
|
701
|
+
): Promise<FindingsCommandResult> {
|
|
702
|
+
const parsed = parseArgs(args);
|
|
703
|
+
try {
|
|
704
|
+
return await executeSubmit(config, parsed);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
return {
|
|
707
|
+
output: error instanceof Error ? error.message : String(error),
|
|
708
|
+
exitCode: 1,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function executeSubmit(
|
|
714
|
+
config: HoundsmanConfig,
|
|
715
|
+
parsed: ParsedArgs,
|
|
716
|
+
): Promise<FindingsCommandResult> {
|
|
717
|
+
const state = loadState(config, parsed);
|
|
718
|
+
const selection = submitSelection(parsed);
|
|
719
|
+
const target = resolvePrTarget(defaultGhRunner, state.branch);
|
|
720
|
+
const preview = buildSubmitPreview(state, target, selection);
|
|
721
|
+
const json = parsed.bools.has("--json");
|
|
722
|
+
|
|
723
|
+
// Prévia explícita, ou alvo indisponível (fail-open) — nada a postar.
|
|
724
|
+
if (parsed.bools.has("--dry-run") || !target.ok) {
|
|
725
|
+
return { output: renderSubmitPreview(preview, json), exitCode: 0 };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const outcome = submitFindings({
|
|
729
|
+
state,
|
|
730
|
+
target: target.target,
|
|
731
|
+
runner: defaultGhRunner,
|
|
732
|
+
selection,
|
|
733
|
+
});
|
|
734
|
+
FindingsStateStore.at(
|
|
735
|
+
parsed.values["--dir"] ?? resolveArtifactDir(config),
|
|
736
|
+
state.branch,
|
|
737
|
+
).write(state);
|
|
738
|
+
pingStatus(config, parsed);
|
|
739
|
+
|
|
740
|
+
return {
|
|
741
|
+
output: renderSubmitOutcome(outcome, json),
|
|
742
|
+
exitCode: outcome.failed.length > 0 ? 1 : 0,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
@@ -67,6 +67,22 @@ export type FindingHistoryEntry = {
|
|
|
67
67
|
by?: string;
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Vínculo com o comentário remoto no GitHub (envio da revisão). Gravado pelo
|
|
72
|
+
* `findings submit` / UI — permite pular reenvios e abrir o comentário.
|
|
73
|
+
*/
|
|
74
|
+
export type GitHubLink = {
|
|
75
|
+
/** Id do comentário (review comment inline ou issue comment geral). */
|
|
76
|
+
commentId: number;
|
|
77
|
+
url?: string;
|
|
78
|
+
/** Origem do comentário remoto: linha, arquivo ou comentário geral. */
|
|
79
|
+
kind: "line" | "file" | "comment";
|
|
80
|
+
/** Número da PR de destino. */
|
|
81
|
+
pr: number;
|
|
82
|
+
/** Quando foi enviado (ISO). */
|
|
83
|
+
at: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
70
86
|
/** Marco da revisão que originou o estado: commit revisado + quando. */
|
|
71
87
|
export type FindingsReviewed = {
|
|
72
88
|
/** SHA do commit revisado (head do diff no `--finalize`). */
|
|
@@ -99,6 +115,8 @@ export type FindingState = {
|
|
|
99
115
|
createdAt: string;
|
|
100
116
|
updatedAt: string;
|
|
101
117
|
history: FindingHistoryEntry[];
|
|
118
|
+
/** Vínculo com o comentário remoto no GitHub (envio da revisão). */
|
|
119
|
+
github?: GitHubLink;
|
|
102
120
|
};
|
|
103
121
|
|
|
104
122
|
/** Conteúdo do `state.json`. */
|
|
@@ -345,6 +363,7 @@ export function buildStateFromFindings(
|
|
|
345
363
|
createdAt: prev?.createdAt ?? now,
|
|
346
364
|
updatedAt: now,
|
|
347
365
|
history: prev?.history ?? [],
|
|
366
|
+
github: prev?.github,
|
|
348
367
|
};
|
|
349
368
|
byFingerprint.set(fingerprint, created);
|
|
350
369
|
result[created.id] = created;
|