@payconductor/houndsman 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -39
- package/package.json +1 -1
- package/src/cli/commands/context-command.ts +11 -9
- package/src/cli/commands/doctor-command.ts +5 -0
- package/src/cli/commands/findings-command.ts +13 -3
- package/src/cli/commands/review-command.ts +1 -2
- package/src/cli/commands/status-command.ts +46 -0
- package/src/cli/help.ts +8 -3
- package/src/cli/index.ts +5 -2
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +216 -125
- package/src/core/commands/review.ts +7 -8
- package/src/core/commands/scaffold.ts +15 -16
- package/src/core/commands/status.ts +63 -0
- package/src/core/config.ts +16 -7
- package/src/core/engine/houndsman-engine.ts +15 -1
- package/src/core/engine/review-scope.ts +26 -8
- package/src/core/engine/review-session.ts +37 -9
- package/src/core/findings-state.ts +53 -75
- package/src/core/github-review.ts +620 -57
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +271 -134
- package/src/core/paths.ts +26 -17
- package/src/core/pr-context.ts +19 -5
- package/src/core/project-config.ts +4 -5
- package/src/core/renderers/manifest-writer.ts +29 -18
- package/src/core/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/run-layout.ts +4 -104
- package/src/core/run-status.ts +59 -50
- package/src/core/services/git-repository.ts +16 -0
- package/src/core/services/project-files.ts +13 -1
- package/src/core/storage/factory.ts +24 -0
- package/src/core/storage/file/file-storage.ts +271 -0
- package/src/core/storage/paths.ts +64 -0
- package/src/core/storage/repo-identity.ts +70 -0
- package/src/core/storage/sqlite/mappers.ts +162 -0
- package/src/core/storage/sqlite/schema.ts +116 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +431 -0
- package/src/core/storage/types.ts +152 -0
- package/src/core/storage/work-area.ts +191 -0
- package/src/index.ts +27 -6
- package/src/server/artifacts.ts +8 -61
- package/src/server/client/App.tsx +26 -9
- package/src/server/client/ArtifactsPanel.tsx +144 -23
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +50 -15
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +181 -0
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +58 -10
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/utils/code-highlight.ts +422 -0
- package/src/server/client/utils/constants.ts +226 -0
- package/src/server/client/utils/json-highlight.ts +55 -0
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +81 -0
- package/src/server/index.ts +154 -16
- package/src/server/run-repository.ts +153 -136
- package/src/skill/OUTPUT-FORMAT.md +6 -4
- package/src/skill/README.md +22 -14
- package/src/skill/SETUP.md +4 -4
- package/src/skill/example-review.md +22 -14
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +55 -32
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +7 -6
- package/src/tests/__snapshots__/baseline.test.ts.snap +74 -61
- package/src/tests/baseline.test.ts +79 -34
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +1 -1
- package/src/tests/findings-commands.test.ts +153 -131
- package/src/tests/findings-state.test.ts +56 -5
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/infrastructure.test.ts +33 -38
- package/src/tests/markdown.test.ts +149 -0
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +81 -5
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +86 -23
- package/src/tests/server.test.ts +184 -96
- package/src/tests/storage.test.ts +449 -0
- package/src/server/client/constants.ts +0 -91
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contrato de persistência do houndsman (biblioteca).
|
|
3
|
+
*
|
|
4
|
+
* Tudo que hoje é "run" (reviews, findings, artefatos) passa por aqui. As
|
|
5
|
+
* implementações (`SQLiteStorageProvider` e `FileStorageProvider`) compartilham
|
|
6
|
+
* esta interface — trocar o backend é só trocar a config.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { FindingsStateFile } from "../findings-state";
|
|
10
|
+
|
|
11
|
+
/** Estados de execução de uma run. */
|
|
12
|
+
export type ReviewStatus = "reviewing" | "review_done" | "fixing" | "fix_done";
|
|
13
|
+
|
|
14
|
+
/** Identidade estável de um repositório (remote normalizado ou raiz git). */
|
|
15
|
+
export type RepoIdentity = {
|
|
16
|
+
/** Chave usada para escopar as runs (`remote:<...>` ou `path:<...>`). */
|
|
17
|
+
key: string;
|
|
18
|
+
/** Raiz absoluta do repositório no disco. */
|
|
19
|
+
root: string;
|
|
20
|
+
/** Remote `origin` cru, quando existir. */
|
|
21
|
+
remote?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Registro de uma revisão (run de uma branch). */
|
|
25
|
+
export type ReviewRecord = {
|
|
26
|
+
id: string;
|
|
27
|
+
repoKey: string;
|
|
28
|
+
repoRoot: string;
|
|
29
|
+
repoRemote?: string;
|
|
30
|
+
branch: string;
|
|
31
|
+
slug: string;
|
|
32
|
+
base?: string;
|
|
33
|
+
head?: string;
|
|
34
|
+
headSha?: string;
|
|
35
|
+
reviewedSha?: string;
|
|
36
|
+
reviewedAt?: string;
|
|
37
|
+
status: ReviewStatus | null;
|
|
38
|
+
startedAt?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Último sinal de vida de uma execução ativa (`reviewing`/`fixing`).
|
|
41
|
+
* Diferente de `updatedAt`: não muda com triagem/sync — só com a execução.
|
|
42
|
+
*/
|
|
43
|
+
heartbeatAt?: string;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
updatedAt: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Campos mutáveis de uma review (os demais são identidade). */
|
|
49
|
+
export type ReviewPatch = Partial<
|
|
50
|
+
Pick<
|
|
51
|
+
ReviewRecord,
|
|
52
|
+
| "branch"
|
|
53
|
+
| "slug"
|
|
54
|
+
| "base"
|
|
55
|
+
| "head"
|
|
56
|
+
| "headSha"
|
|
57
|
+
| "reviewedSha"
|
|
58
|
+
| "reviewedAt"
|
|
59
|
+
| "status"
|
|
60
|
+
| "startedAt"
|
|
61
|
+
| "heartbeatAt"
|
|
62
|
+
| "updatedAt"
|
|
63
|
+
>
|
|
64
|
+
> & {
|
|
65
|
+
/** Reinicia o `startedAt` no início de um novo ciclo de revisão. */
|
|
66
|
+
resetStartedAt?: boolean;
|
|
67
|
+
/** Limpa o heartbeat (execução terminou). */
|
|
68
|
+
resetHeartbeat?: boolean;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Entrada de `getOrCreateReview`. */
|
|
72
|
+
export type CreateReviewInput = {
|
|
73
|
+
repo: RepoIdentity;
|
|
74
|
+
branch: string;
|
|
75
|
+
slug: string;
|
|
76
|
+
base?: string;
|
|
77
|
+
head?: string;
|
|
78
|
+
now?: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Espécie de artefato (orienta a UI e o manifesto). */
|
|
82
|
+
export type ArtifactKind =
|
|
83
|
+
| "brief"
|
|
84
|
+
| "classification"
|
|
85
|
+
| "manifest"
|
|
86
|
+
| "candidates"
|
|
87
|
+
| "context"
|
|
88
|
+
| "context-brief"
|
|
89
|
+
| "context-summary"
|
|
90
|
+
| "finding-report"
|
|
91
|
+
| "review"
|
|
92
|
+
| "resume"
|
|
93
|
+
| "domain-review"
|
|
94
|
+
| "verify"
|
|
95
|
+
| "other";
|
|
96
|
+
|
|
97
|
+
/** Artefato persistido de uma review. */
|
|
98
|
+
export type ArtifactRecord = {
|
|
99
|
+
id: string;
|
|
100
|
+
reviewId: string;
|
|
101
|
+
name: string;
|
|
102
|
+
kind: ArtifactKind;
|
|
103
|
+
content: string;
|
|
104
|
+
contentType: string;
|
|
105
|
+
createdAt: string;
|
|
106
|
+
updatedAt: string;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Entrada de `writeArtifact`. */
|
|
110
|
+
export type WriteArtifactInput = {
|
|
111
|
+
name: string;
|
|
112
|
+
kind: ArtifactKind;
|
|
113
|
+
content: string;
|
|
114
|
+
contentType?: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Persistência síncrona de reviews, findings e artefatos.
|
|
119
|
+
*
|
|
120
|
+
* Síncrona de propósito: `bun:sqlite` e o filesystem são síncronos, e o motor
|
|
121
|
+
* inteiro já trabalha nesse modelo.
|
|
122
|
+
*/
|
|
123
|
+
export interface StorageProvider {
|
|
124
|
+
readonly kind: "sqlite" | "file";
|
|
125
|
+
|
|
126
|
+
/** Recupera ou cria a review de uma branch (identidade = repo + branch). */
|
|
127
|
+
getOrCreateReview(input: CreateReviewInput): ReviewRecord;
|
|
128
|
+
findReview(repoKey: string, branch: string): ReviewRecord | null;
|
|
129
|
+
getReview(id: string): ReviewRecord | null;
|
|
130
|
+
/** Reviews do repositório (mais recentes primeiro); sem chave = todas. */
|
|
131
|
+
listReviews(repoKey?: string): ReviewRecord[];
|
|
132
|
+
updateReview(id: string, patch: ReviewPatch): ReviewRecord | null;
|
|
133
|
+
deleteReview(id: string): void;
|
|
134
|
+
|
|
135
|
+
/** Estado de triagem completo; null se a review ainda não tem findings. */
|
|
136
|
+
readFindings(reviewId: string): FindingsStateFile | null;
|
|
137
|
+
/** Substitui o estado de triagem (transacional no SQLite). */
|
|
138
|
+
writeFindings(reviewId: string, state: FindingsStateFile): void;
|
|
139
|
+
|
|
140
|
+
readArtifact(reviewId: string, name: string): ArtifactRecord | null;
|
|
141
|
+
writeArtifact(reviewId: string, input: WriteArtifactInput): ArtifactRecord;
|
|
142
|
+
listArtifacts(reviewId: string): ArtifactRecord[];
|
|
143
|
+
deleteArtifact(reviewId: string, name: string): void;
|
|
144
|
+
|
|
145
|
+
/** Manutenção do backend (SQLite: `PRAGMA optimize` + `VACUUM`). */
|
|
146
|
+
maintain(): void;
|
|
147
|
+
|
|
148
|
+
/** Referência legível do armazenamento de uma review (diagnóstico/logs). */
|
|
149
|
+
location(reviewId: string): string;
|
|
150
|
+
|
|
151
|
+
close(): void;
|
|
152
|
+
}
|
|
@@ -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/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type {
|
|
|
16
16
|
HoundsmanConcurrency,
|
|
17
17
|
HoundsmanConfig,
|
|
18
18
|
HoundsmanConfigInput,
|
|
19
|
+
HoundsmanStorage,
|
|
19
20
|
} from "./core/config";
|
|
20
21
|
export {
|
|
21
22
|
DEFAULT_ALWAYS_INCLUDE_DOCS,
|
|
@@ -75,6 +76,7 @@ export type {
|
|
|
75
76
|
FindingDecision,
|
|
76
77
|
FindingExecState,
|
|
77
78
|
FindingHistoryEntry,
|
|
79
|
+
FindingIdResolution,
|
|
78
80
|
FindingState,
|
|
79
81
|
FindingsFilter,
|
|
80
82
|
FindingsStateFile,
|
|
@@ -85,7 +87,6 @@ export type {
|
|
|
85
87
|
export {
|
|
86
88
|
applyFindingUpdate,
|
|
87
89
|
buildStateFromFindings,
|
|
88
|
-
buildStateFromRunDir,
|
|
89
90
|
compareFindings,
|
|
90
91
|
emptyState,
|
|
91
92
|
FINDING_DECISIONS,
|
|
@@ -99,20 +100,18 @@ export {
|
|
|
99
100
|
filterFindings,
|
|
100
101
|
newFindingId,
|
|
101
102
|
reorder,
|
|
102
|
-
|
|
103
|
-
stateFile,
|
|
103
|
+
resolveFindingId,
|
|
104
104
|
summarize,
|
|
105
105
|
syncState,
|
|
106
106
|
} from "./core/findings-state";
|
|
107
107
|
export type { GitResult, GitRunner } from "./core/git-runner";
|
|
108
108
|
export {
|
|
109
|
-
ARTIFACT_DIR,
|
|
110
109
|
CORE_DIR,
|
|
111
110
|
CORE_SKILL_DIR,
|
|
111
|
+
displayPath,
|
|
112
112
|
OUTPUT_FORMAT_PATH,
|
|
113
113
|
PROJECT_DIR,
|
|
114
114
|
relFromRoot,
|
|
115
|
-
STATE_FILE,
|
|
116
115
|
} from "./core/paths";
|
|
117
116
|
export { ProjectConfig } from "./core/project-config";
|
|
118
117
|
export { checkRegistry } from "./core/registry-check";
|
|
@@ -121,7 +120,6 @@ export { ManifestWriter } from "./core/renderers/manifest-writer";
|
|
|
121
120
|
export { ReportRenderer } from "./core/renderers/report-renderer";
|
|
122
121
|
export type { DomainReport } from "./core/report-builder";
|
|
123
122
|
export { renderDomainReport } from "./core/report-builder";
|
|
124
|
-
export { RunLayout } from "./core/run-layout";
|
|
125
123
|
export type { Clock } from "./core/services/clock";
|
|
126
124
|
export { systemClock } from "./core/services/clock";
|
|
127
125
|
export { FileClassifier } from "./core/services/file-classifier";
|
|
@@ -131,6 +129,29 @@ export { GhClient } from "./core/services/gh-client";
|
|
|
131
129
|
export { GitRepository } from "./core/services/git-repository";
|
|
132
130
|
export { ProjectFiles } from "./core/services/project-files";
|
|
133
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";
|
|
134
155
|
export type {
|
|
135
156
|
ChurnMap,
|
|
136
157
|
ClassifyRule,
|
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,25 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* App: layout do servidor — sidebar de execuções, header fixo (abas segmentadas,
|
|
3
|
-
* auto-refresh e reancoragem), KPIs, filtros e os painéis de findings/
|
|
4
|
-
* Concentra o estado e as chamadas à API.
|
|
3
|
+
* auto-refresh e reancoragem), KPIs, filtros e os painéis de resumo/findings/
|
|
4
|
+
* artefatos. Concentra o estado e as chamadas à API.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
|
8
8
|
import { ArtifactsPanel } from "./ArtifactsPanel";
|
|
9
9
|
import { AutoRefresh } from "./AutoRefresh";
|
|
10
|
-
import { api, computeSummary } from "./api";
|
|
11
|
-
import { AUTO_REFRESH_MS, TAB_CAPTIONS } from "./constants";
|
|
12
10
|
import { Filters, type FiltersState } from "./Filters";
|
|
13
11
|
import { FindingsList } from "./FindingsList";
|
|
14
|
-
import { Layers, ListIcon, Refresh, Send } from "./icons";
|
|
12
|
+
import { FileText, Layers, ListIcon, Refresh, Send } from "./icons";
|
|
13
|
+
import { ResumePanel } from "./ResumePanel";
|
|
15
14
|
import { RunList } from "./RunList";
|
|
16
15
|
import { SubmitModal } from "./SubmitModal";
|
|
17
16
|
import { SummaryBar } from "./SummaryBar";
|
|
18
|
-
import
|
|
17
|
+
import { api, computeSummary } from "./utils/api";
|
|
18
|
+
import { AUTO_REFRESH_MS, TAB_CAPTIONS } from "./utils/constants";
|
|
19
|
+
import type { Finding, FindingsResponse, RunSummary } from "./utils/types";
|
|
19
20
|
|
|
20
|
-
type Tab = "findings" | "artifacts";
|
|
21
|
+
type Tab = "resume" | "findings" | "artifacts";
|
|
21
22
|
|
|
22
23
|
const TABS: { id: Tab; label: string; hint: string }[] = [
|
|
24
|
+
{
|
|
25
|
+
id: "resume",
|
|
26
|
+
label: "Resumo",
|
|
27
|
+
hint: "Visão principal da revisão: negócio, números e pontos técnicos",
|
|
28
|
+
},
|
|
23
29
|
{
|
|
24
30
|
id: "findings",
|
|
25
31
|
label: "Findings",
|
|
@@ -36,7 +42,7 @@ export function App() {
|
|
|
36
42
|
const [runs, setRuns] = useState<RunSummary[]>([]);
|
|
37
43
|
const [slug, setSlug] = useState<string | null>(null);
|
|
38
44
|
const [data, setData] = useState<FindingsResponse | null>(null);
|
|
39
|
-
const [tab, setTab] = useState<Tab>("
|
|
45
|
+
const [tab, setTab] = useState<Tab>("resume");
|
|
40
46
|
const [filters, setFilters] = useState<FiltersState>({
|
|
41
47
|
query: "",
|
|
42
48
|
decision: "all",
|
|
@@ -200,7 +206,9 @@ export function App() {
|
|
|
200
206
|
class={`seg-btn${tab === item.id ? " active" : ""}`}
|
|
201
207
|
onClick={() => setTab(item.id)}
|
|
202
208
|
>
|
|
203
|
-
{item.id === "
|
|
209
|
+
{item.id === "resume" ? (
|
|
210
|
+
<FileText size={15} />
|
|
211
|
+
) : item.id === "findings" ? (
|
|
204
212
|
<ListIcon size={15} />
|
|
205
213
|
) : (
|
|
206
214
|
<Layers size={15} />
|
|
@@ -254,6 +262,15 @@ export function App() {
|
|
|
254
262
|
</div>
|
|
255
263
|
{error && <p class="error">{error}</p>}
|
|
256
264
|
|
|
265
|
+
{slug && tab === "resume" && data && (
|
|
266
|
+
<ResumePanel
|
|
267
|
+
slug={slug}
|
|
268
|
+
branch={branch}
|
|
269
|
+
findings={data.findings}
|
|
270
|
+
summary={summary}
|
|
271
|
+
onOpenArtifacts={() => setTab("artifacts")}
|
|
272
|
+
/>
|
|
273
|
+
)}
|
|
257
274
|
{slug && tab === "findings" && data && (
|
|
258
275
|
<>
|
|
259
276
|
<SummaryBar summary={summary} />
|