@payconductor/houndsman 0.2.0 → 0.3.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/arg-parser.ts +8 -0
- package/src/cli/commands/command.ts +23 -0
- package/src/cli/commands/context-command.ts +38 -0
- package/src/cli/commands/doctor-command.ts +55 -0
- package/src/cli/commands/findings-command.ts +28 -0
- package/src/cli/commands/help-command.ts +13 -0
- package/src/cli/commands/registry.ts +34 -0
- package/src/cli/commands/review-command.ts +22 -0
- package/src/cli/commands/server-command.ts +37 -0
- package/src/cli/commands/setup-command.ts +34 -0
- package/src/cli/commands/watchdog-command.ts +34 -0
- package/src/cli/help.ts +43 -0
- package/src/cli/index.ts +58 -266
- package/src/core/arg-parser.ts +48 -0
- package/src/core/commands/findings.ts +34 -144
- package/src/core/commands/review.ts +18 -35
- package/src/core/commands/scaffold.ts +174 -64
- package/src/core/commands/setup.ts +16 -11
- package/src/core/commands/update.ts +7 -7
- package/src/core/commands/watchdog.ts +8 -80
- package/src/core/config.ts +9 -19
- package/src/core/defaults.ts +20 -0
- package/src/core/diff-source.ts +1 -13
- package/src/core/doc-discoverer.ts +25 -15
- package/src/core/domain/budget.ts +42 -0
- package/src/core/domain/classification.ts +94 -0
- package/src/core/domain/diff.ts +53 -0
- package/src/core/domain/watchdog.ts +96 -0
- package/src/core/engine/carryover.ts +67 -0
- package/src/core/engine/houndsman-engine.ts +51 -0
- package/src/core/engine/review-scope.ts +95 -0
- package/src/core/engine/review-session.ts +51 -0
- package/src/core/file-classifier.ts +11 -67
- package/src/core/findings-state.ts +170 -38
- package/src/core/git-revision.ts +14 -40
- package/src/core/git-runner.ts +46 -0
- package/src/core/github-review.ts +4 -33
- package/src/core/orchestrator.ts +116 -337
- package/src/core/paths.ts +9 -0
- package/src/core/pr-context.ts +28 -54
- package/src/core/project-config.ts +57 -0
- package/src/core/renderers/batch-presenter.ts +40 -0
- package/src/core/renderers/brief-renderer.ts +23 -0
- package/src/core/renderers/findings-presenter.ts +74 -0
- package/src/core/renderers/manifest-writer.ts +112 -0
- package/src/core/renderers/report-renderer.ts +66 -0
- package/src/core/renderers/verify-presenter.ts +50 -0
- package/src/core/renderers/watchdog-presenter.ts +100 -0
- package/src/core/report-builder.ts +12 -7
- package/src/core/run-layout.ts +108 -0
- package/src/core/run-status.ts +6 -11
- package/src/core/services/clock.ts +12 -0
- package/src/core/services/file-classifier.ts +34 -0
- package/src/core/services/fix-batch-builder.ts +13 -0
- package/src/core/services/gh-client.ts +54 -0
- package/src/core/services/git-repository.ts +123 -0
- package/src/core/services/project-files.ts +79 -0
- package/src/core/services/verifier.ts +58 -0
- package/src/core/verify.ts +2 -1
- package/src/core/watchdog-registry.ts +58 -16
- package/src/index.ts +31 -1
- package/src/server/index.ts +64 -234
- package/src/server/run-repository.ts +291 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +814 -0
- package/src/tests/baseline.test.ts +513 -493
- package/src/tests/domain.test.ts +328 -0
- package/src/tests/infrastructure.test.ts +210 -0
|
@@ -1,18 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Registry de watchdogs (biblioteca) — acesso tipado ao `HoundsmanConfig`.
|
|
3
3
|
*
|
|
4
|
-
* O motor não conhece ids concretos
|
|
4
|
+
* O motor não conhece ids concretos: o registry resolve definições, contexto e
|
|
5
|
+
* casamento de arquivos por domínio. As funções antigas continuam como
|
|
6
|
+
* adaptadores para a API pública.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import type { HoundsmanConfig } from "./config";
|
|
10
|
+
import { Watchdog, type WatchdogContext } from "./domain/watchdog";
|
|
8
11
|
import type { WatchdogDefinition } from "./types";
|
|
9
12
|
|
|
13
|
+
/** Coleção tipada de watchdogs do projeto. */
|
|
14
|
+
export class WatchdogRegistry<TId extends string = string> {
|
|
15
|
+
private readonly config: HoundsmanConfig<TId>;
|
|
16
|
+
|
|
17
|
+
constructor(config: HoundsmanConfig<TId>) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Ids na ordem de declaração. */
|
|
22
|
+
get ids(): readonly TId[] {
|
|
23
|
+
return this.config.watchdogIds;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Definição de um watchdog (id string em runtime). */
|
|
27
|
+
get(watchdogId: string): WatchdogDefinition {
|
|
28
|
+
return this.config.watchdogs[watchdogId as TId];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Busca null-safe por id. */
|
|
32
|
+
find(watchdogId: string): Watchdog | null {
|
|
33
|
+
const definition = this.config.watchdogs[watchdogId as TId];
|
|
34
|
+
return definition ? new Watchdog(definition) : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Nome legível (default: o próprio id). */
|
|
38
|
+
nameOf(watchdogId: string): string {
|
|
39
|
+
return this.find(watchdogId)?.name ?? watchdogId;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Contexto essencial (sem regras). */
|
|
43
|
+
context(watchdogId: string): WatchdogContext {
|
|
44
|
+
return new Watchdog(this.get(watchdogId)).context();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Ids dos watchdogs que cobrem o arquivo. */
|
|
48
|
+
matching(filePath: string, content?: string): TId[] {
|
|
49
|
+
return this.config.watchdogIds.filter((id) =>
|
|
50
|
+
new Watchdog(this.config.watchdogs[id]).matches(filePath, content),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Todos os watchdogs (na ordem de declaração). */
|
|
55
|
+
all(): Watchdog[] {
|
|
56
|
+
return this.config.watchdogIds.map(
|
|
57
|
+
(id) => new Watchdog(this.config.watchdogs[id]),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
10
62
|
/** Acesso tipado a um watchdog do projeto (id string em runtime). */
|
|
11
63
|
export function getWatchdog<TId extends string>(
|
|
12
64
|
config: HoundsmanConfig<TId>,
|
|
13
65
|
watchdogId: string,
|
|
14
66
|
): WatchdogDefinition {
|
|
15
|
-
return config.
|
|
67
|
+
return new WatchdogRegistry(config).get(watchdogId);
|
|
16
68
|
}
|
|
17
69
|
|
|
18
70
|
/** Busca null-safe por id string (runtime). */
|
|
@@ -20,23 +72,13 @@ export function findWatchdog<TId extends string>(
|
|
|
20
72
|
config: HoundsmanConfig<TId>,
|
|
21
73
|
watchdogId: string,
|
|
22
74
|
): WatchdogDefinition | null {
|
|
23
|
-
return config.
|
|
75
|
+
return new WatchdogRegistry(config).find(watchdogId)?.definition ?? null;
|
|
24
76
|
}
|
|
25
77
|
|
|
26
78
|
/** Contexto "essencial" do watchdog — SEM regras (docs/UI/inspeção). */
|
|
27
79
|
export function getWatchdogContext<TId extends string>(
|
|
28
80
|
config: HoundsmanConfig<TId>,
|
|
29
81
|
watchdogId: string,
|
|
30
|
-
) {
|
|
31
|
-
|
|
32
|
-
getWatchdog(config, watchdogId);
|
|
33
|
-
return {
|
|
34
|
-
id,
|
|
35
|
-
name,
|
|
36
|
-
description,
|
|
37
|
-
scope,
|
|
38
|
-
reviewFocus,
|
|
39
|
-
exclude,
|
|
40
|
-
contextFiles,
|
|
41
|
-
};
|
|
82
|
+
): WatchdogContext {
|
|
83
|
+
return new WatchdogRegistry(config).context(watchdogId);
|
|
42
84
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* (`houndsman.config.ts`): watchdogs, tipos e helpers de autoria.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
export { ArgParser } from "./core/arg-parser";
|
|
8
9
|
export { assetPath, OUTPUT_FORMAT_FILE } from "./core/assets";
|
|
9
10
|
export { defaultRenderBrief, PROCEDURE, renderBrief } from "./core/brief";
|
|
10
11
|
export type { FindingsCommandResult } from "./core/commands/findings";
|
|
11
12
|
export { runFindingsCommand, runVerifyCommand } from "./core/commands/findings";
|
|
13
|
+
export { ProjectScaffolder } from "./core/commands/scaffold";
|
|
12
14
|
export type {
|
|
13
15
|
HoundsmanBudgets,
|
|
14
16
|
HoundsmanConcurrency,
|
|
@@ -36,6 +38,14 @@ export {
|
|
|
36
38
|
parseDiffIndex,
|
|
37
39
|
parseNumstat,
|
|
38
40
|
} from "./core/diff-source";
|
|
41
|
+
export { DocDiscoverer } from "./core/doc-discoverer";
|
|
42
|
+
export { Classification } from "./core/domain/classification";
|
|
43
|
+
export { Diff } from "./core/domain/diff";
|
|
44
|
+
export { Watchdog } from "./core/domain/watchdog";
|
|
45
|
+
export { buildCarryover, mergeCarryover } from "./core/engine/carryover";
|
|
46
|
+
export { HoundsmanEngine } from "./core/engine/houndsman-engine";
|
|
47
|
+
export { ReviewScopeResolver } from "./core/engine/review-scope";
|
|
48
|
+
export { ReviewSession } from "./core/engine/review-session";
|
|
39
49
|
export {
|
|
40
50
|
churnOf,
|
|
41
51
|
classifyEntries,
|
|
@@ -81,6 +91,9 @@ export {
|
|
|
81
91
|
FINDING_DECISIONS,
|
|
82
92
|
FINDING_EXEC_STATES,
|
|
83
93
|
FINDINGS_STATE_VERSION,
|
|
94
|
+
Finding,
|
|
95
|
+
FindingsRepository,
|
|
96
|
+
FindingsState,
|
|
84
97
|
FindingsStateStore,
|
|
85
98
|
fileReader,
|
|
86
99
|
filterFindings,
|
|
@@ -91,6 +104,7 @@ export {
|
|
|
91
104
|
summarize,
|
|
92
105
|
syncState,
|
|
93
106
|
} from "./core/findings-state";
|
|
107
|
+
export type { GitResult, GitRunner } from "./core/git-runner";
|
|
94
108
|
export {
|
|
95
109
|
ARTIFACT_DIR,
|
|
96
110
|
CORE_DIR,
|
|
@@ -100,9 +114,23 @@ export {
|
|
|
100
114
|
relFromRoot,
|
|
101
115
|
STATE_FILE,
|
|
102
116
|
} from "./core/paths";
|
|
117
|
+
export { ProjectConfig } from "./core/project-config";
|
|
103
118
|
export { checkRegistry } from "./core/registry-check";
|
|
119
|
+
export { BriefRenderer } from "./core/renderers/brief-renderer";
|
|
120
|
+
export { ManifestWriter } from "./core/renderers/manifest-writer";
|
|
121
|
+
export { ReportRenderer } from "./core/renderers/report-renderer";
|
|
104
122
|
export type { DomainReport } from "./core/report-builder";
|
|
105
123
|
export { renderDomainReport } from "./core/report-builder";
|
|
124
|
+
export { RunLayout } from "./core/run-layout";
|
|
125
|
+
export type { Clock } from "./core/services/clock";
|
|
126
|
+
export { systemClock } from "./core/services/clock";
|
|
127
|
+
export { FileClassifier } from "./core/services/file-classifier";
|
|
128
|
+
export { FixBatchBuilder } from "./core/services/fix-batch-builder";
|
|
129
|
+
export type { GhResult, GhRunner } from "./core/services/gh-client";
|
|
130
|
+
export { GhClient } from "./core/services/gh-client";
|
|
131
|
+
export { GitRepository } from "./core/services/git-repository";
|
|
132
|
+
export { ProjectFiles } from "./core/services/project-files";
|
|
133
|
+
export { Verifier } from "./core/services/verifier";
|
|
106
134
|
export type {
|
|
107
135
|
ChurnMap,
|
|
108
136
|
ClassifyRule,
|
|
@@ -127,6 +155,8 @@ export {
|
|
|
127
155
|
findWatchdog,
|
|
128
156
|
getWatchdog,
|
|
129
157
|
getWatchdogContext,
|
|
158
|
+
WatchdogRegistry,
|
|
130
159
|
} from "./core/watchdog-registry";
|
|
131
160
|
export type { ServerOptions } from "./server";
|
|
132
|
-
export { createRequestHandler, startServer } from "./server";
|
|
161
|
+
export { createRequestHandler, HoundsmanServer, startServer } from "./server";
|
|
162
|
+
export { RunRepository } from "./server/run-repository";
|
package/src/server/index.ts
CHANGED
|
@@ -6,41 +6,16 @@
|
|
|
6
6
|
* Bind padrão em `127.0.0.1` — ferramenta local, sem autenticação.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { join, resolve } from "node:path";
|
|
10
9
|
import { mediaPath } from "../core/assets";
|
|
11
|
-
import {
|
|
10
|
+
import type { HoundsmanConfig } from "../core/config";
|
|
11
|
+
import type { FindingUpdate } from "../core/findings-state";
|
|
12
12
|
import {
|
|
13
|
-
applyFindingUpdate,
|
|
14
|
-
buildStateFromRunDir,
|
|
15
|
-
emptyState,
|
|
16
|
-
type FindingDecision,
|
|
17
|
-
type FindingExecState,
|
|
18
|
-
type FindingState,
|
|
19
|
-
type FindingsFilter,
|
|
20
|
-
type FindingsStateFile,
|
|
21
|
-
FindingsStateStore,
|
|
22
|
-
type FindingsSummary,
|
|
23
|
-
type FindingUpdate,
|
|
24
|
-
fileReader,
|
|
25
|
-
filterFindings,
|
|
26
|
-
reorder,
|
|
27
|
-
type SyncResult,
|
|
28
|
-
summarize,
|
|
29
|
-
syncState,
|
|
30
|
-
} from "../core/findings-state";
|
|
31
|
-
import {
|
|
32
|
-
buildSubmitPreview,
|
|
33
13
|
defaultGhRunner,
|
|
34
14
|
type GhRunner,
|
|
35
|
-
resolvePrTarget,
|
|
36
15
|
type SubmitSelection,
|
|
37
|
-
submitFindings,
|
|
38
16
|
} from "../core/github-review";
|
|
39
|
-
import { STATE_FILE } from "../core/paths";
|
|
40
|
-
import { pingFindingsStatus, type RunStatus, readRunStatus } from "../core/run-status";
|
|
41
|
-
import type { Severity } from "../core/types";
|
|
42
|
-
import { listRunSlugs, readRunFile, readRunJson, runDirForSlug } from "./artifacts";
|
|
43
17
|
import { buildClientBundle } from "./client-build";
|
|
18
|
+
import { RunRepository } from "./run-repository";
|
|
44
19
|
|
|
45
20
|
type BunServer = ReturnType<typeof Bun.serve>;
|
|
46
21
|
|
|
@@ -77,180 +52,8 @@ function notFound(detail = "não encontrado"): Response {
|
|
|
77
52
|
// Estado / dados
|
|
78
53
|
// ---------------------------------------------------------------------------
|
|
79
54
|
|
|
80
|
-
/**
|
|
81
|
-
type
|
|
82
|
-
slug: string;
|
|
83
|
-
branch: string;
|
|
84
|
-
updatedAt?: string;
|
|
85
|
-
status: RunStatus | null;
|
|
86
|
-
summary: FindingsSummary;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
function buildForRun(dir: string, slug: string): FindingsStateFile | null {
|
|
90
|
-
const manifest = readRunJson<{ branch?: string }>(dir, "manifest.json");
|
|
91
|
-
return buildStateFromRunDir(dir, manifest?.branch ?? slug, fileReader(process.cwd()));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Lê o estado da run. Se ausente, reconstrói dos artefatos e PERSISTE — os ids
|
|
96
|
-
* são aleatórios, então precisam ser estáveis entre requisições.
|
|
97
|
-
*/
|
|
98
|
-
function loadState(dir: string, slug: string): FindingsStateFile {
|
|
99
|
-
const store = new FindingsStateStore(join(dir, STATE_FILE), slug);
|
|
100
|
-
const existing = store.read();
|
|
101
|
-
if (existing) return existing;
|
|
102
|
-
const built = buildForRun(dir, slug);
|
|
103
|
-
if (built) {
|
|
104
|
-
store.write(built);
|
|
105
|
-
return built;
|
|
106
|
-
}
|
|
107
|
-
return emptyState(slug);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function listRuns(artifactDir: string): RunListItem[] {
|
|
111
|
-
return (
|
|
112
|
-
listRunSlugs(artifactDir)
|
|
113
|
-
.map((slug) => {
|
|
114
|
-
const dir = join(artifactDir, slug);
|
|
115
|
-
const state = loadState(dir, slug);
|
|
116
|
-
const manifest = readRunJson<{ branch?: string; generatedAt?: string }>(
|
|
117
|
-
dir,
|
|
118
|
-
"manifest.json",
|
|
119
|
-
);
|
|
120
|
-
const status = readRunStatus(dir);
|
|
121
|
-
return {
|
|
122
|
-
slug,
|
|
123
|
-
branch: state.branch || manifest?.branch || slug,
|
|
124
|
-
updatedAt: status?.updatedAt ?? state.updatedAt,
|
|
125
|
-
status: status?.status ?? null,
|
|
126
|
-
summary: summarize(state),
|
|
127
|
-
};
|
|
128
|
-
})
|
|
129
|
-
// Mais recentes primeiro (ISO 8601 compara lexicograficamente).
|
|
130
|
-
.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Finding + caminho absoluto. O cliente monta o link `vscode://file/...` e não
|
|
136
|
-
* conhece o cwd do servidor — o caminho relativo do relatório não basta.
|
|
137
|
-
*/
|
|
138
|
-
function toClientFinding(finding: FindingState): FindingState & { absFile: string } {
|
|
139
|
-
return { ...finding, absFile: resolve(process.cwd(), finding.file) };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function runDetail(dir: string, slug: string): unknown {
|
|
143
|
-
const state = loadState(dir, slug);
|
|
144
|
-
const classification = readRunJson<{
|
|
145
|
-
watchdogs?: Record<string, string[]>;
|
|
146
|
-
unclassified?: string[];
|
|
147
|
-
deleted?: string[];
|
|
148
|
-
}>(dir, "classification.json");
|
|
149
|
-
return {
|
|
150
|
-
slug,
|
|
151
|
-
branch: state.branch || slug,
|
|
152
|
-
status: readRunStatus(dir),
|
|
153
|
-
manifest: readRunJson(dir, "manifest.json"),
|
|
154
|
-
summary: summarize(state),
|
|
155
|
-
watchdogs: Object.keys(classification?.watchdogs ?? {}),
|
|
156
|
-
unclassified: classification?.unclassified ?? [],
|
|
157
|
-
deleted: classification?.deleted ?? [],
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function findingsFor(dir: string, slug: string, params: URLSearchParams): unknown {
|
|
162
|
-
const state = loadState(dir, slug);
|
|
163
|
-
const filter: FindingsFilter = {
|
|
164
|
-
decision: (params.get("decision") as FindingDecision) || undefined,
|
|
165
|
-
execState: (params.get("exec") as FindingExecState) || undefined,
|
|
166
|
-
severity: (params.get("severity") as Severity) || undefined,
|
|
167
|
-
watchdog: params.get("watchdog") || undefined,
|
|
168
|
-
file: params.get("file") || undefined,
|
|
169
|
-
includeAbsent: params.get("all") === "1",
|
|
170
|
-
};
|
|
171
|
-
return {
|
|
172
|
-
branch: state.branch,
|
|
173
|
-
findings: filterFindings(state, filter).map(toClientFinding),
|
|
174
|
-
summary: summarize(state),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function updateFinding(
|
|
179
|
-
dir: string,
|
|
180
|
-
slug: string,
|
|
181
|
-
id: string,
|
|
182
|
-
request: Request,
|
|
183
|
-
): Promise<Response> {
|
|
184
|
-
const body = (await request.json().catch(() => ({}))) as FindingUpdate;
|
|
185
|
-
const store = new FindingsStateStore(join(dir, STATE_FILE), slug);
|
|
186
|
-
const state = store.read() ?? buildForRun(dir, slug) ?? emptyState(slug);
|
|
187
|
-
const finding = state.findings[id];
|
|
188
|
-
if (!finding) return notFound(`finding '${id}' não encontrado`);
|
|
189
|
-
applyFindingUpdate(finding, body);
|
|
190
|
-
reorder(state);
|
|
191
|
-
store.write(state);
|
|
192
|
-
pingFindingsStatus(dir, state);
|
|
193
|
-
return json({ finding: toClientFinding(finding) });
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function syncRun(dir: string, slug: string): Response {
|
|
197
|
-
const store = new FindingsStateStore(join(dir, STATE_FILE), slug);
|
|
198
|
-
const state = store.read() ?? buildForRun(dir, slug) ?? emptyState(slug);
|
|
199
|
-
const result: SyncResult = syncState(state, fileReader(process.cwd()));
|
|
200
|
-
store.write(state);
|
|
201
|
-
pingFindingsStatus(dir, state);
|
|
202
|
-
return json({ result, summary: summarize(state) });
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** Prévia do envio da revisão: candidatos + disponibilidade da PR (via gh). */
|
|
206
|
-
function submitPreview(dir: string, slug: string, ghRunner: GhRunner): Response {
|
|
207
|
-
const state = loadState(dir, slug);
|
|
208
|
-
const target = resolvePrTarget(ghRunner, state.branch || slug);
|
|
209
|
-
return json(buildSubmitPreview(state, target));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Envia os findings selecionados como comentários na PR. Fail-open: sem PR/gh
|
|
214
|
-
* devolve `{ available:false, reason }` (a UI só informa). Persiste o vínculo
|
|
215
|
-
* (`finding.github`) no `state.json` e atualiza o status da run.
|
|
216
|
-
*/
|
|
217
|
-
async function submitRun(
|
|
218
|
-
dir: string,
|
|
219
|
-
slug: string,
|
|
220
|
-
ghRunner: GhRunner,
|
|
221
|
-
request: Request,
|
|
222
|
-
): Promise<Response> {
|
|
223
|
-
const state = loadState(dir, slug);
|
|
224
|
-
const body = (await request.json().catch(() => ({}))) as SubmitSelection;
|
|
225
|
-
const selection: SubmitSelection = {
|
|
226
|
-
severities: body.severities,
|
|
227
|
-
execStates: body.execStates,
|
|
228
|
-
includeSent: body.includeSent,
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
const target = resolvePrTarget(ghRunner, state.branch || slug);
|
|
232
|
-
if (!target.ok) return json({ available: false, reason: target.reason });
|
|
233
|
-
|
|
234
|
-
const outcome = submitFindings({
|
|
235
|
-
state,
|
|
236
|
-
target: target.target,
|
|
237
|
-
runner: ghRunner,
|
|
238
|
-
selection,
|
|
239
|
-
});
|
|
240
|
-
new FindingsStateStore(join(dir, STATE_FILE), slug).write(state);
|
|
241
|
-
pingFindingsStatus(dir, state);
|
|
242
|
-
return json({ available: true, target: target.target, outcome });
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function serveArtifact(dir: string, name: string): Response {
|
|
246
|
-
if (!/\.(json|md)$/i.test(name)) return notFound("tipo de artefato não servido");
|
|
247
|
-
const content = readRunFile(dir, name);
|
|
248
|
-
if (content === null) return notFound(`artefato '${name}' não encontrado`);
|
|
249
|
-
const type = name.toLowerCase().endsWith(".json")
|
|
250
|
-
? "application/json; charset=utf-8"
|
|
251
|
-
: "text/markdown; charset=utf-8";
|
|
252
|
-
return text(content, type);
|
|
253
|
-
}
|
|
55
|
+
/** Repositório das runs (dados) — ver `run-repository.ts`. */
|
|
56
|
+
type ServerDeps = { ghRunner?: GhRunner };
|
|
254
57
|
|
|
255
58
|
// ---------------------------------------------------------------------------
|
|
256
59
|
// Rotas
|
|
@@ -270,7 +73,7 @@ async function serveLogo(): Promise<Response> {
|
|
|
270
73
|
|
|
271
74
|
async function route(
|
|
272
75
|
request: Request,
|
|
273
|
-
|
|
76
|
+
runs: RunRepository,
|
|
274
77
|
ghRunner: GhRunner,
|
|
275
78
|
): Promise<Response> {
|
|
276
79
|
const url = new URL(request.url);
|
|
@@ -286,65 +89,77 @@ async function route(
|
|
|
286
89
|
if (path === "/api/health") return json({ ok: true });
|
|
287
90
|
|
|
288
91
|
if (path === "/api/runs" && request.method === "GET") {
|
|
289
|
-
return json({ runs:
|
|
92
|
+
return json({ runs: runs.list() });
|
|
290
93
|
}
|
|
291
94
|
|
|
292
95
|
const match = path.match(API_RUN);
|
|
293
96
|
if (!match) return notFound();
|
|
294
97
|
const slug = decodeURIComponent(match[1]);
|
|
295
98
|
const rest = match[2] ?? "";
|
|
296
|
-
|
|
297
|
-
if (!dir) return notFound(`run '${slug}' não encontrada`);
|
|
99
|
+
if (!runs.exists(slug)) return notFound(`run '${slug}' não encontrada`);
|
|
298
100
|
|
|
299
|
-
if (rest === "" || rest === "/") return json(
|
|
101
|
+
if (rest === "" || rest === "/") return json(runs.detail(slug));
|
|
300
102
|
|
|
301
103
|
if (rest === "/findings" && request.method === "GET") {
|
|
302
|
-
return json(
|
|
104
|
+
return json(runs.findings(slug, url.searchParams));
|
|
303
105
|
}
|
|
304
|
-
if (rest === "/sync" && request.method === "POST") return
|
|
106
|
+
if (rest === "/sync" && request.method === "POST") return json(runs.sync(slug));
|
|
305
107
|
if (rest === "/submit-preview" && request.method === "GET") {
|
|
306
|
-
return submitPreview(
|
|
108
|
+
return json(runs.submitPreview(slug, ghRunner));
|
|
307
109
|
}
|
|
308
110
|
if (rest === "/submit" && request.method === "POST") {
|
|
309
|
-
|
|
111
|
+
const body = (await request.json().catch(() => ({}))) as SubmitSelection;
|
|
112
|
+
const selection: SubmitSelection = {
|
|
113
|
+
severities: body.severities,
|
|
114
|
+
execStates: body.execStates,
|
|
115
|
+
includeSent: body.includeSent,
|
|
116
|
+
};
|
|
117
|
+
return json(runs.submit(slug, selection, ghRunner));
|
|
310
118
|
}
|
|
311
119
|
|
|
312
120
|
const findingMatch = rest.match(/^\/findings\/([^/]+)$/);
|
|
313
121
|
if (findingMatch) {
|
|
314
122
|
const id = decodeURIComponent(findingMatch[1]);
|
|
315
123
|
if (request.method === "GET") {
|
|
316
|
-
const
|
|
317
|
-
const finding = state.findings[id];
|
|
124
|
+
const finding = runs.finding(slug, id);
|
|
318
125
|
return finding
|
|
319
|
-
? json({ finding
|
|
126
|
+
? json({ finding })
|
|
320
127
|
: notFound(`finding '${id}' não encontrado`);
|
|
321
128
|
}
|
|
322
129
|
if (request.method === "PATCH" || request.method === "POST") {
|
|
323
|
-
|
|
130
|
+
const body = (await request.json().catch(() => ({}))) as FindingUpdate;
|
|
131
|
+
const finding = runs.update(slug, id, body);
|
|
132
|
+
return finding
|
|
133
|
+
? json({ finding })
|
|
134
|
+
: notFound(`finding '${id}' não encontrado`);
|
|
324
135
|
}
|
|
325
136
|
}
|
|
326
137
|
|
|
327
138
|
const artifactMatch = rest.match(/^\/artifacts\/(.+)$/);
|
|
328
139
|
if (artifactMatch && request.method === "GET") {
|
|
329
|
-
|
|
140
|
+
const name = decodeURIComponent(artifactMatch[1]);
|
|
141
|
+
const artifact = runs.artifact(slug, name);
|
|
142
|
+
if (!artifact) return notFound(`artefato '${name}' não encontrado`);
|
|
143
|
+
return text(artifact.content, artifact.type);
|
|
330
144
|
}
|
|
331
145
|
|
|
332
146
|
return notFound();
|
|
333
147
|
}
|
|
334
148
|
|
|
335
|
-
/**
|
|
336
|
-
export
|
|
149
|
+
/** Servidor local do houndsman (rotas + shell). */
|
|
150
|
+
export class HoundsmanServer {
|
|
151
|
+
private readonly runs: RunRepository;
|
|
152
|
+
private readonly ghRunner: GhRunner;
|
|
337
153
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return async (request) => {
|
|
154
|
+
constructor(config: HoundsmanConfig, deps: ServerDeps = {}) {
|
|
155
|
+
this.runs = RunRepository.fromConfig(config);
|
|
156
|
+
this.ghRunner = deps.ghRunner ?? defaultGhRunner;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Handler de requisições (testável sem abrir porta). */
|
|
160
|
+
readonly handler = async (request: Request): Promise<Response> => {
|
|
346
161
|
try {
|
|
347
|
-
return await route(request,
|
|
162
|
+
return await route(request, this.runs, this.ghRunner);
|
|
348
163
|
} catch (error) {
|
|
349
164
|
return json(
|
|
350
165
|
{ error: error instanceof Error ? error.message : String(error) },
|
|
@@ -352,6 +167,25 @@ export function createRequestHandler(
|
|
|
352
167
|
);
|
|
353
168
|
}
|
|
354
169
|
};
|
|
170
|
+
|
|
171
|
+
/** Sobe o servidor e devolve a instância + URL. */
|
|
172
|
+
start(options: { port?: number; host?: string } = {}): {
|
|
173
|
+
server: BunServer;
|
|
174
|
+
url: string;
|
|
175
|
+
} {
|
|
176
|
+
const host = options.host ?? "127.0.0.1";
|
|
177
|
+
const port = options.port ?? 7331;
|
|
178
|
+
const server = Bun.serve({ hostname: host, port, fetch: this.handler });
|
|
179
|
+
return { server, url: `http://${host}:${server.port}` };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Cria o handler de requisições (testável sem abrir porta). */
|
|
184
|
+
export function createRequestHandler(
|
|
185
|
+
config: HoundsmanConfig,
|
|
186
|
+
deps: ServerDeps = {},
|
|
187
|
+
): (request: Request) => Promise<Response> {
|
|
188
|
+
return new HoundsmanServer(config, deps).handler;
|
|
355
189
|
}
|
|
356
190
|
|
|
357
191
|
export type ServerOptions = {
|
|
@@ -364,14 +198,10 @@ export type ServerOptions = {
|
|
|
364
198
|
|
|
365
199
|
/** Sobe o servidor e devolve a instância + URL. */
|
|
366
200
|
export function startServer(options: ServerOptions): { server: BunServer; url: string } {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
hostname: host,
|
|
371
|
-
port,
|
|
372
|
-
fetch: createRequestHandler(options.config, { ghRunner: options.ghRunner }),
|
|
201
|
+
return new HoundsmanServer(options.config, { ghRunner: options.ghRunner }).start({
|
|
202
|
+
port: options.port,
|
|
203
|
+
host: options.host,
|
|
373
204
|
});
|
|
374
|
-
return { server, url: `http://${host}:${server.port}` };
|
|
375
205
|
}
|
|
376
206
|
|
|
377
207
|
// ---------------------------------------------------------------------------
|