@payconductor/houndsman 0.2.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/arg-parser.ts +8 -0
- package/src/cli/commands/command.ts +23 -0
- package/src/cli/commands/context-command.ts +40 -0
- package/src/cli/commands/doctor-command.ts +60 -0
- package/src/cli/commands/findings-command.ts +38 -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 +21 -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 +47 -0
- package/src/cli/index.ts +60 -268
- package/src/core/arg-parser.ts +48 -0
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +185 -260
- package/src/core/commands/review.ts +21 -39
- package/src/core/commands/scaffold.ts +189 -80
- 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 +24 -25
- 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 +65 -0
- package/src/core/engine/review-scope.ts +95 -0
- package/src/core/engine/review-session.ts +79 -0
- package/src/core/file-classifier.ts +11 -67
- package/src/core/findings-state.ts +193 -100
- 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/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +247 -421
- package/src/core/paths.ts +26 -11
- package/src/core/pr-context.ts +39 -51
- package/src/core/project-config.ts +56 -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 +123 -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 +8 -0
- package/src/core/run-status.ts +29 -59
- 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 +139 -0
- package/src/core/services/project-files.ts +91 -0
- package/src/core/services/verifier.ts +58 -0
- 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/core/verify.ts +2 -1
- package/src/core/watchdog-registry.ts +58 -16
- package/src/index.ts +57 -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 +155 -245
- package/src/server/run-repository.ts +299 -0
- 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 +825 -0
- package/src/tests/baseline.test.ts +555 -493
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +328 -0
- 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 +205 -0
- 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
package/src/server/index.ts
CHANGED
|
@@ -1,46 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Servidor local do houndsman (`Bun.serve`) — UI de triagem + artefatos.
|
|
3
3
|
*
|
|
4
|
-
* Serve um SPA Preact (bundle em memória) + uma API JSON sobre o
|
|
5
|
-
*
|
|
6
|
-
* Bind padrão em `127.0.0.1` — ferramenta local, sem autenticação.
|
|
4
|
+
* Serve um SPA Preact (bundle em memória) + uma API JSON sobre o storage global
|
|
5
|
+
* (SQLite por default), escopada ao repositório do cwd. Sem dependências além do
|
|
6
|
+
* Bun e do Preact. 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 {
|
|
40
|
-
import {
|
|
41
|
-
import type {
|
|
42
|
-
import { listRunSlugs, readRunFile, readRunJson, runDirForSlug } from "./artifacts";
|
|
17
|
+
import { createStorage } from "../core/storage/factory";
|
|
18
|
+
import { resolveRepoIdentity } from "../core/storage/repo-identity";
|
|
19
|
+
import type { RepoIdentity, StorageProvider } from "../core/storage/types";
|
|
43
20
|
import { buildClientBundle } from "./client-build";
|
|
21
|
+
import { RunRepository } from "./run-repository";
|
|
44
22
|
|
|
45
23
|
type BunServer = ReturnType<typeof Bun.serve>;
|
|
46
24
|
|
|
@@ -77,181 +55,13 @@ function notFound(detail = "não encontrado"): Response {
|
|
|
77
55
|
// Estado / dados
|
|
78
56
|
// ---------------------------------------------------------------------------
|
|
79
57
|
|
|
80
|
-
/**
|
|
81
|
-
type
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
status: RunStatus | null;
|
|
86
|
-
summary: FindingsSummary;
|
|
58
|
+
/** Repositório das runs (dados) — ver `run-repository.ts`. */
|
|
59
|
+
type ServerDeps = {
|
|
60
|
+
ghRunner?: GhRunner;
|
|
61
|
+
storage?: StorageProvider;
|
|
62
|
+
repo?: RepoIdentity;
|
|
87
63
|
};
|
|
88
64
|
|
|
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
|
-
}
|
|
254
|
-
|
|
255
65
|
// ---------------------------------------------------------------------------
|
|
256
66
|
// Rotas
|
|
257
67
|
// ---------------------------------------------------------------------------
|
|
@@ -270,7 +80,7 @@ async function serveLogo(): Promise<Response> {
|
|
|
270
80
|
|
|
271
81
|
async function route(
|
|
272
82
|
request: Request,
|
|
273
|
-
|
|
83
|
+
runs: RunRepository,
|
|
274
84
|
ghRunner: GhRunner,
|
|
275
85
|
): Promise<Response> {
|
|
276
86
|
const url = new URL(request.url);
|
|
@@ -286,65 +96,83 @@ async function route(
|
|
|
286
96
|
if (path === "/api/health") return json({ ok: true });
|
|
287
97
|
|
|
288
98
|
if (path === "/api/runs" && request.method === "GET") {
|
|
289
|
-
return json({ runs:
|
|
99
|
+
return json({ runs: runs.list() });
|
|
290
100
|
}
|
|
291
101
|
|
|
292
102
|
const match = path.match(API_RUN);
|
|
293
103
|
if (!match) return notFound();
|
|
294
104
|
const slug = decodeURIComponent(match[1]);
|
|
295
105
|
const rest = match[2] ?? "";
|
|
296
|
-
|
|
297
|
-
if (!dir) return notFound(`run '${slug}' não encontrada`);
|
|
106
|
+
if (!runs.exists(slug)) return notFound(`run '${slug}' não encontrada`);
|
|
298
107
|
|
|
299
|
-
if (rest === "" || rest === "/") return json(
|
|
108
|
+
if (rest === "" || rest === "/") return json(runs.detail(slug));
|
|
300
109
|
|
|
301
110
|
if (rest === "/findings" && request.method === "GET") {
|
|
302
|
-
return json(
|
|
111
|
+
return json(runs.findings(slug, url.searchParams));
|
|
303
112
|
}
|
|
304
|
-
if (rest === "/sync" && request.method === "POST") return
|
|
113
|
+
if (rest === "/sync" && request.method === "POST") return json(runs.sync(slug));
|
|
305
114
|
if (rest === "/submit-preview" && request.method === "GET") {
|
|
306
|
-
return submitPreview(
|
|
115
|
+
return json(runs.submitPreview(slug, ghRunner));
|
|
307
116
|
}
|
|
308
117
|
if (rest === "/submit" && request.method === "POST") {
|
|
309
|
-
|
|
118
|
+
const body = (await request.json().catch(() => ({}))) as SubmitSelection;
|
|
119
|
+
const selection: SubmitSelection = {
|
|
120
|
+
severities: body.severities,
|
|
121
|
+
execStates: body.execStates,
|
|
122
|
+
includeSent: body.includeSent,
|
|
123
|
+
};
|
|
124
|
+
return json(runs.submit(slug, selection, ghRunner));
|
|
310
125
|
}
|
|
311
126
|
|
|
312
127
|
const findingMatch = rest.match(/^\/findings\/([^/]+)$/);
|
|
313
128
|
if (findingMatch) {
|
|
314
129
|
const id = decodeURIComponent(findingMatch[1]);
|
|
315
130
|
if (request.method === "GET") {
|
|
316
|
-
const
|
|
317
|
-
const finding = state.findings[id];
|
|
131
|
+
const finding = runs.finding(slug, id);
|
|
318
132
|
return finding
|
|
319
|
-
? json({ finding
|
|
133
|
+
? json({ finding })
|
|
320
134
|
: notFound(`finding '${id}' não encontrado`);
|
|
321
135
|
}
|
|
322
136
|
if (request.method === "PATCH" || request.method === "POST") {
|
|
323
|
-
|
|
137
|
+
const body = (await request.json().catch(() => ({}))) as FindingUpdate;
|
|
138
|
+
const finding = runs.update(slug, id, body);
|
|
139
|
+
return finding
|
|
140
|
+
? json({ finding })
|
|
141
|
+
: notFound(`finding '${id}' não encontrado`);
|
|
324
142
|
}
|
|
325
143
|
}
|
|
326
144
|
|
|
327
145
|
const artifactMatch = rest.match(/^\/artifacts\/(.+)$/);
|
|
328
146
|
if (artifactMatch && request.method === "GET") {
|
|
329
|
-
|
|
147
|
+
const name = decodeURIComponent(artifactMatch[1]);
|
|
148
|
+
const artifact = runs.artifact(slug, name);
|
|
149
|
+
if (!artifact) return notFound(`artefato '${name}' não encontrado`);
|
|
150
|
+
return text(artifact.content, artifact.type);
|
|
151
|
+
}
|
|
152
|
+
if (rest === "/artifacts" && request.method === "GET") {
|
|
153
|
+
return json({ artifacts: runs.artifacts(slug) ?? [] });
|
|
330
154
|
}
|
|
331
155
|
|
|
332
156
|
return notFound();
|
|
333
157
|
}
|
|
334
158
|
|
|
335
|
-
/**
|
|
336
|
-
export
|
|
159
|
+
/** Servidor local do houndsman (rotas + shell). */
|
|
160
|
+
export class HoundsmanServer {
|
|
161
|
+
private readonly runs: RunRepository;
|
|
162
|
+
private readonly ghRunner: GhRunner;
|
|
163
|
+
|
|
164
|
+
constructor(config: HoundsmanConfig, deps: ServerDeps = {}) {
|
|
165
|
+
this.runs = new RunRepository(
|
|
166
|
+
deps.storage ?? createStorage(config),
|
|
167
|
+
deps.repo ?? resolveRepoIdentity(),
|
|
168
|
+
);
|
|
169
|
+
this.ghRunner = deps.ghRunner ?? defaultGhRunner;
|
|
170
|
+
}
|
|
337
171
|
|
|
338
|
-
/**
|
|
339
|
-
|
|
340
|
-
config: HoundsmanConfig,
|
|
341
|
-
deps: ServerDeps = {},
|
|
342
|
-
): (request: Request) => Promise<Response> {
|
|
343
|
-
const artifactDir = resolveArtifactDir(config);
|
|
344
|
-
const ghRunner = deps.ghRunner ?? defaultGhRunner;
|
|
345
|
-
return async (request) => {
|
|
172
|
+
/** Handler de requisições (testável sem abrir porta). */
|
|
173
|
+
readonly handler = async (request: Request): Promise<Response> => {
|
|
346
174
|
try {
|
|
347
|
-
return await route(request,
|
|
175
|
+
return await route(request, this.runs, this.ghRunner);
|
|
348
176
|
} catch (error) {
|
|
349
177
|
return json(
|
|
350
178
|
{ error: error instanceof Error ? error.message : String(error) },
|
|
@@ -352,6 +180,40 @@ export function createRequestHandler(
|
|
|
352
180
|
);
|
|
353
181
|
}
|
|
354
182
|
};
|
|
183
|
+
|
|
184
|
+
/** Sobe o servidor e devolve a instância + URL. */
|
|
185
|
+
start(options: { port?: number; host?: string } = {}): {
|
|
186
|
+
server: BunServer;
|
|
187
|
+
url: string;
|
|
188
|
+
} {
|
|
189
|
+
// Manutenção do banco no start: compacta o que sobrou de runs antigas.
|
|
190
|
+
try {
|
|
191
|
+
this.runs.maintain();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.warn(
|
|
194
|
+
`[houndsman] VACUUM falhou (seguindo sem compactar): ${
|
|
195
|
+
error instanceof Error ? error.message : String(error)
|
|
196
|
+
}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const host = options.host ?? "127.0.0.1";
|
|
200
|
+
const port = options.port ?? 7331;
|
|
201
|
+
const server = Bun.serve({ hostname: host, port, fetch: this.handler });
|
|
202
|
+
return { server, url: `http://${host}:${server.port}` };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Fecha o provider (após `server.stop()`). */
|
|
206
|
+
dispose(): void {
|
|
207
|
+
this.runs.dispose();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Cria o handler de requisições (testável sem abrir porta). */
|
|
212
|
+
export function createRequestHandler(
|
|
213
|
+
config: HoundsmanConfig,
|
|
214
|
+
deps: ServerDeps = {},
|
|
215
|
+
): (request: Request) => Promise<Response> {
|
|
216
|
+
return new HoundsmanServer(config, deps).handler;
|
|
355
217
|
}
|
|
356
218
|
|
|
357
219
|
export type ServerOptions = {
|
|
@@ -360,18 +222,21 @@ export type ServerOptions = {
|
|
|
360
222
|
host?: string;
|
|
361
223
|
/** Runner do `gh` (injetável nos testes). */
|
|
362
224
|
ghRunner?: GhRunner;
|
|
225
|
+
/** Provider/storage injetável (testes). */
|
|
226
|
+
storage?: StorageProvider;
|
|
227
|
+
repo?: RepoIdentity;
|
|
363
228
|
};
|
|
364
229
|
|
|
365
230
|
/** Sobe o servidor e devolve a instância + URL. */
|
|
366
231
|
export function startServer(options: ServerOptions): { server: BunServer; url: string } {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
232
|
+
return new HoundsmanServer(options.config, {
|
|
233
|
+
ghRunner: options.ghRunner,
|
|
234
|
+
storage: options.storage,
|
|
235
|
+
repo: options.repo,
|
|
236
|
+
}).start({
|
|
237
|
+
port: options.port,
|
|
238
|
+
host: options.host,
|
|
373
239
|
});
|
|
374
|
-
return { server, url: `http://${host}:${server.port}` };
|
|
375
240
|
}
|
|
376
241
|
|
|
377
242
|
// ---------------------------------------------------------------------------
|
|
@@ -397,7 +262,7 @@ html, body { height:100%; }
|
|
|
397
262
|
body { margin:0; font-size:14px; background:var(--bg); color:var(--fg); font-family:var(--font-sans); -webkit-font-smoothing:antialiased; }
|
|
398
263
|
body::before { content:""; position:fixed; inset:0; pointer-events:none; z-index:0;
|
|
399
264
|
background:radial-gradient(820px 420px at 8% -12%, rgba(240,165,44,.08), transparent 62%); }
|
|
400
|
-
code, pre, .mono { font-family:var(--font-mono); }
|
|
265
|
+
code, pre, .mono { font-family:var(--font-mono); tab-size:4; }
|
|
401
266
|
::-webkit-scrollbar { width:10px; height:10px; }
|
|
402
267
|
::-webkit-scrollbar-thumb { background:rgba(255,255,255,.1); border:2px solid transparent; border-radius:999px; background-clip:padding-box; }
|
|
403
268
|
::-webkit-scrollbar-thumb:hover { background:rgba(255,255,255,.18); background-clip:padding-box; }
|
|
@@ -539,13 +404,35 @@ code, pre, .mono { font-family:var(--font-mono); }
|
|
|
539
404
|
.meta { display:inline-flex; align-items:center; gap:7px; }
|
|
540
405
|
.meta-k { font-size:10px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
|
|
541
406
|
.meta code { padding:2px 6px; font-size:11.5px; color:var(--fg); background:rgba(255,255,255,.04); border:1px solid var(--border); border-radius:6px; }
|
|
542
|
-
.action-block { display:flex; gap:11px; padding:
|
|
543
|
-
.action-icon { flex:none; display:inline-block; margin-
|
|
544
|
-
.action-
|
|
545
|
-
.action-
|
|
407
|
+
.action-block { display:flex; gap:11px; padding:0; background:transparent; border:0; border-radius:0; }
|
|
408
|
+
.action-icon { flex:none; display:inline-block; margin-top:3px; margin-right:2px; color:var(--accent); }
|
|
409
|
+
.action-text { flex:1; min-width:0; }
|
|
410
|
+
.action-label { display:block; margin-bottom:4px; font-size:10px; font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:var(--accent); }
|
|
411
|
+
.action-text p, .md-p { margin:0 0 6px; font-size:13px; line-height:1.65; color:#f0ece6; white-space:pre-wrap; }
|
|
412
|
+
.action-text p:last-child { margin-bottom:0; }
|
|
413
|
+
.md-list { margin:0 0 6px; padding-left:20px; font-size:13px; line-height:1.65; color:#f0ece6; }
|
|
414
|
+
.md-list li { margin:0 0 2px; }
|
|
415
|
+
.md-quote { margin:0 0 6px; padding:2px 0 2px 10px; border-left:2px solid var(--accent-line); color:var(--muted); font-size:13px; line-height:1.65; }
|
|
416
|
+
.action-text code, .md-code { font-family:var(--font-mono); font-size:11.5px; padding:1px 5px; color:#f0ece6; background:rgba(255,255,255,.06); border:1px solid var(--border); border-radius:5px; }
|
|
417
|
+
.action-text a { color:var(--accent-strong); text-decoration:underline; text-underline-offset:2px; }
|
|
418
|
+
.action-text strong { color:#fff; font-weight:600; }
|
|
419
|
+
.action-text em { color:#e9e4dc; }
|
|
420
|
+
.md-code-block { margin:6px 0; padding:10px 12px; max-height:320px; overflow:auto; white-space:pre; font-size:11.5px; line-height:1.6; color:#cdc7bf; background:#0b0b0e; border-radius:10px; }
|
|
546
421
|
.decision-tags-block { display:flex; align-items:center; gap:8px; }
|
|
547
422
|
.evidence { margin:0; padding: 16px; white-space:break-spaces; font-size:12px; line-height:1.6; color:#cdc7bf;
|
|
548
423
|
background:#0b0b0e; border:1px solid var(--border); border-radius:var(--radius); }
|
|
424
|
+
.evidence-block { display:flex; flex-direction:column; gap:6px; }
|
|
425
|
+
.evidence-head { display:flex; justify-content:flex-end; }
|
|
426
|
+
.code-lang { font-family:var(--font-mono); font-size:9.5px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
|
|
427
|
+
.code-evidence { white-space:pre; overflow:auto; }
|
|
428
|
+
.tok-keyword { color:#c792ea; }
|
|
429
|
+
.tok-string { color:#9ddc8a; }
|
|
430
|
+
.tok-comment { color:#6d6862; font-style:italic; }
|
|
431
|
+
.tok-number { color:#8fc7ff; }
|
|
432
|
+
.tok-type { color:var(--accent-strong); }
|
|
433
|
+
.tok-function { color:#82aaff; }
|
|
434
|
+
.tok-property { color:#e6c07b; }
|
|
435
|
+
.tok-punct { color:#8d8a85; }
|
|
549
436
|
.tags { display:flex; flex-wrap:wrap; align-items:center; gap:7px; margin-left: auto; }
|
|
550
437
|
.tags-label { display:inline-flex; align-items:center; gap:6px; font-size:10px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
|
|
551
438
|
.tag { display:inline-flex; align-items:center; gap:6px; padding:3px 9px; font-family:var(--font-mono); font-size:11px; color:var(--muted);
|
|
@@ -568,11 +455,34 @@ code, pre, .mono { font-family:var(--font-mono); }
|
|
|
568
455
|
font-family:var(--font-mono); font-size:11px; color:var(--muted); }
|
|
569
456
|
.comment-status.saved { color:var(--success); }
|
|
570
457
|
.artifacts { display:flex; flex-direction:column; }
|
|
571
|
-
.artifact-
|
|
572
|
-
.artifact-
|
|
573
|
-
.artifact-
|
|
574
|
-
.artifact-
|
|
458
|
+
.artifact-layout { display:grid; grid-template-columns:minmax(190px,250px) minmax(0,1fr); gap:16px; align-items:start; }
|
|
459
|
+
.artifact-list { display:flex; flex-direction:column; gap:14px; max-height:72vh; overflow:auto; padding:2px 4px 2px 0; }
|
|
460
|
+
.artifact-group { display:flex; flex-direction:column; gap:4px; }
|
|
461
|
+
.artifact-group-label { margin:0 0 2px; font-size:10px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); }
|
|
462
|
+
.artifact-item { display:flex; flex-direction:column; gap:2px; text-align:left; cursor:pointer; font:inherit; color:var(--muted);
|
|
463
|
+
padding:8px 10px; background:transparent; border:1px solid transparent; border-radius:10px;
|
|
464
|
+
transition:background .15s ease, border-color .15s ease, color .15s ease; }
|
|
465
|
+
.artifact-item:hover { color:var(--fg); background:var(--surface-2); border-color:var(--border); }
|
|
466
|
+
.artifact-item.active { color:var(--fg); background:var(--accent-soft); border-color:var(--accent-line); }
|
|
467
|
+
.artifact-item-name { font-family:var(--font-mono); font-size:11.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
468
|
+
.artifact-item-kind { font-size:9.5px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
|
|
469
|
+
.artifact-item.active .artifact-item-kind { color:var(--accent); }
|
|
470
|
+
.artifact-pane { min-width:0; }
|
|
471
|
+
.artifact-head { display:flex; align-items:flex-start; justify-content:space-between; gap:14px; margin-bottom:12px; }
|
|
472
|
+
.artifact-title { margin:0 0 5px; font-family:var(--font-mono); font-size:13px; font-weight:600; word-break:break-word; }
|
|
473
|
+
.artifact-desc { margin:0; max-width:78ch; color:var(--muted); font-size:12.5px; line-height:1.55; }
|
|
474
|
+
.artifact-kind-badge { flex:none; display:inline-flex; align-items:center; padding:3px 9px; font-size:10px; font-weight:700;
|
|
475
|
+
letter-spacing:.08em; text-transform:uppercase; color:var(--accent); background:var(--accent-soft);
|
|
476
|
+
border:1px solid var(--accent-line); border-radius:999px; }
|
|
477
|
+
.artifact-view { margin:0; padding:14px 16px; max-height:70vh; overflow:auto; font-size:12px; line-height:1.6; color:#cdc7bf;
|
|
575
478
|
background:#0b0b0e; border:1px solid var(--border); border-radius:var(--radius); }
|
|
479
|
+
.json-view { white-space:pre; }
|
|
480
|
+
.json-key { color:var(--accent-strong); }
|
|
481
|
+
.json-string { color:#9ddc8a; }
|
|
482
|
+
.json-number { color:#8fc7ff; }
|
|
483
|
+
.json-literal { color:#d7a6ff; }
|
|
484
|
+
.json-punct { color:#8d8a85; }
|
|
485
|
+
@media (max-width:900px) { .artifact-layout { grid-template-columns:1fr; } .artifact-list { max-height:240px; } }
|
|
576
486
|
.sent-tag { display:inline-flex; align-items:center; gap:5px; padding:2px 8px; font-size:10px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
|
|
577
487
|
color:var(--success); background:rgba(69,192,122,.12); border:1px solid rgba(69,192,122,.32); border-radius:999px; text-decoration:none; }
|
|
578
488
|
.sent-tag:hover { color:#fff; }
|