@payconductor/houndsman 0.3.0 → 0.4.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 +18 -6
- 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/help.ts +7 -3
- package/src/cli/index.ts +2 -2
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +155 -120
- package/src/core/commands/review.ts +7 -8
- package/src/core/commands/scaffold.ts +15 -16
- package/src/core/config.ts +16 -7
- package/src/core/engine/houndsman-engine.ts +15 -1
- package/src/core/engine/review-session.ts +37 -9
- package/src/core/findings-state.ts +32 -71
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +175 -128
- package/src/core/paths.ts +23 -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/run-layout.ts +4 -104
- package/src/core/run-status.ts +28 -53
- 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 +269 -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 +160 -0
- package/src/core/storage/sqlite/schema.ts +108 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
- package/src/core/storage/types.ts +143 -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/ArtifactsPanel.tsx +137 -23
- package/src/server/client/FindingCard.tsx +37 -6
- package/src/server/client/MarkdownText.tsx +101 -0
- package/src/server/client/SubmitModal.tsx +9 -4
- package/src/server/client/code-highlight.ts +422 -0
- package/src/server/client/constants.ts +130 -24
- package/src/server/client/json-highlight.ts +55 -0
- package/src/server/client/markdown.ts +153 -0
- package/src/server/client/types.ts +23 -0
- package/src/server/index.ts +95 -15
- package/src/server/run-repository.ts +144 -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/fix.md +5 -5
- package/src/skill/hunt.md +27 -18
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +5 -5
- package/src/tests/__snapshots__/baseline.test.ts.snap +44 -33
- package/src/tests/baseline.test.ts +76 -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 +6 -3
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/infrastructure.test.ts +33 -38
- package/src/tests/markdown.test.ts +89 -0
- package/src/tests/review-scripts.test.ts +8 -5
- package/src/tests/run-status.test.ts +33 -23
- package/src/tests/server.test.ts +108 -75
- package/src/tests/storage.test.ts +386 -0
|
@@ -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
|
}
|
|
@@ -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";
|
|
@@ -142,16 +133,6 @@ export const FINDINGS_STATE_VERSION = 2;
|
|
|
142
133
|
// Caminhos
|
|
143
134
|
// ---------------------------------------------------------------------------
|
|
144
135
|
|
|
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
136
|
/** Leitor padrão a partir do cwd (null se ausente/ilegível). */
|
|
156
137
|
export function fileReader(cwd: string): FileReader {
|
|
157
138
|
return new ProjectFiles(cwd).reader();
|
|
@@ -264,6 +245,32 @@ export function filterFindings(
|
|
|
264
245
|
.sort(compareFindings);
|
|
265
246
|
}
|
|
266
247
|
|
|
248
|
+
/** Resultado da resolução de um id (completo ou prefixo único). */
|
|
249
|
+
export type FindingIdResolution =
|
|
250
|
+
| { status: "found"; id: string }
|
|
251
|
+
| { status: "ambiguous"; matches: string[] }
|
|
252
|
+
| { status: "missing" };
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Resolve um id de finding: exato primeiro, depois prefixo único.
|
|
256
|
+
* Ambíguo devolve os candidatos para a mensagem de erro.
|
|
257
|
+
*/
|
|
258
|
+
export function resolveFindingId(
|
|
259
|
+
state: FindingsStateFile,
|
|
260
|
+
input: string,
|
|
261
|
+
): FindingIdResolution {
|
|
262
|
+
const needle = input.trim();
|
|
263
|
+
if (needle.length === 0) return { status: "missing" };
|
|
264
|
+
if (state.findings[needle]) return { status: "found", id: needle };
|
|
265
|
+
|
|
266
|
+
const matches = Object.keys(state.findings).filter((id) =>
|
|
267
|
+
id.toLowerCase().startsWith(needle.toLowerCase()),
|
|
268
|
+
);
|
|
269
|
+
if (matches.length === 1) return { status: "found", id: matches[0] };
|
|
270
|
+
if (matches.length > 1) return { status: "ambiguous", matches };
|
|
271
|
+
return { status: "missing" };
|
|
272
|
+
}
|
|
273
|
+
|
|
267
274
|
// ---------------------------------------------------------------------------
|
|
268
275
|
// Construção (finalize)
|
|
269
276
|
// ---------------------------------------------------------------------------
|
|
@@ -377,47 +384,6 @@ export function buildStateFromFindings(
|
|
|
377
384
|
return state;
|
|
378
385
|
}
|
|
379
386
|
|
|
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
387
|
// ---------------------------------------------------------------------------
|
|
422
388
|
|
|
423
389
|
export type FindingUpdate = {
|
|
@@ -574,7 +540,7 @@ export class Finding {
|
|
|
574
540
|
}
|
|
575
541
|
}
|
|
576
542
|
|
|
577
|
-
/** Coleção de triagem de uma run — superfície OO sobre o
|
|
543
|
+
/** Coleção de triagem de uma run — superfície OO sobre o dump persistido. */
|
|
578
544
|
export class FindingsState {
|
|
579
545
|
readonly file: FindingsStateFile;
|
|
580
546
|
|
|
@@ -671,11 +637,6 @@ export class FindingsRepository {
|
|
|
671
637
|
this.branch = branch;
|
|
672
638
|
}
|
|
673
639
|
|
|
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
640
|
get filePath(): string {
|
|
680
641
|
return this.path;
|
|
681
642
|
}
|
|
@@ -36,6 +36,50 @@ export type JevCandidatesFile = {
|
|
|
36
36
|
|
|
37
37
|
const SEVERITY_SET = new Set<string>(SEVERITY_ORDER);
|
|
38
38
|
|
|
39
|
+
/** Serializa o artefato `candidates.json` (sempre com quebra de linha final). */
|
|
40
|
+
export function serializeCandidates(data: JevCandidatesFile): string {
|
|
41
|
+
return `${JSON.stringify(data, null, 2)}\n`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Valida/parseia o JSON bruto do artefato; null se inválido. */
|
|
45
|
+
export function parseCandidates(
|
|
46
|
+
raw: unknown,
|
|
47
|
+
validIds: readonly string[] = [],
|
|
48
|
+
): JevCandidatesFile | null {
|
|
49
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
50
|
+
const value = raw as Record<string, unknown>;
|
|
51
|
+
const idSet = validIds.length > 0 ? new Set(validIds) : null;
|
|
52
|
+
const isCandidate = (entry: unknown): entry is JevCandidate => {
|
|
53
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
54
|
+
const c = entry as Record<string, unknown>;
|
|
55
|
+
return (
|
|
56
|
+
typeof c.watchdogId === "string" &&
|
|
57
|
+
(idSet === null || idSet.has(c.watchdogId)) &&
|
|
58
|
+
typeof c.rule === "string" &&
|
|
59
|
+
typeof c.severity === "string" &&
|
|
60
|
+
SEVERITY_SET.has(c.severity) &&
|
|
61
|
+
typeof c.file === "string" &&
|
|
62
|
+
typeof c.confidence === "number" &&
|
|
63
|
+
typeof c.questionKey === "string"
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
const candidates = Array.isArray(value.candidates)
|
|
67
|
+
? value.candidates.filter(isCandidate)
|
|
68
|
+
: [];
|
|
69
|
+
return {
|
|
70
|
+
available: value.available === true,
|
|
71
|
+
reason: typeof value.reason === "string" ? value.reason : undefined,
|
|
72
|
+
model: typeof value.model === "string" ? value.model : undefined,
|
|
73
|
+
threshold: typeof value.threshold === "number" ? value.threshold : undefined,
|
|
74
|
+
analyzedFiles:
|
|
75
|
+
typeof value.analyzedFiles === "number" ? value.analyzedFiles : undefined,
|
|
76
|
+
candidates,
|
|
77
|
+
skipped: Array.isArray(value.skipped)
|
|
78
|
+
? (value.skipped as JevSkippedFile[])
|
|
79
|
+
: undefined,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
39
83
|
/** Persistência do artefato `candidates.json` para um diretório de review. */
|
|
40
84
|
export class JevCandidatesStore {
|
|
41
85
|
/** Limiar padrão de confiança (`noul`) para virar candidato. */
|
|
@@ -57,49 +101,19 @@ export class JevCandidatesStore {
|
|
|
57
101
|
|
|
58
102
|
/** Grava o `candidates.json` (sempre com candidatos válidos apenas). */
|
|
59
103
|
write(data: JevCandidatesFile): void {
|
|
60
|
-
writeFileSync(this.filePath,
|
|
104
|
+
writeFileSync(this.filePath, serializeCandidates(data));
|
|
61
105
|
}
|
|
62
106
|
|
|
63
107
|
/** Lê o `candidates.json`; null se ausente/inválido. */
|
|
64
108
|
read(): JevCandidatesFile | null {
|
|
65
109
|
if (!existsSync(this.filePath)) return null;
|
|
66
110
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const candidates = Array.isArray(raw.candidates)
|
|
72
|
-
? raw.candidates.filter((c) => this.isCandidate(c))
|
|
73
|
-
: [];
|
|
74
|
-
return {
|
|
75
|
-
available: raw.available === true,
|
|
76
|
-
reason: typeof raw.reason === "string" ? raw.reason : undefined,
|
|
77
|
-
model: typeof raw.model === "string" ? raw.model : undefined,
|
|
78
|
-
threshold: typeof raw.threshold === "number" ? raw.threshold : undefined,
|
|
79
|
-
analyzedFiles:
|
|
80
|
-
typeof raw.analyzedFiles === "number" ? raw.analyzedFiles : undefined,
|
|
81
|
-
candidates,
|
|
82
|
-
skipped: Array.isArray(raw.skipped)
|
|
83
|
-
? (raw.skipped as JevSkippedFile[])
|
|
84
|
-
: undefined,
|
|
85
|
-
};
|
|
111
|
+
return parseCandidates(
|
|
112
|
+
JSON.parse(readFileSync(this.filePath, "utf8")),
|
|
113
|
+
this.watchdogIdSet ? [...this.watchdogIdSet] : [],
|
|
114
|
+
);
|
|
86
115
|
} catch {
|
|
87
116
|
return null;
|
|
88
117
|
}
|
|
89
118
|
}
|
|
90
|
-
|
|
91
|
-
private isCandidate(value: unknown): value is JevCandidate {
|
|
92
|
-
if (typeof value !== "object" || value === null) return false;
|
|
93
|
-
const c = value as Record<string, unknown>;
|
|
94
|
-
return (
|
|
95
|
-
typeof c.watchdogId === "string" &&
|
|
96
|
-
(this.watchdogIdSet === null || this.watchdogIdSet.has(c.watchdogId)) &&
|
|
97
|
-
typeof c.rule === "string" &&
|
|
98
|
-
typeof c.severity === "string" &&
|
|
99
|
-
SEVERITY_SET.has(c.severity) &&
|
|
100
|
-
typeof c.file === "string" &&
|
|
101
|
-
typeof c.confidence === "number" &&
|
|
102
|
-
typeof c.questionKey === "string"
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
119
|
}
|