@payconductor/houndsman 0.3.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 +25 -39
- package/package.json +1 -1
- package/src/cli/commands/context-command.ts +11 -9
- package/src/cli/commands/doctor-command.ts +5 -0
- package/src/cli/commands/findings-command.ts +13 -3
- package/src/cli/commands/review-command.ts +1 -2
- package/src/cli/commands/status-command.ts +46 -0
- package/src/cli/help.ts +8 -3
- package/src/cli/index.ts +5 -2
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +216 -125
- package/src/core/commands/review.ts +7 -8
- package/src/core/commands/scaffold.ts +15 -16
- package/src/core/commands/status.ts +63 -0
- package/src/core/config.ts +16 -7
- package/src/core/engine/houndsman-engine.ts +15 -1
- package/src/core/engine/review-scope.ts +26 -8
- package/src/core/engine/review-session.ts +37 -9
- package/src/core/findings-state.ts +53 -75
- package/src/core/github-review.ts +620 -57
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +271 -134
- package/src/core/paths.ts +26 -17
- package/src/core/pr-context.ts +19 -5
- package/src/core/project-config.ts +4 -5
- package/src/core/renderers/manifest-writer.ts +29 -18
- package/src/core/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/run-layout.ts +4 -104
- package/src/core/run-status.ts +59 -50
- package/src/core/services/git-repository.ts +16 -0
- package/src/core/services/project-files.ts +13 -1
- package/src/core/storage/factory.ts +24 -0
- package/src/core/storage/file/file-storage.ts +271 -0
- package/src/core/storage/paths.ts +64 -0
- package/src/core/storage/repo-identity.ts +70 -0
- package/src/core/storage/sqlite/mappers.ts +162 -0
- package/src/core/storage/sqlite/schema.ts +116 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +431 -0
- package/src/core/storage/types.ts +152 -0
- package/src/core/storage/work-area.ts +191 -0
- package/src/index.ts +27 -6
- package/src/server/artifacts.ts +8 -61
- package/src/server/client/App.tsx +26 -9
- package/src/server/client/ArtifactsPanel.tsx +144 -23
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +50 -15
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +181 -0
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +58 -10
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/utils/code-highlight.ts +422 -0
- package/src/server/client/utils/constants.ts +226 -0
- package/src/server/client/utils/json-highlight.ts +55 -0
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +81 -0
- package/src/server/index.ts +154 -16
- package/src/server/run-repository.ts +153 -136
- package/src/skill/OUTPUT-FORMAT.md +6 -4
- package/src/skill/README.md +22 -14
- package/src/skill/SETUP.md +4 -4
- package/src/skill/example-review.md +22 -14
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +55 -32
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +7 -6
- package/src/tests/__snapshots__/baseline.test.ts.snap +74 -61
- package/src/tests/baseline.test.ts +79 -34
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +1 -1
- package/src/tests/findings-commands.test.ts +153 -131
- package/src/tests/findings-state.test.ts +56 -5
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/infrastructure.test.ts +33 -38
- package/src/tests/markdown.test.ts +149 -0
- 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 +81 -5
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +86 -23
- package/src/tests/server.test.ts +184 -96
- package/src/tests/storage.test.ts +449 -0
- package/src/server/client/constants.ts +0 -91
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* **do usuário**:
|
|
6
6
|
*
|
|
7
7
|
* .houndsman/
|
|
8
|
-
* ├── .gitignore AUTO — ignora core/
|
|
8
|
+
* ├── .gitignore AUTO — ignora core/ (gerado)
|
|
9
9
|
* ├── core/types.ts AUTO — snapshot dos tipos + helpers
|
|
10
10
|
* ├── core/skill/*.md AUTO — docs do skill (com {{projectName}} resolvido)
|
|
11
11
|
* ├── watchdogs/*.ts USUÁRIO
|
|
@@ -16,13 +16,7 @@
|
|
|
16
16
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { basename, join } from "node:path";
|
|
18
18
|
import { SKILL_DIR, SKILL_FILE } from "../assets";
|
|
19
|
-
import {
|
|
20
|
-
ARTIFACT_DIR,
|
|
21
|
-
CORE_DIR,
|
|
22
|
-
CORE_SKILL_DIR,
|
|
23
|
-
OUTPUT_FORMAT_PATH,
|
|
24
|
-
PROJECT_DIR,
|
|
25
|
-
} from "../paths";
|
|
19
|
+
import { CORE_DIR, CORE_SKILL_DIR, OUTPUT_FORMAT_PATH, PROJECT_DIR } from "../paths";
|
|
26
20
|
|
|
27
21
|
export type Harness = "agents" | "claude";
|
|
28
22
|
|
|
@@ -87,9 +81,9 @@ import type { HoundsmanConfig } from "./${CORE_DIR}/types";
|
|
|
87
81
|
/**
|
|
88
82
|
* Configuração do houndsman para ${projectName}.
|
|
89
83
|
*
|
|
90
|
-
*
|
|
91
|
-
* output em ${OUTPUT_FORMAT_PATH}. Ajuste
|
|
92
|
-
* docs conforme o projeto.
|
|
84
|
+
* Runs, findings e artefatos vivem no storage global (SQLite por default) —
|
|
85
|
+
* fora do projeto. O schema de output fica em ${OUTPUT_FORMAT_PATH}. Ajuste
|
|
86
|
+
* watchdogs, exclusões e mapeamentos de docs conforme o projeto.
|
|
93
87
|
*/
|
|
94
88
|
export const houndsmanConfig = {
|
|
95
89
|
projectName: "${projectName}",
|
|
@@ -182,10 +176,16 @@ export type HoundsmanConcurrency = {
|
|
|
182
176
|
jev?: number;
|
|
183
177
|
};
|
|
184
178
|
|
|
179
|
+
/** Backend de persistência (reviews, findings e artefatos). */
|
|
180
|
+
export type HoundsmanStorage = {
|
|
181
|
+
provider?: "sqlite" | "file";
|
|
182
|
+
path?: string;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
185
|
/** Contrato de configuração do motor. */
|
|
186
186
|
export type HoundsmanConfig<TId extends string = string> = {
|
|
187
187
|
projectName: string;
|
|
188
|
-
|
|
188
|
+
storage?: HoundsmanStorage;
|
|
189
189
|
watchdogIds?: readonly TId[];
|
|
190
190
|
watchdogs: Record<TId, WatchdogDefinition>;
|
|
191
191
|
exclusions: readonly RegExp[];
|
|
@@ -267,10 +267,9 @@ export function isExcluded(filePath: string): boolean {
|
|
|
267
267
|
const DOC_MAPPINGS_TEMPLATE = `/**
|
|
268
268
|
* Mapeamento de padrões nos paths → documentação relevante.
|
|
269
269
|
* Consumido pelo motor via \`HoundsmanConfig.docMappings\`.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
];
|
|
270
|
+
*/\`core/\` é regenerado por \`houndsman update\`.
|
|
271
|
+
# Runs, findings e artefatos vivem no storage global (fora do projeto).
|
|
272
|
+
core
|
|
274
273
|
`;
|
|
275
274
|
|
|
276
275
|
const PROJECT_GITIGNORE = `# Diretórios gerados pelo houndsman — \`core/\` é regenerado por \`houndsman update\`
|
|
@@ -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
|
+
}
|
package/src/core/config.ts
CHANGED
|
@@ -27,6 +27,20 @@ export type HoundsmanConcurrency = {
|
|
|
27
27
|
jev?: number;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
/** Backend de persistência (reviews, findings e artefatos). */
|
|
31
|
+
export type HoundsmanStorage = {
|
|
32
|
+
/**
|
|
33
|
+
* Provider de storage (default: `sqlite`). `file` mantém o formato antigo
|
|
34
|
+
* em arquivos, mas sob o diretório global do houndsman.
|
|
35
|
+
*/
|
|
36
|
+
provider?: "sqlite" | "file";
|
|
37
|
+
/**
|
|
38
|
+
* Caminho do banco (sqlite) ou raiz das runs (file). Default:
|
|
39
|
+
* `<HOUNDSMAN_HOME>/houndsman.db` / `<HOUNDSMAN_HOME>/runs`.
|
|
40
|
+
*/
|
|
41
|
+
path?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
30
44
|
/**
|
|
31
45
|
* Configuração completa do motor de revisão para um projeto.
|
|
32
46
|
*
|
|
@@ -35,8 +49,8 @@ export type HoundsmanConcurrency = {
|
|
|
35
49
|
export type HoundsmanConfig<TId extends string = string> = {
|
|
36
50
|
/** Nome legível do projeto (usado em prompts e docs). */
|
|
37
51
|
projectName: string;
|
|
38
|
-
/**
|
|
39
|
-
|
|
52
|
+
/** Persistência das runs e artefatos (default: SQLite global). */
|
|
53
|
+
storage?: HoundsmanStorage;
|
|
40
54
|
/**
|
|
41
55
|
* Ids dos watchdogs — **derivados** das chaves de `watchdogs` por
|
|
42
56
|
* `defineHoundsmanConfig` (não informar manualmente).
|
|
@@ -72,11 +86,6 @@ export function defineHoundsmanConfig<TId extends string>(
|
|
|
72
86
|
return { ...input, watchdogIds: Object.keys(input.watchdogs) as TId[] };
|
|
73
87
|
}
|
|
74
88
|
|
|
75
|
-
/** Diretório dos artefatos — o valor do projeto ou o default da camada. */
|
|
76
|
-
export function resolveArtifactDir(config: HoundsmanConfig): string {
|
|
77
|
-
return new ProjectConfig(config).artifactDir;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
89
|
/**
|
|
81
90
|
* Teto de arquivos por brief. Valor ausente/não-positivo cai no default —
|
|
82
91
|
* mantém o contexto do subagente limitado em PRs grandes.
|
|
@@ -13,6 +13,9 @@ import { FileClassifier } from "../services/file-classifier";
|
|
|
13
13
|
import { FixBatchBuilder } from "../services/fix-batch-builder";
|
|
14
14
|
import { GitRepository } from "../services/git-repository";
|
|
15
15
|
import { Verifier } from "../services/verifier";
|
|
16
|
+
import { createStorage } from "../storage/factory";
|
|
17
|
+
import { resolveRepoIdentity } from "../storage/repo-identity";
|
|
18
|
+
import type { RepoIdentity, StorageProvider } from "../storage/types";
|
|
16
19
|
import { ReviewSession } from "./review-session";
|
|
17
20
|
|
|
18
21
|
/** Colaboradores injetáveis (testes/CLI). */
|
|
@@ -21,6 +24,8 @@ export type EngineDeps = {
|
|
|
21
24
|
classifier?: FileClassifier;
|
|
22
25
|
verifier?: Verifier;
|
|
23
26
|
fixBatch?: FixBatchBuilder;
|
|
27
|
+
storage?: StorageProvider;
|
|
28
|
+
repo?: RepoIdentity;
|
|
24
29
|
};
|
|
25
30
|
|
|
26
31
|
/** Composition root do motor. */
|
|
@@ -30,6 +35,8 @@ export class HoundsmanEngine {
|
|
|
30
35
|
readonly classifier: FileClassifier;
|
|
31
36
|
readonly verifier: Verifier;
|
|
32
37
|
readonly fixBatch: FixBatchBuilder;
|
|
38
|
+
readonly storage: StorageProvider;
|
|
39
|
+
readonly repo: RepoIdentity;
|
|
33
40
|
|
|
34
41
|
constructor(raw: HoundsmanConfig, deps: EngineDeps = {}) {
|
|
35
42
|
this.project = new ProjectConfig(raw);
|
|
@@ -37,6 +44,8 @@ export class HoundsmanEngine {
|
|
|
37
44
|
this.classifier = deps.classifier ?? new FileClassifier(raw);
|
|
38
45
|
this.verifier = deps.verifier ?? new Verifier();
|
|
39
46
|
this.fixBatch = deps.fixBatch ?? new FixBatchBuilder();
|
|
47
|
+
this.storage = deps.storage ?? createStorage(raw);
|
|
48
|
+
this.repo = deps.repo ?? resolveRepoIdentity(this.git);
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
/** Configuração crua (contrato do consumidor). */
|
|
@@ -46,6 +55,11 @@ export class HoundsmanEngine {
|
|
|
46
55
|
|
|
47
56
|
/** Abre uma sessão de revisão (prepare/finalize). */
|
|
48
57
|
session(options: ReviewOptions): ReviewSession {
|
|
49
|
-
return ReviewSession.from(this.config, options);
|
|
58
|
+
return ReviewSession.from(this.config, options, this.storage, this.repo);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Libera o provider (fecha o SQLite e faz checkpoint do WAL). */
|
|
62
|
+
close(): void {
|
|
63
|
+
this.storage.close();
|
|
50
64
|
}
|
|
51
65
|
}
|
|
@@ -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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sessão de uma run (engine) — amarra opções, branch e
|
|
2
|
+
* Sessão de uma run (engine) — amarra opções, branch, storage e staging.
|
|
3
3
|
*
|
|
4
4
|
* É o ponto de entrada do `prepare`/`finalize` para o CLI e para o engine.
|
|
5
5
|
*/
|
|
@@ -10,38 +10,66 @@ import {
|
|
|
10
10
|
finalize,
|
|
11
11
|
type PrepareResult,
|
|
12
12
|
prepare,
|
|
13
|
+
type ReviewContext,
|
|
13
14
|
type ReviewOptions,
|
|
14
15
|
resolveBranch,
|
|
16
|
+
reviewContext,
|
|
15
17
|
} from "../orchestrator";
|
|
16
|
-
import {
|
|
18
|
+
import { sanitizeBranch } from "../report-builder";
|
|
19
|
+
import type { RepoIdentity, StorageProvider } from "../storage/types";
|
|
17
20
|
|
|
18
21
|
/** Uma revisão em andamento (prepare ou finalize). */
|
|
19
22
|
export class ReviewSession {
|
|
20
23
|
readonly config: HoundsmanConfig;
|
|
21
24
|
readonly options: ReviewOptions;
|
|
22
25
|
readonly branch: string;
|
|
23
|
-
readonly
|
|
26
|
+
readonly context: ReviewContext;
|
|
24
27
|
|
|
25
|
-
constructor(
|
|
28
|
+
constructor(
|
|
29
|
+
config: HoundsmanConfig,
|
|
30
|
+
options: ReviewOptions,
|
|
31
|
+
branch: string,
|
|
32
|
+
storage: StorageProvider,
|
|
33
|
+
repo: RepoIdentity,
|
|
34
|
+
) {
|
|
26
35
|
this.config = config;
|
|
27
36
|
this.options = options;
|
|
28
37
|
this.branch = branch;
|
|
29
|
-
|
|
38
|
+
const review = options.finalize
|
|
39
|
+
? storage.findReview(repo.key, branch)
|
|
40
|
+
: storage.getOrCreateReview({
|
|
41
|
+
repo,
|
|
42
|
+
branch,
|
|
43
|
+
slug: sanitizeBranch(branch),
|
|
44
|
+
base: options.base,
|
|
45
|
+
head: options.head,
|
|
46
|
+
});
|
|
47
|
+
if (!review) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Nenhuma revisão para '${branch}'. Rode \`houndsman\` (preparar) primeiro.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
this.context = reviewContext(storage, review);
|
|
30
53
|
}
|
|
31
54
|
|
|
32
55
|
/** Resolve a branch (opção explícita ou a atual do git). */
|
|
33
|
-
static from(
|
|
34
|
-
|
|
56
|
+
static from(
|
|
57
|
+
config: HoundsmanConfig,
|
|
58
|
+
options: ReviewOptions,
|
|
59
|
+
storage: StorageProvider,
|
|
60
|
+
repo: RepoIdentity,
|
|
61
|
+
): ReviewSession {
|
|
62
|
+
return new ReviewSession(config, options, resolveBranch(options), storage, repo);
|
|
35
63
|
}
|
|
36
64
|
|
|
37
65
|
/** Passo 1 — preparar. */
|
|
38
66
|
prepare(): Promise<PrepareResult> {
|
|
39
|
-
return prepare(this.options, this.branch, this.config, this.
|
|
67
|
+
return prepare(this.options, this.branch, this.config, this.context);
|
|
40
68
|
}
|
|
41
69
|
|
|
42
70
|
/** Passo 3 — finalizar. */
|
|
43
71
|
finalize(): FinalizeResult {
|
|
44
|
-
return finalize(this.options, this.branch, this.config, this.
|
|
72
|
+
return finalize(this.options, this.branch, this.config, this.context);
|
|
45
73
|
}
|
|
46
74
|
|
|
47
75
|
/** Executa o passo escolhido em `--finalize`. */
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Estado de triagem dos findings (biblioteca).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `fingerprint` de conteúdo: nas re-execuções do `--finalize`
|
|
7
|
-
* preservado casando por fingerprint.
|
|
4
|
+
* O dump (`state.json`) é um detalhe do provider de arquivos; no SQLite o
|
|
5
|
+
* estado é reconstruído de `findings` + `finding_history`. Cada finding tem um
|
|
6
|
+
* `id` curto e um `fingerprint` de conteúdo: nas re-execuções do `--finalize`
|
|
7
|
+
* o estado é preservado casando por fingerprint.
|
|
8
8
|
*
|
|
9
9
|
* A `line` é cache — `syncState` a recalcula após edições (auto-fix) e marca
|
|
10
10
|
* `stale` quando a âncora some.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
-
import { join } from "node:path";
|
|
16
14
|
import {
|
|
17
15
|
buildAnchor,
|
|
18
16
|
type FindingAnchor,
|
|
@@ -20,14 +18,7 @@ import {
|
|
|
20
18
|
locateAnchor,
|
|
21
19
|
reanchor,
|
|
22
20
|
} from "./finding-anchor";
|
|
23
|
-
import {
|
|
24
|
-
dedupeFindings,
|
|
25
|
-
type MergedFinding,
|
|
26
|
-
parseWatchdogReport,
|
|
27
|
-
sanitizeBranch,
|
|
28
|
-
type WatchdogReport,
|
|
29
|
-
} from "./report-builder";
|
|
30
|
-
import { RunLayout } from "./run-layout";
|
|
21
|
+
import { type MergedFinding, sanitizeBranch } from "./report-builder";
|
|
31
22
|
import { systemClock } from "./services/clock";
|
|
32
23
|
import { type FileReader, ProjectFiles } from "./services/project-files";
|
|
33
24
|
import { SEVERITY_ORDER, type Severity, severityRank } from "./types";
|
|
@@ -85,6 +76,10 @@ export type GitHubLink = {
|
|
|
85
76
|
pr: number;
|
|
86
77
|
/** Quando foi enviado (ISO). */
|
|
87
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;
|
|
88
83
|
};
|
|
89
84
|
|
|
90
85
|
/** Marco da revisão que originou o estado: commit revisado + quando. */
|
|
@@ -142,16 +137,6 @@ export const FINDINGS_STATE_VERSION = 2;
|
|
|
142
137
|
// Caminhos
|
|
143
138
|
// ---------------------------------------------------------------------------
|
|
144
139
|
|
|
145
|
-
/** Diretório de artefatos da run (branch-slug). */
|
|
146
|
-
export function runDir(artifactDir: string, branch: string): string {
|
|
147
|
-
return new RunLayout(artifactDir, branch).outDir;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Caminho do `state.json` da run. */
|
|
151
|
-
export function stateFile(artifactDir: string, branch: string): string {
|
|
152
|
-
return new RunLayout(artifactDir, branch).stateFile;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
140
|
/** Leitor padrão a partir do cwd (null se ausente/ilegível). */
|
|
156
141
|
export function fileReader(cwd: string): FileReader {
|
|
157
142
|
return new ProjectFiles(cwd).reader();
|
|
@@ -264,6 +249,32 @@ export function filterFindings(
|
|
|
264
249
|
.sort(compareFindings);
|
|
265
250
|
}
|
|
266
251
|
|
|
252
|
+
/** Resultado da resolução de um id (completo ou prefixo único). */
|
|
253
|
+
export type FindingIdResolution =
|
|
254
|
+
| { status: "found"; id: string }
|
|
255
|
+
| { status: "ambiguous"; matches: string[] }
|
|
256
|
+
| { status: "missing" };
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolve um id de finding: exato primeiro, depois prefixo único.
|
|
260
|
+
* Ambíguo devolve os candidatos para a mensagem de erro.
|
|
261
|
+
*/
|
|
262
|
+
export function resolveFindingId(
|
|
263
|
+
state: FindingsStateFile,
|
|
264
|
+
input: string,
|
|
265
|
+
): FindingIdResolution {
|
|
266
|
+
const needle = input.trim();
|
|
267
|
+
if (needle.length === 0) return { status: "missing" };
|
|
268
|
+
if (state.findings[needle]) return { status: "found", id: needle };
|
|
269
|
+
|
|
270
|
+
const matches = Object.keys(state.findings).filter((id) =>
|
|
271
|
+
id.toLowerCase().startsWith(needle.toLowerCase()),
|
|
272
|
+
);
|
|
273
|
+
if (matches.length === 1) return { status: "found", id: matches[0] };
|
|
274
|
+
if (matches.length > 1) return { status: "ambiguous", matches };
|
|
275
|
+
return { status: "missing" };
|
|
276
|
+
}
|
|
277
|
+
|
|
267
278
|
// ---------------------------------------------------------------------------
|
|
268
279
|
// Construção (finalize)
|
|
269
280
|
// ---------------------------------------------------------------------------
|
|
@@ -276,6 +287,12 @@ export type BuildStateOptions = {
|
|
|
276
287
|
reviewed?: FindingsReviewed;
|
|
277
288
|
/** SHA usado para carimbar `foundAtSha` de findings novos/re-reportados. */
|
|
278
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>;
|
|
279
296
|
now?: string;
|
|
280
297
|
};
|
|
281
298
|
|
|
@@ -296,14 +313,21 @@ export function buildStateFromFindings(
|
|
|
296
313
|
|
|
297
314
|
const result: Record<string, FindingState> = {};
|
|
298
315
|
// 1. Preserva o estado anterior (marca como ausente até provar o contrário).
|
|
316
|
+
const reviewedPaths = options.reviewedPaths;
|
|
299
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";
|
|
300
327
|
result[prev.id] = {
|
|
301
328
|
...prev,
|
|
302
329
|
present: false,
|
|
303
|
-
execState:
|
|
304
|
-
prev.execState === "pending" && prev.decision === "execute"
|
|
305
|
-
? "done"
|
|
306
|
-
: prev.execState,
|
|
330
|
+
execState: resolvedByReview || queuedAndGone ? "done" : prev.execState,
|
|
307
331
|
updatedAt: now,
|
|
308
332
|
};
|
|
309
333
|
}
|
|
@@ -377,47 +401,6 @@ export function buildStateFromFindings(
|
|
|
377
401
|
return state;
|
|
378
402
|
}
|
|
379
403
|
|
|
380
|
-
/**
|
|
381
|
-
* Reconstrói o estado a partir dos artefatos da run (`classification.json` +
|
|
382
|
-
* `findings/<id>.json`), sem exigir `--finalize`. Devolve null se não houver
|
|
383
|
-
* findings gravados.
|
|
384
|
-
*/
|
|
385
|
-
export function buildStateFromRunDir(
|
|
386
|
-
outDir: string,
|
|
387
|
-
branch: string,
|
|
388
|
-
readFile: FileReader,
|
|
389
|
-
): FindingsStateFile | null {
|
|
390
|
-
const classificationPath = join(outDir, "classification.json");
|
|
391
|
-
if (!existsSync(classificationPath)) return null;
|
|
392
|
-
|
|
393
|
-
let activeIds: string[] = [];
|
|
394
|
-
try {
|
|
395
|
-
const classification = JSON.parse(readFileSync(classificationPath, "utf8")) as {
|
|
396
|
-
watchdogs?: Record<string, string[]>;
|
|
397
|
-
};
|
|
398
|
-
activeIds = Object.keys(classification.watchdogs ?? {});
|
|
399
|
-
} catch {
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const reports: WatchdogReport[] = [];
|
|
404
|
-
for (const id of activeIds) {
|
|
405
|
-
const path = join(outDir, "findings", `${id}.json`);
|
|
406
|
-
if (!existsSync(path)) continue;
|
|
407
|
-
try {
|
|
408
|
-
const report = parseWatchdogReport(
|
|
409
|
-
JSON.parse(readFileSync(path, "utf8")),
|
|
410
|
-
path,
|
|
411
|
-
);
|
|
412
|
-
if (report) reports.push(report);
|
|
413
|
-
} catch {
|
|
414
|
-
// Ignora findings ilegíveis — reconstrução best-effort.
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (reports.length === 0) return null;
|
|
418
|
-
|
|
419
|
-
return buildStateFromFindings(dedupeFindings(reports), { branch, readFile });
|
|
420
|
-
}
|
|
421
404
|
// ---------------------------------------------------------------------------
|
|
422
405
|
|
|
423
406
|
export type FindingUpdate = {
|
|
@@ -574,7 +557,7 @@ export class Finding {
|
|
|
574
557
|
}
|
|
575
558
|
}
|
|
576
559
|
|
|
577
|
-
/** Coleção de triagem de uma run — superfície OO sobre o
|
|
560
|
+
/** Coleção de triagem de uma run — superfície OO sobre o dump persistido. */
|
|
578
561
|
export class FindingsState {
|
|
579
562
|
readonly file: FindingsStateFile;
|
|
580
563
|
|
|
@@ -671,11 +654,6 @@ export class FindingsRepository {
|
|
|
671
654
|
this.branch = branch;
|
|
672
655
|
}
|
|
673
656
|
|
|
674
|
-
/** Repository para a run de uma branch. */
|
|
675
|
-
static at(artifactDir: string, branch: string): FindingsRepository {
|
|
676
|
-
return new FindingsRepository(stateFile(artifactDir, branch), branch);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
657
|
get filePath(): string {
|
|
680
658
|
return this.path;
|
|
681
659
|
}
|