@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
package/src/core/paths.ts
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* Caminhos do houndsman (biblioteca): raiz relativa para logs + layout fixo da
|
|
3
3
|
* camada de projeto (`.houndsman/`).
|
|
4
4
|
*
|
|
5
|
-
* A camada gerada tem local fixo, então nada aqui é configurável pelo projeto
|
|
6
|
-
*
|
|
5
|
+
* A camada gerada tem local fixo, então nada aqui é configurável pelo projeto.
|
|
6
|
+
* Dados de execução (reviews/findings/artefatos) vivem no storage global; aqui
|
|
7
|
+
* ficam só os paths da camada e os nomes dos artefatos de staging.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
import {
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { relative, sep } from "node:path";
|
|
10
12
|
import { OUTPUT_FORMAT_FILE } from "./assets";
|
|
11
13
|
|
|
12
14
|
/** Caminho relativo ao cwd, sempre com `/` (portável/legível em logs). */
|
|
@@ -14,6 +16,16 @@ export function relFromRoot(absPath: string): string {
|
|
|
14
16
|
return relative(process.cwd(), absPath).replace(/\\/g, "/");
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
/** Caminho legível para mensagens: `~/...` quando sob o home, senão absoluto. */
|
|
20
|
+
export function displayPath(absPath: string): string {
|
|
21
|
+
const home = homedir();
|
|
22
|
+
if (absPath === home) return "~";
|
|
23
|
+
if (absPath.startsWith(`${home}${sep}`)) {
|
|
24
|
+
return `~${absPath.slice(home.length).replace(/\\/g, "/")}`;
|
|
25
|
+
}
|
|
26
|
+
return absPath.replace(/\\/g, "/");
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
/** Diretório da camada de projeto (regras + artefatos). */
|
|
18
30
|
export const PROJECT_DIR = ".houndsman";
|
|
19
31
|
|
|
@@ -23,26 +35,23 @@ export const CORE_DIR = `${PROJECT_DIR}/core`;
|
|
|
23
35
|
/** Skill canônico (markdown) dentro da camada gerada. */
|
|
24
36
|
export const CORE_SKILL_DIR = `${CORE_DIR}/skill`;
|
|
25
37
|
|
|
26
|
-
/** Artefatos da revisão (briefs, findings, review.md) — default de `artifactDir`. */
|
|
27
|
-
export const ARTIFACT_DIR = `${PROJECT_DIR}/.runs`;
|
|
28
|
-
|
|
29
38
|
/** Schema de output dos watchdogs, citado nos briefs. */
|
|
30
39
|
export const OUTPUT_FORMAT_PATH = `${CORE_SKILL_DIR}/${OUTPUT_FORMAT_FILE}`;
|
|
31
40
|
|
|
32
|
-
/**
|
|
33
|
-
export const STATE_FILE = "state.json";
|
|
34
|
-
|
|
35
|
-
/** Status da run (revisão/correção), dentro da pasta da run (`<slug>/run.json`). */
|
|
36
|
-
export const RUN_STATUS_FILE = "run.json";
|
|
37
|
-
|
|
38
|
-
/** Relatório da verificação de findings, dentro da pasta da run (`<slug>/verify.md`). */
|
|
39
|
-
export const VERIFY_FILE = "verify.md";
|
|
40
|
-
|
|
41
|
-
/** Contexto bruto da PR (`gh pr view`), dentro da pasta da run. */
|
|
41
|
+
/** Contexto bruto da PR (`gh pr view`), no staging da review. */
|
|
42
42
|
export const CONTEXT_FILE = "context.json";
|
|
43
43
|
|
|
44
|
-
/** Resumo do contexto gravado pelo subagente,
|
|
44
|
+
/** Resumo do contexto gravado pelo subagente, no staging da review. */
|
|
45
45
|
export const CONTEXT_SUMMARY_FILE = "context-summary.json";
|
|
46
46
|
|
|
47
47
|
/** Brief do subagente que resume o contexto da PR. */
|
|
48
48
|
export const CONTEXT_BRIEF_FILE = "context-brief.md";
|
|
49
|
+
|
|
50
|
+
/** Relatório da verificação de findings (artefato `verify.md`). */
|
|
51
|
+
export const VERIFY_FILE = "verify.md";
|
|
52
|
+
|
|
53
|
+
/** Relatório consolidado da review (artefato `review.md`). */
|
|
54
|
+
export const REVIEW_FILE = "review.md";
|
|
55
|
+
|
|
56
|
+
/** Resumo final da review (artefato `review-resume.md`) — leitura humana apenas. */
|
|
57
|
+
export const REVIEW_RESUME_FILE = "review-resume.md";
|
package/src/core/pr-context.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* OPCIONAL: sem binário, sem login ou sem PR para a branch, grava
|
|
6
6
|
* `context.json` com `{ available: false, reason }` e sai com código 0.
|
|
7
7
|
*
|
|
8
|
-
* Artefatos
|
|
8
|
+
* Artefatos no staging da review (`<HOUNDSMAN_HOME>/work/<reviewId>/`):
|
|
9
9
|
* context.json payload bruto do `gh pr view` (ou marcador de indisponível)
|
|
10
10
|
* context-brief.md instruções para o subagente resumir o contexto
|
|
11
11
|
*
|
|
@@ -22,13 +22,19 @@ import {
|
|
|
22
22
|
} from "./paths";
|
|
23
23
|
import type { ContextSummary } from "./report-builder";
|
|
24
24
|
import { parseContextSummary } from "./report-builder";
|
|
25
|
-
import { type ContextPaths, RunLayout } from "./run-layout";
|
|
26
25
|
import { systemClock } from "./services/clock";
|
|
27
26
|
import { defaultGhRunner, GhClient, type GhRunner } from "./services/gh-client";
|
|
28
27
|
import { GitRepository } from "./services/git-repository";
|
|
29
28
|
|
|
30
29
|
export { CONTEXT_BRIEF_FILE, CONTEXT_FILE, CONTEXT_SUMMARY_FILE };
|
|
31
|
-
|
|
30
|
+
|
|
31
|
+
/** Caminhos dos artefatos de contexto da PR (staging da review). */
|
|
32
|
+
export type ContextPaths = {
|
|
33
|
+
dir: string;
|
|
34
|
+
context: string;
|
|
35
|
+
summary: string;
|
|
36
|
+
brief: string;
|
|
37
|
+
};
|
|
32
38
|
|
|
33
39
|
const GH_JSON_FIELDS =
|
|
34
40
|
"number,title,body,url,baseRefName,headRefName,author,commits,files";
|
|
@@ -60,8 +66,16 @@ export type PrContextRaw = {
|
|
|
60
66
|
};
|
|
61
67
|
|
|
62
68
|
/** Caminhos dos artefatos de contexto para uma branch. */
|
|
63
|
-
export function
|
|
64
|
-
|
|
69
|
+
export function contextFileNames(): {
|
|
70
|
+
context: string;
|
|
71
|
+
summary: string;
|
|
72
|
+
brief: string;
|
|
73
|
+
} {
|
|
74
|
+
return {
|
|
75
|
+
context: CONTEXT_FILE,
|
|
76
|
+
summary: CONTEXT_SUMMARY_FILE,
|
|
77
|
+
brief: CONTEXT_BRIEF_FILE,
|
|
78
|
+
};
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
/** Detecta a branch atual; lança se não houver (o CLI decide o exit). */
|
|
@@ -2,17 +2,16 @@
|
|
|
2
2
|
* Configuração do projeto (domínio) — resolve defaults e expõe o registry.
|
|
3
3
|
*
|
|
4
4
|
* Evita o "data clump" de passar `HoundsmanConfig` para dezenas de funções:
|
|
5
|
-
* quem precisa de
|
|
5
|
+
* quem precisa de orçamento/concorrência/watchdogs pede aqui.
|
|
6
6
|
* O contrato do consumidor (`houndsman.config.ts`) não muda.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { HoundsmanConfig } from "./config";
|
|
9
|
+
import type { HoundsmanConfig, HoundsmanStorage } from "./config";
|
|
10
10
|
import {
|
|
11
11
|
DEFAULT_ALWAYS_INCLUDE_DOCS,
|
|
12
12
|
DEFAULT_JEV_CONCURRENCY,
|
|
13
13
|
DEFAULT_MAX_FILES_PER_BRIEF,
|
|
14
14
|
} from "./defaults";
|
|
15
|
-
import { ARTIFACT_DIR } from "./paths";
|
|
16
15
|
import { WatchdogRegistry } from "./watchdog-registry";
|
|
17
16
|
|
|
18
17
|
/** Configuração resolvida do projeto. */
|
|
@@ -29,8 +28,8 @@ export class ProjectConfig<TId extends string = string> {
|
|
|
29
28
|
return this.raw.projectName;
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
get
|
|
33
|
-
return this.raw.
|
|
31
|
+
get storage(): HoundsmanStorage {
|
|
32
|
+
return this.raw.storage ?? {};
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
get maxFilesPerBrief(): number {
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Manifesto da run (biblioteca) — monta e grava o `manifest.json`.
|
|
2
|
+
* Manifesto da run (biblioteca) — monta e grava o artefato `manifest.json`.
|
|
3
3
|
*
|
|
4
4
|
* Concentra o resumo do que foi ativado (watchdogs, orçamento, churn, contexto,
|
|
5
|
-
* Jev e incremental)
|
|
5
|
+
* Jev e incremental). O manifesto vive no storage; os caminhos de briefs e
|
|
6
|
+
* findings apontam para o staging da review.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { Classification } from "../domain/classification";
|
|
9
10
|
import type { ClassificationResult } from "../file-classifier";
|
|
10
11
|
import type { JevCandidatesFile } from "../jev";
|
|
11
|
-
import { OUTPUT_FORMAT_PATH
|
|
12
|
-
import { readContextSummary } from "../pr-context";
|
|
12
|
+
import { OUTPUT_FORMAT_PATH } from "../paths";
|
|
13
13
|
import type { ProjectConfig } from "../project-config";
|
|
14
14
|
import type { MergedFinding } from "../report-builder";
|
|
15
|
-
import type { RunLayout } from "../run-layout";
|
|
16
15
|
import { systemClock } from "../services/clock";
|
|
17
|
-
import {
|
|
16
|
+
import type { ReviewRecord, StorageProvider } from "../storage/types";
|
|
17
|
+
import type { ReviewWorkArea } from "../storage/work-area";
|
|
18
18
|
|
|
19
19
|
/** Opções do manifesto (subset do `ReviewOptions`). */
|
|
20
20
|
export type ManifestOptions = {
|
|
@@ -32,12 +32,22 @@ export type ManifestClassification = ClassificationResult & {
|
|
|
32
32
|
|
|
33
33
|
/** Escreve o `manifest.json` de uma run. */
|
|
34
34
|
export class ManifestWriter {
|
|
35
|
-
private readonly
|
|
35
|
+
private readonly storage: StorageProvider;
|
|
36
|
+
private readonly review: ReviewRecord;
|
|
37
|
+
private readonly work: ReviewWorkArea;
|
|
36
38
|
private readonly project: ProjectConfig;
|
|
37
39
|
private readonly options: ManifestOptions;
|
|
38
40
|
|
|
39
|
-
constructor(
|
|
40
|
-
|
|
41
|
+
constructor(
|
|
42
|
+
storage: StorageProvider,
|
|
43
|
+
review: ReviewRecord,
|
|
44
|
+
work: ReviewWorkArea,
|
|
45
|
+
project: ProjectConfig,
|
|
46
|
+
options: ManifestOptions,
|
|
47
|
+
) {
|
|
48
|
+
this.storage = storage;
|
|
49
|
+
this.review = review;
|
|
50
|
+
this.work = work;
|
|
41
51
|
this.project = project;
|
|
42
52
|
this.options = options;
|
|
43
53
|
}
|
|
@@ -60,16 +70,16 @@ export class ManifestWriter {
|
|
|
60
70
|
name: this.project.watchdogs.nameOf(id),
|
|
61
71
|
files: selection.selected,
|
|
62
72
|
overflow: selection.overflow.length,
|
|
63
|
-
brief:
|
|
64
|
-
findings:
|
|
73
|
+
brief: this.work.briefFile(id),
|
|
74
|
+
findings: this.work.findingFile(id),
|
|
65
75
|
status: pending.includes(id) ? "pending" : "done",
|
|
66
76
|
};
|
|
67
77
|
});
|
|
68
78
|
|
|
69
|
-
const summary =
|
|
79
|
+
const summary = this.work.readContextSummary();
|
|
70
80
|
const manifest = {
|
|
71
|
-
branch: this.
|
|
72
|
-
slug: this.
|
|
81
|
+
branch: this.review.branch,
|
|
82
|
+
slug: this.review.slug,
|
|
73
83
|
base: this.options.base,
|
|
74
84
|
head: this.options.head,
|
|
75
85
|
headSha: classification.headSha,
|
|
@@ -104,9 +114,10 @@ export class ManifestWriter {
|
|
|
104
114
|
excludedCount: classification.excludedCount,
|
|
105
115
|
};
|
|
106
116
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
this.storage.writeArtifact(this.review.id, {
|
|
118
|
+
name: "manifest.json",
|
|
119
|
+
kind: "manifest",
|
|
120
|
+
content: `${JSON.stringify(manifest, null, 2)}\n`,
|
|
121
|
+
});
|
|
111
122
|
}
|
|
112
123
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resumo final da revisão (`review-resume.md`).
|
|
3
|
+
*
|
|
4
|
+
* Artefato somente-leitura para humanos: não entra em briefs, não é lido pelos
|
|
5
|
+
* watchdogs e não participa de nenhuma etapa seguinte. Une o contexto de
|
|
6
|
+
* negócio da PR aos números técnicos diretos da revisão, com limite de
|
|
7
|
+
* `RESUME_MAX_CHARS`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ContextSummary,
|
|
12
|
+
MergedFinding,
|
|
13
|
+
WatchdogNameResolver,
|
|
14
|
+
} from "../report-builder";
|
|
15
|
+
import { systemClock } from "../services/clock";
|
|
16
|
+
|
|
17
|
+
/** Teto de caracteres do artefato (inclui cabeçalho e quebras de linha). */
|
|
18
|
+
export const RESUME_MAX_CHARS = 2500;
|
|
19
|
+
|
|
20
|
+
/** Dados de classificação usados no resumo. */
|
|
21
|
+
export type ResumeClassification = {
|
|
22
|
+
files: Record<string, { status: string }>;
|
|
23
|
+
watchdogs: Record<string, string[]>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ReviewResumeInput = {
|
|
27
|
+
branch: string;
|
|
28
|
+
/** `YYYY-MM-DD`; default: data corrente do sistema. */
|
|
29
|
+
date?: string;
|
|
30
|
+
context?: ContextSummary | null;
|
|
31
|
+
classification: ResumeClassification;
|
|
32
|
+
findings: readonly MergedFinding[];
|
|
33
|
+
nameOf?: WatchdogNameResolver;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const identityName: WatchdogNameResolver = (id) => id;
|
|
37
|
+
|
|
38
|
+
const STATUS_BY_SEVERITY: Record<"blocking" | "attention" | "clean", string> = {
|
|
39
|
+
blocking: "❌ Mudanças necessárias",
|
|
40
|
+
attention: "⚠️ Aprovado com ressalvas",
|
|
41
|
+
clean: "✅ Aprovado",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Corta o texto em `max` caracteres, sinalizando com reticências. */
|
|
45
|
+
function truncate(value: string, max: number): string {
|
|
46
|
+
const text = value.trim().replace(/\s+/g, " ");
|
|
47
|
+
return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function plural(count: number, singular: string, plural: string): string {
|
|
51
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Resumo de negócio: contexto da PR; sem ele, o alcance da branch. */
|
|
55
|
+
function businessSection(input: ReviewResumeInput): string[] {
|
|
56
|
+
const context = input.context;
|
|
57
|
+
const hasContext =
|
|
58
|
+
context?.available === true &&
|
|
59
|
+
Boolean(
|
|
60
|
+
context.objective ||
|
|
61
|
+
context.motivation ||
|
|
62
|
+
(context.changeSummary && context.changeSummary.length > 0),
|
|
63
|
+
);
|
|
64
|
+
const lines = ["## Resumo de negócio", ""];
|
|
65
|
+
|
|
66
|
+
if (!hasContext) {
|
|
67
|
+
const domainCount = Object.keys(input.classification.watchdogs).length;
|
|
68
|
+
lines.push(
|
|
69
|
+
`A branch \`${input.branch}\` altera ${plural(
|
|
70
|
+
Object.keys(input.classification.files).length,
|
|
71
|
+
"arquivo",
|
|
72
|
+
"arquivos",
|
|
73
|
+
)} em ${plural(domainCount, "domínio", "domínios")} de revisão.`,
|
|
74
|
+
"Sem resumo de contexto da PR disponível.",
|
|
75
|
+
"",
|
|
76
|
+
);
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (context.objective) lines.push(context.objective, "");
|
|
81
|
+
if (context.motivation) lines.push(`**Por quê:** ${context.motivation}`, "");
|
|
82
|
+
if (context.changeSummary && context.changeSummary.length > 0) {
|
|
83
|
+
for (const item of context.changeSummary.slice(0, 4)) {
|
|
84
|
+
lines.push(`- ${truncate(item, 180)}`);
|
|
85
|
+
}
|
|
86
|
+
lines.push("");
|
|
87
|
+
}
|
|
88
|
+
return lines;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Achados que exigem ação, em formato direto (máximo 5). */
|
|
92
|
+
function technicalSection(
|
|
93
|
+
findings: readonly MergedFinding[],
|
|
94
|
+
nameOf: WatchdogNameResolver,
|
|
95
|
+
): string[] {
|
|
96
|
+
const relevant = findings
|
|
97
|
+
.filter(
|
|
98
|
+
(finding) =>
|
|
99
|
+
finding.severity === "blocking" || finding.severity === "attention",
|
|
100
|
+
)
|
|
101
|
+
.slice(0, 5);
|
|
102
|
+
const lines = ["## Pontos técnicos", ""];
|
|
103
|
+
if (relevant.length === 0) {
|
|
104
|
+
lines.push("- Nenhum bloqueio ou ponto de atenção.", "");
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
for (const finding of relevant) {
|
|
108
|
+
const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
|
|
109
|
+
const watchdogs = finding.watchdogs.map((id) => nameOf(id)).join(", ");
|
|
110
|
+
lines.push(
|
|
111
|
+
`- \`${location}\` — ${truncate(finding.message, 130)}${watchdogs ? ` _(${watchdogs})_` : ""}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
lines.push("");
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Garante o teto de caracteres cortando em uma quebra de linha. */
|
|
119
|
+
function clamp(markdown: string, max: number): string {
|
|
120
|
+
if (markdown.length <= max) return markdown;
|
|
121
|
+
const head = markdown.slice(0, max - 2);
|
|
122
|
+
const cut = head.lastIndexOf("\n");
|
|
123
|
+
return `${(cut > 0 ? head.slice(0, cut) : head).trimEnd()}\n…`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Renderiza o resumo final. Determinístico e sem chamadas externas: o texto
|
|
128
|
+
* nasce do contexto já disponível e dos findings consolidados.
|
|
129
|
+
*/
|
|
130
|
+
export function renderReviewResume(input: ReviewResumeInput): string {
|
|
131
|
+
const nameOf = input.nameOf ?? identityName;
|
|
132
|
+
const blockers = input.findings.filter((f) => f.severity === "blocking").length;
|
|
133
|
+
const attention = input.findings.filter((f) => f.severity === "attention").length;
|
|
134
|
+
const status =
|
|
135
|
+
blockers > 0
|
|
136
|
+
? STATUS_BY_SEVERITY.blocking
|
|
137
|
+
: attention > 0
|
|
138
|
+
? STATUS_BY_SEVERITY.attention
|
|
139
|
+
: STATUS_BY_SEVERITY.clean;
|
|
140
|
+
|
|
141
|
+
const lines = [
|
|
142
|
+
`# Resumo da revisão — ${input.branch}`,
|
|
143
|
+
"",
|
|
144
|
+
`**Data:** ${input.date ?? systemClock().slice(0, 10)} · **Status:** ${status}`,
|
|
145
|
+
"",
|
|
146
|
+
...businessSection(input),
|
|
147
|
+
...technicalSection(input.findings, nameOf),
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
return clamp(`${lines.join("\n").trimEnd()}\n`, RESUME_MAX_CHARS);
|
|
151
|
+
}
|
|
@@ -294,6 +294,7 @@ function renderConclusion(counts: Record<Severity, number>): string[] {
|
|
|
294
294
|
"",
|
|
295
295
|
`**Status:** ${status}`,
|
|
296
296
|
`**Resumo:** ${counts.blocking} violações bloqueantes, ${counts.attention} pontos de atenção, ${counts.suggestion} sugestões e ${counts.nit} nits.`,
|
|
297
|
+
"",
|
|
297
298
|
];
|
|
298
299
|
}
|
|
299
300
|
|
|
@@ -453,6 +454,12 @@ export function renderReport(
|
|
|
453
454
|
|
|
454
455
|
if (context) lines.push(...renderContext(context));
|
|
455
456
|
|
|
457
|
+
// Sumário primeiro: quem lê o review.md vê domínios, cobertura e conclusão
|
|
458
|
+
// antes dos pontos específicos.
|
|
459
|
+
lines.push(...renderDomainIndex(domains));
|
|
460
|
+
lines.push(...renderCoverage(reports, coverage, nameOf));
|
|
461
|
+
lines.push(...renderConclusion(counts));
|
|
462
|
+
|
|
456
463
|
lines.push(...renderFindingsSections(merged, nameOf));
|
|
457
464
|
|
|
458
465
|
if (merged.length === 0) {
|
|
@@ -463,9 +470,5 @@ export function renderReport(
|
|
|
463
470
|
lines.push(...renderUnconfirmedCandidates(candidates, merged));
|
|
464
471
|
}
|
|
465
472
|
|
|
466
|
-
lines.push(...renderDomainIndex(domains));
|
|
467
|
-
lines.push(...renderCoverage(reports, coverage, nameOf));
|
|
468
|
-
lines.push(...renderConclusion(counts));
|
|
469
|
-
|
|
470
473
|
return lines.join("\n");
|
|
471
474
|
}
|
package/src/core/run-layout.ts
CHANGED
|
@@ -1,108 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Removido: o layout de artefatos do projeto deu lugar ao staging global por
|
|
3
|
+
* review (`ReviewWorkArea` em `core/storage/work-area.ts`).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* motor: quem precisa de um caminho pede ao layout.
|
|
5
|
+
* Mantido apenas para não quebrar imports antigos de tipos.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
CONTEXT_BRIEF_FILE,
|
|
12
|
-
CONTEXT_FILE,
|
|
13
|
-
CONTEXT_SUMMARY_FILE,
|
|
14
|
-
RUN_STATUS_FILE,
|
|
15
|
-
STATE_FILE,
|
|
16
|
-
VERIFY_FILE,
|
|
17
|
-
} from "./paths";
|
|
18
|
-
import { sanitizeBranch } from "./report-builder";
|
|
19
|
-
|
|
20
|
-
/** Caminhos dos artefatos de contexto da PR. */
|
|
21
|
-
export type ContextPaths = {
|
|
22
|
-
dir: string;
|
|
23
|
-
context: string;
|
|
24
|
-
summary: string;
|
|
25
|
-
brief: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/** Diretório e caminhos de uma run (branch-slug). */
|
|
29
|
-
export class RunLayout {
|
|
30
|
-
readonly artifactDir: string;
|
|
31
|
-
readonly branch: string;
|
|
32
|
-
|
|
33
|
-
constructor(artifactDir: string, branch: string) {
|
|
34
|
-
this.artifactDir = artifactDir;
|
|
35
|
-
this.branch = branch;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
get slug(): string {
|
|
39
|
-
return sanitizeBranch(this.branch);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
get outDir(): string {
|
|
43
|
-
return join(this.artifactDir, this.slug);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
get briefsDir(): string {
|
|
47
|
-
return join(this.outDir, "briefs");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
get findingsDir(): string {
|
|
51
|
-
return join(this.outDir, "findings");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Caminho de um arquivo dentro da pasta da run. */
|
|
55
|
-
file(name: string): string {
|
|
56
|
-
return join(this.outDir, name);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
briefFile(id: string): string {
|
|
60
|
-
return join(this.briefsDir, `${id}.md`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
findingFile(id: string): string {
|
|
64
|
-
return join(this.findingsDir, `${id}.json`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
get classificationFile(): string {
|
|
68
|
-
return this.file("classification.json");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
get manifestFile(): string {
|
|
72
|
-
return this.file("manifest.json");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
get candidatesFile(): string {
|
|
76
|
-
return this.file("candidates.json");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
get reviewFile(): string {
|
|
80
|
-
return this.file("review.md");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
get stateFile(): string {
|
|
84
|
-
return this.file(STATE_FILE);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
get runStatusFile(): string {
|
|
88
|
-
return this.file(RUN_STATUS_FILE);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
get verifyFile(): string {
|
|
92
|
-
return this.file(VERIFY_FILE);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Relatório isolado de um watchdog (`review.<id>.md`). */
|
|
96
|
-
domainReportFile(id: string): string {
|
|
97
|
-
return this.file(`review.${sanitizeBranch(id)}.md`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
get contextPaths(): ContextPaths {
|
|
101
|
-
return {
|
|
102
|
-
dir: this.outDir,
|
|
103
|
-
context: this.file(CONTEXT_FILE),
|
|
104
|
-
summary: this.file(CONTEXT_SUMMARY_FILE),
|
|
105
|
-
brief: this.file(CONTEXT_BRIEF_FILE),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
8
|
+
export type { ContextPaths } from "./storage/work-area";
|