@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Área de trabalho (staging) de uma review — biblioteca.
|
|
3
|
+
*
|
|
4
|
+
* O banco é a fonte de verdade de reviews/findings/artefatos. Este diretório
|
|
5
|
+
* guarda só o que os subagentes leem e escrevem: briefs, findings brutos e o
|
|
6
|
+
* contexto da PR. Vive em `<HOUNDSMAN_HOME>/work/<reviewId>/` e é limpo após o
|
|
7
|
+
* `finalize`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
CONTEXT_BRIEF_FILE,
|
|
14
|
+
CONTEXT_FILE,
|
|
15
|
+
CONTEXT_SUMMARY_FILE,
|
|
16
|
+
displayPath,
|
|
17
|
+
} from "../paths";
|
|
18
|
+
import type { ContextPaths } from "../pr-context";
|
|
19
|
+
import {
|
|
20
|
+
type ContextSummary,
|
|
21
|
+
parseContextSummary,
|
|
22
|
+
parseWatchdogReport,
|
|
23
|
+
type WatchdogReport,
|
|
24
|
+
} from "../report-builder";
|
|
25
|
+
import { systemClock } from "../services/clock";
|
|
26
|
+
import { workRoot } from "./paths";
|
|
27
|
+
import type { ReviewRecord } from "./types";
|
|
28
|
+
|
|
29
|
+
export type { ContextPaths };
|
|
30
|
+
|
|
31
|
+
/** Resultado da leitura dos findings brutos de uma rodada. */
|
|
32
|
+
export type FindingsIngest = {
|
|
33
|
+
reports: WatchdogReport[];
|
|
34
|
+
pending: string[];
|
|
35
|
+
warnings: string[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Staging de uma review (briefs/findings/contexto). */
|
|
39
|
+
export class ReviewWorkArea {
|
|
40
|
+
readonly review: ReviewRecord;
|
|
41
|
+
readonly root: string;
|
|
42
|
+
|
|
43
|
+
constructor(review: ReviewRecord, base: string = workRoot()) {
|
|
44
|
+
this.review = review;
|
|
45
|
+
this.root = join(base, review.id);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get branch(): string {
|
|
49
|
+
return this.review.branch;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get slug(): string {
|
|
53
|
+
return this.review.slug;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get briefsDir(): string {
|
|
57
|
+
return join(this.root, "briefs");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get findingsDir(): string {
|
|
61
|
+
return join(this.root, "findings");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Caminho de um arquivo dentro da área de trabalho. */
|
|
65
|
+
file(name: string): string {
|
|
66
|
+
return join(this.root, name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
briefFile(id: string): string {
|
|
70
|
+
return join(this.briefsDir, `${id}.md`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
findingFile(id: string): string {
|
|
74
|
+
return join(this.findingsDir, `${id}.json`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get contextPaths(): ContextPaths {
|
|
78
|
+
return {
|
|
79
|
+
dir: this.root,
|
|
80
|
+
context: this.file(CONTEXT_FILE),
|
|
81
|
+
summary: this.file(CONTEXT_SUMMARY_FILE),
|
|
82
|
+
brief: this.file(CONTEXT_BRIEF_FILE),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Garante os diretórios; `clean` recria a área do zero. */
|
|
87
|
+
ensure(clean = false): void {
|
|
88
|
+
if (clean) this.clean();
|
|
89
|
+
mkdirSync(this.briefsDir, { recursive: true });
|
|
90
|
+
mkdirSync(this.findingsDir, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Remove todo o staging da review. */
|
|
94
|
+
clean(): void {
|
|
95
|
+
rmSync(this.root, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Remove só os findings brutos (equivalente ao antigo `--clean`). */
|
|
99
|
+
cleanFindings(): void {
|
|
100
|
+
rmSync(this.findingsDir, { recursive: true, force: true });
|
|
101
|
+
mkdirSync(this.findingsDir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
writeBrief(id: string, content: string): void {
|
|
105
|
+
mkdirSync(this.briefsDir, { recursive: true });
|
|
106
|
+
writeFileSync(this.briefFile(id), content);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
writeContext(context: unknown, brief: string): void {
|
|
110
|
+
mkdirSync(this.root, { recursive: true });
|
|
111
|
+
writeFileSync(this.contextPaths.context, `${JSON.stringify(context, null, 2)}\n`);
|
|
112
|
+
writeFileSync(this.contextPaths.brief, `${brief}\n`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Finds brutos gravados pelos subagentes (um JSON por watchdog). */
|
|
116
|
+
readFindings(activeIds: string[]): FindingsIngest {
|
|
117
|
+
const reports: WatchdogReport[] = [];
|
|
118
|
+
const pending: string[] = [];
|
|
119
|
+
const warnings: string[] = [];
|
|
120
|
+
for (const id of activeIds) {
|
|
121
|
+
const path = this.findingFile(id);
|
|
122
|
+
if (!existsSync(path)) {
|
|
123
|
+
pending.push(id);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
128
|
+
const report = parseWatchdogReport(raw, path, (message) =>
|
|
129
|
+
warnings.push(message),
|
|
130
|
+
);
|
|
131
|
+
if (report) reports.push(report);
|
|
132
|
+
else pending.push(id);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
warnings.push(
|
|
135
|
+
`[houndsman] ${displayPath(path)} inválido: ${err instanceof Error ? err.message : err}`,
|
|
136
|
+
);
|
|
137
|
+
pending.push(id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { reports, pending, warnings };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Resumo do contexto gravado pelo subagente (null se ausente/inválido). */
|
|
144
|
+
readContextSummary(): ContextSummary | null {
|
|
145
|
+
const path = this.contextPaths.summary;
|
|
146
|
+
if (!existsSync(path)) return null;
|
|
147
|
+
try {
|
|
148
|
+
return parseContextSummary(JSON.parse(readFileSync(path, "utf8")));
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Conteúdo cru do resumo (para persistir como artefato). */
|
|
155
|
+
readContextSummaryRaw(): string | null {
|
|
156
|
+
const path = this.contextPaths.summary;
|
|
157
|
+
if (!existsSync(path)) return null;
|
|
158
|
+
try {
|
|
159
|
+
return readFileSync(path, "utf8");
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Contexto bruto da PR (para persistir como artefato). */
|
|
166
|
+
readContextRaw(): string | null {
|
|
167
|
+
const path = this.contextPaths.context;
|
|
168
|
+
if (!existsSync(path)) return null;
|
|
169
|
+
try {
|
|
170
|
+
return readFileSync(path, "utf8");
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Brief do subagente de contexto (para persistir como artefato). */
|
|
177
|
+
readContextBriefRaw(): string | null {
|
|
178
|
+
const path = this.contextPaths.brief;
|
|
179
|
+
if (!existsSync(path)) return null;
|
|
180
|
+
try {
|
|
181
|
+
return readFileSync(path, "utf8");
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Toque de `updatedAt` (diagnóstico). */
|
|
188
|
+
touch(): string {
|
|
189
|
+
return systemClock();
|
|
190
|
+
}
|
|
191
|
+
}
|
package/src/core/verify.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type FindingsStateFile,
|
|
20
20
|
filterFindings,
|
|
21
21
|
} from "./findings-state";
|
|
22
|
+
import { systemClock } from "./services/clock";
|
|
22
23
|
import type { Severity } from "./types";
|
|
23
24
|
|
|
24
25
|
/** Resultado da verificação de um finding. */
|
|
@@ -160,7 +161,7 @@ export function summarizeVerify(entries: readonly VerifyEntry[]): VerifySummary
|
|
|
160
161
|
export function applyResolutions(
|
|
161
162
|
state: FindingsStateFile,
|
|
162
163
|
entries: readonly VerifyEntry[],
|
|
163
|
-
now: string =
|
|
164
|
+
now: string = systemClock(),
|
|
164
165
|
): string[] {
|
|
165
166
|
const changed: string[] = [];
|
|
166
167
|
for (const entry of entries) {
|
|
@@ -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,15 +5,18 @@
|
|
|
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,
|
|
15
17
|
HoundsmanConfig,
|
|
16
18
|
HoundsmanConfigInput,
|
|
19
|
+
HoundsmanStorage,
|
|
17
20
|
} from "./core/config";
|
|
18
21
|
export {
|
|
19
22
|
DEFAULT_ALWAYS_INCLUDE_DOCS,
|
|
@@ -36,6 +39,14 @@ export {
|
|
|
36
39
|
parseDiffIndex,
|
|
37
40
|
parseNumstat,
|
|
38
41
|
} from "./core/diff-source";
|
|
42
|
+
export { DocDiscoverer } from "./core/doc-discoverer";
|
|
43
|
+
export { Classification } from "./core/domain/classification";
|
|
44
|
+
export { Diff } from "./core/domain/diff";
|
|
45
|
+
export { Watchdog } from "./core/domain/watchdog";
|
|
46
|
+
export { buildCarryover, mergeCarryover } from "./core/engine/carryover";
|
|
47
|
+
export { HoundsmanEngine } from "./core/engine/houndsman-engine";
|
|
48
|
+
export { ReviewScopeResolver } from "./core/engine/review-scope";
|
|
49
|
+
export { ReviewSession } from "./core/engine/review-session";
|
|
39
50
|
export {
|
|
40
51
|
churnOf,
|
|
41
52
|
classifyEntries,
|
|
@@ -65,6 +76,7 @@ export type {
|
|
|
65
76
|
FindingDecision,
|
|
66
77
|
FindingExecState,
|
|
67
78
|
FindingHistoryEntry,
|
|
79
|
+
FindingIdResolution,
|
|
68
80
|
FindingState,
|
|
69
81
|
FindingsFilter,
|
|
70
82
|
FindingsStateFile,
|
|
@@ -75,34 +87,71 @@ export type {
|
|
|
75
87
|
export {
|
|
76
88
|
applyFindingUpdate,
|
|
77
89
|
buildStateFromFindings,
|
|
78
|
-
buildStateFromRunDir,
|
|
79
90
|
compareFindings,
|
|
80
91
|
emptyState,
|
|
81
92
|
FINDING_DECISIONS,
|
|
82
93
|
FINDING_EXEC_STATES,
|
|
83
94
|
FINDINGS_STATE_VERSION,
|
|
95
|
+
Finding,
|
|
96
|
+
FindingsRepository,
|
|
97
|
+
FindingsState,
|
|
84
98
|
FindingsStateStore,
|
|
85
99
|
fileReader,
|
|
86
100
|
filterFindings,
|
|
87
101
|
newFindingId,
|
|
88
102
|
reorder,
|
|
89
|
-
|
|
90
|
-
stateFile,
|
|
103
|
+
resolveFindingId,
|
|
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
|
-
ARTIFACT_DIR,
|
|
96
109
|
CORE_DIR,
|
|
97
110
|
CORE_SKILL_DIR,
|
|
111
|
+
displayPath,
|
|
98
112
|
OUTPUT_FORMAT_PATH,
|
|
99
113
|
PROJECT_DIR,
|
|
100
114
|
relFromRoot,
|
|
101
|
-
STATE_FILE,
|
|
102
115
|
} from "./core/paths";
|
|
116
|
+
export { ProjectConfig } from "./core/project-config";
|
|
103
117
|
export { checkRegistry } from "./core/registry-check";
|
|
118
|
+
export { BriefRenderer } from "./core/renderers/brief-renderer";
|
|
119
|
+
export { ManifestWriter } from "./core/renderers/manifest-writer";
|
|
120
|
+
export { ReportRenderer } from "./core/renderers/report-renderer";
|
|
104
121
|
export type { DomainReport } from "./core/report-builder";
|
|
105
122
|
export { renderDomainReport } from "./core/report-builder";
|
|
123
|
+
export type { Clock } from "./core/services/clock";
|
|
124
|
+
export { systemClock } from "./core/services/clock";
|
|
125
|
+
export { FileClassifier } from "./core/services/file-classifier";
|
|
126
|
+
export { FixBatchBuilder } from "./core/services/fix-batch-builder";
|
|
127
|
+
export type { GhResult, GhRunner } from "./core/services/gh-client";
|
|
128
|
+
export { GhClient } from "./core/services/gh-client";
|
|
129
|
+
export { GitRepository } from "./core/services/git-repository";
|
|
130
|
+
export { ProjectFiles } from "./core/services/project-files";
|
|
131
|
+
export { Verifier } from "./core/services/verifier";
|
|
132
|
+
export {
|
|
133
|
+
createMemoryStorage,
|
|
134
|
+
createStorage,
|
|
135
|
+
} from "./core/storage/factory";
|
|
136
|
+
export { FileStorageProvider } from "./core/storage/file/file-storage";
|
|
137
|
+
export {
|
|
138
|
+
buildRepoIdentity,
|
|
139
|
+
normalizeRemote,
|
|
140
|
+
resolveRepoIdentity,
|
|
141
|
+
} from "./core/storage/repo-identity";
|
|
142
|
+
export { SQLiteStorageProvider } from "./core/storage/sqlite/sqlite-storage";
|
|
143
|
+
export type {
|
|
144
|
+
ArtifactKind,
|
|
145
|
+
ArtifactRecord,
|
|
146
|
+
CreateReviewInput,
|
|
147
|
+
RepoIdentity,
|
|
148
|
+
ReviewPatch,
|
|
149
|
+
ReviewRecord,
|
|
150
|
+
ReviewStatus,
|
|
151
|
+
StorageProvider,
|
|
152
|
+
WriteArtifactInput,
|
|
153
|
+
} from "./core/storage/types";
|
|
154
|
+
export { ReviewWorkArea } from "./core/storage/work-area";
|
|
106
155
|
export type {
|
|
107
156
|
ChurnMap,
|
|
108
157
|
ClassifyRule,
|
|
@@ -127,6 +176,8 @@ export {
|
|
|
127
176
|
findWatchdog,
|
|
128
177
|
getWatchdog,
|
|
129
178
|
getWatchdogContext,
|
|
179
|
+
WatchdogRegistry,
|
|
130
180
|
} from "./core/watchdog-registry";
|
|
131
181
|
export type { ServerOptions } from "./server";
|
|
132
|
-
export { createRequestHandler, startServer } from "./server";
|
|
182
|
+
export { createRequestHandler, HoundsmanServer, startServer } from "./server";
|
|
183
|
+
export { RunRepository } from "./server/run-repository";
|
package/src/server/artifacts.ts
CHANGED
|
@@ -1,67 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Validação do slug da run (`branch-slug`) usado nas URLs do servidor.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Os dados vivem no storage global; a única preocupação restante aqui é barrar
|
|
5
|
+
* slugs malformados antes de consultar o provider.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
/** Slugs aceitos: letras, números, ponto, underscore e hífen. */
|
|
9
|
+
const SLUG_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"state.json",
|
|
14
|
-
"run.json",
|
|
15
|
-
"classification.json",
|
|
16
|
-
"review.md",
|
|
17
|
-
] as const;
|
|
18
|
-
|
|
19
|
-
/** Slugs (nomes de pasta) das runs sob o `artifactDir`. */
|
|
20
|
-
export function listRunSlugs(artifactDir: string): string[] {
|
|
21
|
-
if (!existsSync(artifactDir)) return [];
|
|
22
|
-
try {
|
|
23
|
-
return readdirSync(artifactDir, { withFileTypes: true })
|
|
24
|
-
.filter((entry) => entry.isDirectory())
|
|
25
|
-
.map((entry) => entry.name)
|
|
26
|
-
.filter((slug) =>
|
|
27
|
-
RUN_MARKERS.some((marker) => existsSync(join(artifactDir, slug, marker))),
|
|
28
|
-
)
|
|
29
|
-
.sort();
|
|
30
|
-
} catch {
|
|
31
|
-
return [];
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Diretório absoluto de uma run; null se o slug for inválido/inexistente. */
|
|
36
|
-
export function runDirForSlug(artifactDir: string, slug: string): string | null {
|
|
37
|
-
if (!/^[a-zA-Z0-9._-]+$/.test(slug)) return null;
|
|
38
|
-
const dir = join(artifactDir, slug);
|
|
39
|
-
if (!existsSync(dir) || !statSync(dir).isDirectory()) return null;
|
|
40
|
-
return dir;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Lê um arquivo DENTRO da run; null se ausente ou fora do diretório. */
|
|
44
|
-
export function readRunFile(runDir: string, name: string): string | null {
|
|
45
|
-
const root = resolve(runDir);
|
|
46
|
-
const target = resolve(root, name);
|
|
47
|
-
const rel = relative(root, target);
|
|
48
|
-
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return null;
|
|
49
|
-
try {
|
|
50
|
-
return existsSync(target) && statSync(target).isFile()
|
|
51
|
-
? readFileSync(target, "utf8")
|
|
52
|
-
: null;
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Lê e faz parse de um JSON da run (null se ausente/inválido). */
|
|
59
|
-
export function readRunJson<T>(runDir: string, name: string): T | null {
|
|
60
|
-
const raw = readRunFile(runDir, name);
|
|
61
|
-
if (raw === null) return null;
|
|
62
|
-
try {
|
|
63
|
-
return JSON.parse(raw) as T;
|
|
64
|
-
} catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
11
|
+
/** `true` se o slug é seguro/plausível. */
|
|
12
|
+
export function isSlug(value: string): boolean {
|
|
13
|
+
return SLUG_PATTERN.test(value);
|
|
67
14
|
}
|
|
@@ -1,48 +1,162 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Painel de artefatos:
|
|
2
|
+
* Painel de artefatos: lista todos os artefatos persistidos da run e renderiza
|
|
3
|
+
* o conteúdo bruto. JSON ganha destaque de sintaxe; briefs e relatórios são
|
|
4
|
+
* auditáveis pela mesma navegação.
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
|
-
import { useEffect, useState } from "preact/hooks";
|
|
6
|
-
import {
|
|
7
|
+
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
8
|
+
import { api } from "./api";
|
|
9
|
+
import {
|
|
10
|
+
ARTIFACT_KIND_LABELS,
|
|
11
|
+
describeArtifact,
|
|
12
|
+
displayArtifactName,
|
|
13
|
+
groupArtifacts,
|
|
14
|
+
isJsonArtifact,
|
|
15
|
+
} from "./constants";
|
|
16
|
+
import { formatJson, highlightJson } from "./json-highlight";
|
|
17
|
+
import type { ArtifactMeta } from "./types";
|
|
18
|
+
|
|
19
|
+
/** Prefere o relatório consolidado; senão, o primeiro artefato disponível. */
|
|
20
|
+
function pickDefault(items: ArtifactMeta[]): string {
|
|
21
|
+
return (items.find((item) => item.name === "review.md") ?? items[0])?.name ?? "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** JSON com destaque de sintaxe (tokens em spans, sem lib externa). */
|
|
25
|
+
function JsonView({ content }: { content: string }) {
|
|
26
|
+
const tokens = useMemo(() => highlightJson(formatJson(content)), [content]);
|
|
27
|
+
return (
|
|
28
|
+
<pre class="artifact-view json-view">
|
|
29
|
+
{tokens.map((token) => (
|
|
30
|
+
<span class={token.type === "plain" ? undefined : `json-${token.type}`}>
|
|
31
|
+
{token.text}
|
|
32
|
+
</span>
|
|
33
|
+
))}
|
|
34
|
+
</pre>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
7
37
|
|
|
8
38
|
export function ArtifactsPanel({ slug }: { slug: string }) {
|
|
9
|
-
const [
|
|
39
|
+
const [items, setItems] = useState<ArtifactMeta[]>([]);
|
|
40
|
+
const [name, setName] = useState("");
|
|
10
41
|
const [content, setContent] = useState("");
|
|
11
42
|
const [error, setError] = useState("");
|
|
43
|
+
const [loading, setLoading] = useState(false);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let cancelled = false;
|
|
47
|
+
setError("");
|
|
48
|
+
api<{ artifacts: ArtifactMeta[] }>(`/api/runs/${slug}/artifacts`)
|
|
49
|
+
.then(({ artifacts }) => {
|
|
50
|
+
if (cancelled) return;
|
|
51
|
+
setItems(artifacts);
|
|
52
|
+
setName((current) =>
|
|
53
|
+
artifacts.some((item) => item.name === current)
|
|
54
|
+
? current
|
|
55
|
+
: pickDefault(artifacts),
|
|
56
|
+
);
|
|
57
|
+
})
|
|
58
|
+
.catch((err) => {
|
|
59
|
+
if (cancelled) return;
|
|
60
|
+
setItems([]);
|
|
61
|
+
setName("");
|
|
62
|
+
setError(String(err));
|
|
63
|
+
});
|
|
64
|
+
return () => {
|
|
65
|
+
cancelled = true;
|
|
66
|
+
};
|
|
67
|
+
}, [slug]);
|
|
12
68
|
|
|
13
69
|
useEffect(() => {
|
|
70
|
+
if (!name) {
|
|
71
|
+
setContent("");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let cancelled = false;
|
|
75
|
+
setLoading(true);
|
|
14
76
|
setError("");
|
|
15
|
-
fetch(`/api/runs/${slug}/artifacts/${name}`)
|
|
77
|
+
fetch(`/api/runs/${slug}/artifacts/${encodeURI(name)}`)
|
|
16
78
|
.then(async (res) => {
|
|
17
79
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
18
|
-
|
|
80
|
+
const text = await res.text();
|
|
81
|
+
if (!cancelled) setContent(text);
|
|
19
82
|
})
|
|
20
83
|
.catch((err) => {
|
|
84
|
+
if (cancelled) return;
|
|
21
85
|
setContent("");
|
|
22
86
|
setError(String(err));
|
|
87
|
+
})
|
|
88
|
+
.finally(() => {
|
|
89
|
+
if (!cancelled) setLoading(false);
|
|
23
90
|
});
|
|
91
|
+
return () => {
|
|
92
|
+
cancelled = true;
|
|
93
|
+
};
|
|
24
94
|
}, [slug, name]);
|
|
25
95
|
|
|
96
|
+
const groups = useMemo(() => groupArtifacts(items), [items]);
|
|
97
|
+
const selected = useMemo(
|
|
98
|
+
() => items.find((item) => item.name === name),
|
|
99
|
+
[items, name],
|
|
100
|
+
);
|
|
101
|
+
|
|
26
102
|
return (
|
|
27
103
|
<div class="artifacts">
|
|
28
|
-
<div class="artifact-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
104
|
+
<div class="artifact-layout">
|
|
105
|
+
<nav class="artifact-list" aria-label="Artefatos da revisão">
|
|
106
|
+
{groups.map((group) => (
|
|
107
|
+
<div class="artifact-group" key={group.key}>
|
|
108
|
+
<p class="artifact-group-label">{group.label}</p>
|
|
109
|
+
{group.items.map((item) => (
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
key={item.name}
|
|
113
|
+
title={item.name}
|
|
114
|
+
class={`artifact-item${item.name === name ? " active" : ""}`}
|
|
115
|
+
onClick={() => setName(item.name)}
|
|
116
|
+
>
|
|
117
|
+
<span class="artifact-item-name">
|
|
118
|
+
{displayArtifactName(item)}
|
|
119
|
+
</span>
|
|
120
|
+
<span class="artifact-item-kind">
|
|
121
|
+
{ARTIFACT_KIND_LABELS[item.kind] ?? item.kind}
|
|
122
|
+
</span>
|
|
123
|
+
</button>
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
126
|
+
))}
|
|
127
|
+
{items.length === 0 && !loading && (
|
|
128
|
+
<p class="empty">Nenhum artefato persistido nesta run.</p>
|
|
129
|
+
)}
|
|
130
|
+
</nav>
|
|
131
|
+
|
|
132
|
+
<section class="artifact-pane">
|
|
133
|
+
{selected ? (
|
|
134
|
+
<header class="artifact-head">
|
|
135
|
+
<div>
|
|
136
|
+
<h3 class="artifact-title">{selected.name}</h3>
|
|
137
|
+
<p class="artifact-desc">{describeArtifact(selected)}</p>
|
|
138
|
+
</div>
|
|
139
|
+
<span class="artifact-kind-badge">
|
|
140
|
+
{ARTIFACT_KIND_LABELS[selected.kind] ?? selected.kind}
|
|
141
|
+
</span>
|
|
142
|
+
</header>
|
|
143
|
+
) : null}
|
|
144
|
+
|
|
145
|
+
{error ? (
|
|
146
|
+
<p class="error">{error}</p>
|
|
147
|
+
) : loading ? (
|
|
148
|
+
<p class="empty">Carregando artefato…</p>
|
|
149
|
+
) : selected ? (
|
|
150
|
+
isJsonArtifact(selected) ? (
|
|
151
|
+
<JsonView content={content} />
|
|
152
|
+
) : (
|
|
153
|
+
<pre class="artifact-view">{content || "—"}</pre>
|
|
154
|
+
)
|
|
155
|
+
) : (
|
|
156
|
+
<p class="empty">Selecione um artefato para visualizar.</p>
|
|
157
|
+
)}
|
|
158
|
+
</section>
|
|
39
159
|
</div>
|
|
40
|
-
<p class="artifact-desc">{ARTIFACT_INFO[name]}</p>
|
|
41
|
-
{error ? (
|
|
42
|
-
<p class="error">{error}</p>
|
|
43
|
-
) : (
|
|
44
|
-
<pre class="artifact-view">{content}</pre>
|
|
45
|
-
)}
|
|
46
160
|
</div>
|
|
47
161
|
);
|
|
48
162
|
}
|