@payconductor/houndsman 0.7.0 → 0.10.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 +20 -0
- package/package.json +1 -1
- package/src/cli/commands/command.ts +10 -1
- package/src/cli/commands/context-command.ts +40 -2
- package/src/cli/commands/hunt-command.ts +28 -2
- package/src/cli/commands/review-command.ts +30 -2
- package/src/cli/commands/worktree-command.ts +118 -0
- package/src/cli/help.ts +12 -2
- package/src/cli/index.ts +14 -4
- package/src/core/ai/ai-client.ts +21 -0
- package/src/core/ai/ai-sdk-model-provider.ts +43 -18
- package/src/core/ai/features/technical-summarizer.ts +43 -0
- package/src/core/ai/index.ts +4 -1
- package/src/core/ai/prompts/context.ts +3 -3
- package/src/core/ai/prompts/technical.ts +30 -0
- package/src/core/ai/provider-capabilities.ts +45 -0
- package/src/core/ai/schemas.ts +19 -3
- package/src/core/ai/types.ts +3 -1
- package/src/core/ast/service.ts +7 -2
- package/src/core/brief.ts +26 -4
- package/src/core/commands/context.ts +9 -1
- package/src/core/commands/findings.ts +11 -5
- package/src/core/commands/review.ts +13 -0
- package/src/core/commands/scaffold.ts +9 -2
- package/src/core/config.ts +30 -1
- package/src/core/defaults.ts +7 -0
- package/src/core/engine/houndsman-engine.ts +26 -3
- package/src/core/engine/review-scope.ts +17 -22
- package/src/core/engine/review-session.ts +25 -2
- package/src/core/engine/review-workspace.ts +204 -0
- package/src/core/git-runner.ts +8 -1
- package/src/core/jev/jev-detector.ts +10 -2
- package/src/core/orchestrator.ts +176 -15
- package/src/core/paths.ts +3 -0
- package/src/core/pr-context.ts +11 -6
- package/src/core/project-config.ts +21 -1
- package/src/core/renderers/resume-renderer.ts +17 -34
- package/src/core/report-builder.ts +6 -6
- package/src/core/services/git-repository.ts +46 -0
- package/src/core/services/pr-resolver.ts +147 -0
- package/src/core/services/worktree-manager.ts +293 -0
- package/src/core/storage/file/file-storage.ts +14 -1
- package/src/core/storage/paths.ts +30 -1
- package/src/core/storage/sqlite/mappers.ts +4 -0
- package/src/core/storage/sqlite/schema.ts +13 -3
- package/src/core/storage/sqlite/sqlite-storage.ts +28 -8
- package/src/core/storage/types.ts +13 -0
- package/src/core/storage/work-area.ts +20 -1
- package/src/core/technical-summary.ts +398 -0
- package/src/core/types.ts +2 -0
- package/src/harness/agent/agent-factory.ts +1 -0
- package/src/harness/agent/review-agent.ts +11 -1
- package/src/harness/config/harness-config.ts +6 -1
- package/src/harness/hunt/hunt-options.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +7 -20
- package/src/index.ts +33 -0
- package/src/server/client/App.tsx +51 -12
- package/src/server/client/DeleteRunModal.tsx +163 -0
- package/src/server/client/PrsSection.tsx +296 -0
- package/src/server/client/ResumePanel.tsx +17 -20
- package/src/server/client/RunsSection.tsx +73 -0
- package/src/server/client/Sidebar.tsx +171 -358
- package/src/server/client/icons.tsx +10 -0
- package/src/server/client/sidebar-sections.tsx +73 -0
- package/src/server/client/utils/constants.ts +7 -2
- package/src/server/client/utils/sidebar-prefs.ts +64 -0
- package/src/server/client/utils/types.ts +27 -0
- package/src/server/index.ts +113 -406
- package/src/server/pr-inbox.ts +15 -4
- package/src/server/run-repository.ts +103 -2
- package/src/server/styles.css +454 -0
- package/src/skill/README.md +11 -3
- package/src/skill/SKILL.md +44 -10
- package/src/skill/hunt.md +33 -7
- package/src/tests/__snapshots__/baseline.test.ts.snap +99 -24
- package/src/tests/ai-core.test.ts +71 -0
- package/src/tests/ai-features.test.ts +36 -0
- package/src/tests/baseline.test.ts +4 -1
- package/src/tests/cli.test.ts +2 -1
- package/src/tests/harness-agent.test.ts +19 -0
- package/src/tests/review-resume.test.ts +36 -8
- package/src/tests/review-scope.test.ts +67 -0
- package/src/tests/review-scripts.test.ts +2 -2
- package/src/tests/server.test.ts +54 -1
- package/src/tests/sidebar-sections.test.ts +94 -0
- package/src/tests/technical-summary.test.ts +176 -0
- package/src/tests/worktree.test.ts +404 -0
- package/assets/logo-legacy.png +0 -0
package/src/tests/server.test.ts
CHANGED
|
@@ -111,6 +111,44 @@ describe("server API", () => {
|
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
test("DELETE /api/runs/:slug apaga review, findings, artefatos e staging", async () => {
|
|
115
|
+
const env = makeEnv();
|
|
116
|
+
try {
|
|
117
|
+
const handler = createRequestHandler(env.config, env);
|
|
118
|
+
const res = await handler(
|
|
119
|
+
new Request("http://localhost/api/runs/main", { method: "DELETE" }),
|
|
120
|
+
);
|
|
121
|
+
expect(res.status).toBe(200);
|
|
122
|
+
const body = (await res.json()) as { deleted: boolean };
|
|
123
|
+
expect(body.deleted).toBe(true);
|
|
124
|
+
|
|
125
|
+
expect(env.storage.getReview(env.reviewId)).toBeNull();
|
|
126
|
+
expect(env.storage.readFindings(env.reviewId)).toBeNull();
|
|
127
|
+
expect(env.storage.listArtifacts(env.reviewId)).toHaveLength(0);
|
|
128
|
+
|
|
129
|
+
const list = await get(handler, "/api/runs");
|
|
130
|
+
const after = (await list.json()) as { runs: unknown[] };
|
|
131
|
+
expect(after.runs).toHaveLength(0);
|
|
132
|
+
} finally {
|
|
133
|
+
env.cleanup();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("DELETE /api/runs/:slug inexistente devolve 404", async () => {
|
|
138
|
+
const env = makeEnv();
|
|
139
|
+
try {
|
|
140
|
+
const handler = createRequestHandler(env.config, env);
|
|
141
|
+
const res = await handler(
|
|
142
|
+
new Request("http://localhost/api/runs/nao-existe", {
|
|
143
|
+
method: "DELETE",
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
expect(res.status).toBe(404);
|
|
147
|
+
} finally {
|
|
148
|
+
env.cleanup();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
114
152
|
test("GET findings reconstrói do artefato e PATCH atualiza o estado", async () => {
|
|
115
153
|
const env = makeEnv();
|
|
116
154
|
try {
|
|
@@ -398,7 +436,19 @@ describe("server API", () => {
|
|
|
398
436
|
try {
|
|
399
437
|
const handler = createRequestHandler(env.config, env);
|
|
400
438
|
const shell = await get(handler, "/");
|
|
401
|
-
|
|
439
|
+
const html = await shell.text();
|
|
440
|
+
expect(html).toContain('id="app"');
|
|
441
|
+
expect(html).toContain("/assets/styles.css?v=");
|
|
442
|
+
|
|
443
|
+
// O CSS vive em `styles.css`, servido como arquivo real.
|
|
444
|
+
const css = await get(handler, "/assets/styles.css");
|
|
445
|
+
expect(css.status).toBe(200);
|
|
446
|
+
expect(css.headers.get("content-type")).toContain("text/css");
|
|
447
|
+
const styles = await css.text();
|
|
448
|
+
// Sidebar: rail vertical de seções + painel redimensionável.
|
|
449
|
+
expect(styles).toContain(".side-rail");
|
|
450
|
+
expect(styles).toContain("--side-width");
|
|
451
|
+
expect(styles).not.toContain(".side-tabs");
|
|
402
452
|
|
|
403
453
|
const bundle = await get(handler, "/assets/app.js");
|
|
404
454
|
expect(bundle.status).toBe(200);
|
|
@@ -420,6 +470,9 @@ describe("server API", () => {
|
|
|
420
470
|
|
|
421
471
|
const bundle = await get(handler, "/assets/app.js");
|
|
422
472
|
expect(bundle.headers.get("cache-control")).toContain("no-store");
|
|
473
|
+
|
|
474
|
+
const css = await get(handler, "/assets/styles.css");
|
|
475
|
+
expect(css.headers.get("cache-control")).toContain("no-store");
|
|
423
476
|
} finally {
|
|
424
477
|
env.cleanup();
|
|
425
478
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navegação da sidebar: registry de seções e preferências de layout.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_SIDEBAR_SECTION,
|
|
8
|
+
getSidebarSection,
|
|
9
|
+
resolveSidebarSectionId,
|
|
10
|
+
SIDEBAR_SECTIONS,
|
|
11
|
+
type SidebarSectionContext,
|
|
12
|
+
} from "../server/client/sidebar-sections";
|
|
13
|
+
import {
|
|
14
|
+
clampSidebarWidth,
|
|
15
|
+
readSidebarPrefs,
|
|
16
|
+
SIDEBAR_DEFAULT_WIDTH,
|
|
17
|
+
SIDEBAR_MAX_WIDTH,
|
|
18
|
+
SIDEBAR_MIN_WIDTH,
|
|
19
|
+
} from "../server/client/utils/sidebar-prefs";
|
|
20
|
+
import type { RunSummary } from "../server/client/utils/types";
|
|
21
|
+
|
|
22
|
+
function makeRun(slug: string): RunSummary {
|
|
23
|
+
return {
|
|
24
|
+
slug,
|
|
25
|
+
branch: slug,
|
|
26
|
+
summary: {
|
|
27
|
+
total: 0,
|
|
28
|
+
present: 0,
|
|
29
|
+
byDecision: { pending: 0, execute: 0, ignore: 0 },
|
|
30
|
+
byExecState: { pending: 0, in_progress: 0, done: 0, stale: 0 },
|
|
31
|
+
bySeverity: { blocking: 0, attention: 0, suggestion: 0, nit: 0 },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeContext(runs: RunSummary[]): SidebarSectionContext {
|
|
37
|
+
return {
|
|
38
|
+
runs,
|
|
39
|
+
slug: null,
|
|
40
|
+
onSelect: () => undefined,
|
|
41
|
+
onRefreshRuns: () => undefined,
|
|
42
|
+
active: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("SIDEBAR_SECTIONS", () => {
|
|
47
|
+
test("expõe Execuções e PRs na ordem canônica", () => {
|
|
48
|
+
expect(SIDEBAR_SECTIONS.map((section) => section.id)).toEqual(["runs", "prs"]);
|
|
49
|
+
expect(DEFAULT_SIDEBAR_SECTION).toBe("runs");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("cada seção tem identificação e conteúdo", () => {
|
|
53
|
+
const ids = new Set<string>();
|
|
54
|
+
for (const section of SIDEBAR_SECTIONS) {
|
|
55
|
+
expect(section.id).not.toBe("");
|
|
56
|
+
expect(ids.has(section.id)).toBe(false);
|
|
57
|
+
ids.add(section.id);
|
|
58
|
+
expect(section.label.length).toBeGreaterThan(0);
|
|
59
|
+
expect(section.title.length).toBeGreaterThan(0);
|
|
60
|
+
expect(typeof section.icon).toBe("function");
|
|
61
|
+
expect(typeof section.Content).toBe("function");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("o badge de Execuções reflete a contagem de runs", () => {
|
|
66
|
+
const runs = SIDEBAR_SECTIONS.find((section) => section.id === "runs");
|
|
67
|
+
expect(runs?.badge?.(makeContext([]))).toBeNull();
|
|
68
|
+
expect(runs?.badge?.(makeContext([makeRun("a"), makeRun("b")]))).toBe(2);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("resolve ids salvos e cai no default quando inválidos", () => {
|
|
72
|
+
expect(resolveSidebarSectionId("prs")).toBe("prs");
|
|
73
|
+
expect(resolveSidebarSectionId("desconhecida")).toBe(DEFAULT_SIDEBAR_SECTION);
|
|
74
|
+
expect(resolveSidebarSectionId(null)).toBe(DEFAULT_SIDEBAR_SECTION);
|
|
75
|
+
expect(getSidebarSection("desconhecida").id).toBe(DEFAULT_SIDEBAR_SECTION);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("preferências da sidebar", () => {
|
|
80
|
+
test("clampa a largura entre mínimo e máximo", () => {
|
|
81
|
+
expect(clampSidebarWidth(200)).toBe(SIDEBAR_MIN_WIDTH);
|
|
82
|
+
expect(clampSidebarWidth(900)).toBe(SIDEBAR_MAX_WIDTH);
|
|
83
|
+
expect(clampSidebarWidth(400)).toBe(400);
|
|
84
|
+
expect(clampSidebarWidth(Number.NaN)).toBe(SIDEBAR_DEFAULT_WIDTH);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("lê defaults sem preferências salvas", () => {
|
|
88
|
+
const prefs = readSidebarPrefs();
|
|
89
|
+
expect(prefs.width).toBeGreaterThanOrEqual(SIDEBAR_MIN_WIDTH);
|
|
90
|
+
expect(prefs.width).toBeLessThanOrEqual(SIDEBAR_MAX_WIDTH);
|
|
91
|
+
expect(typeof prefs.collapsed).toBe("boolean");
|
|
92
|
+
if (prefs.section !== null) expect(typeof prefs.section).toBe("string");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pontos técnicos estruturais do resumo final — builder determinístico e
|
|
3
|
+
* validação do artefato `technical-summary.json`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import type { AstAnalysis, AstFacts, AstImpact } from "../core/ast/types";
|
|
8
|
+
import { buildTechnicalSummary, parseTechnicalSummary } from "../core/technical-summary";
|
|
9
|
+
|
|
10
|
+
const facts: AstFacts = {
|
|
11
|
+
language: "typescript",
|
|
12
|
+
symbols: [],
|
|
13
|
+
imports: [],
|
|
14
|
+
exports: [],
|
|
15
|
+
decorators: [],
|
|
16
|
+
hasSyntaxErrors: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function analysis(partial: Partial<AstAnalysis> = {}): AstAnalysis {
|
|
20
|
+
return {
|
|
21
|
+
facts,
|
|
22
|
+
delta: {
|
|
23
|
+
addedSymbols: [],
|
|
24
|
+
removedSymbols: [],
|
|
25
|
+
modifiedSymbols: [],
|
|
26
|
+
changedSignatures: [],
|
|
27
|
+
addedImports: [],
|
|
28
|
+
removedImports: [],
|
|
29
|
+
changedExports: [],
|
|
30
|
+
},
|
|
31
|
+
...partial,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const classification = {
|
|
36
|
+
files: {
|
|
37
|
+
"src/bin-lookup/service.ts": { status: "added" },
|
|
38
|
+
"src/bin-lookup/provider.ts": { status: "added" },
|
|
39
|
+
},
|
|
40
|
+
churn: {
|
|
41
|
+
"src/bin-lookup/service.ts": { additions: 10, deletions: 0 },
|
|
42
|
+
"src/bin-lookup/provider.ts": { additions: 5, deletions: 0 },
|
|
43
|
+
},
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
describe("buildTechnicalSummary", () => {
|
|
47
|
+
test("extrai contratos, implementações, assinaturas e dependências do AST", () => {
|
|
48
|
+
const ast = new Map<string, AstAnalysis>([
|
|
49
|
+
[
|
|
50
|
+
"src/bin-lookup/service.ts",
|
|
51
|
+
analysis({
|
|
52
|
+
delta: {
|
|
53
|
+
addedSymbols: [
|
|
54
|
+
{
|
|
55
|
+
name: "BinLookupProvider",
|
|
56
|
+
kind: "interface",
|
|
57
|
+
exported: true,
|
|
58
|
+
startLine: 1,
|
|
59
|
+
endLine: 3,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "BinLookupService",
|
|
63
|
+
kind: "class",
|
|
64
|
+
exported: true,
|
|
65
|
+
startLine: 5,
|
|
66
|
+
endLine: 20,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
removedSymbols: [],
|
|
70
|
+
modifiedSymbols: [],
|
|
71
|
+
changedSignatures: [
|
|
72
|
+
{
|
|
73
|
+
name: "lookup",
|
|
74
|
+
before: "(bin: string)",
|
|
75
|
+
after: "(bin: string, brand: string)",
|
|
76
|
+
exported: true,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
addedImports: ["./provider", "axios"],
|
|
80
|
+
removedImports: [],
|
|
81
|
+
changedExports: [{ name: "legacy", change: "removed" }],
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
],
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const summary = buildTechnicalSummary({
|
|
88
|
+
classification,
|
|
89
|
+
ast,
|
|
90
|
+
headSha: "abc",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(summary.source).toBe("deterministic");
|
|
94
|
+
expect(summary.headSha).toBe("abc");
|
|
95
|
+
const text = summary.highlights.map((item) => `${item.area}: ${item.detail}`);
|
|
96
|
+
expect(text.join("\n")).toContain("Contratos: `BinLookupProvider`");
|
|
97
|
+
expect(text.join("\n")).toContain("Implementações: `BinLookupService`");
|
|
98
|
+
expect(text.join("\n")).toContain("Assinaturas alteradas: `lookup`");
|
|
99
|
+
expect(text.join("\n")).toContain("Novas dependências: `axios`");
|
|
100
|
+
expect(text.join("\n")).not.toContain("./provider");
|
|
101
|
+
expect(text.join("\n")).toContain("Exports alterados: `legacy`");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("sem AST, agrupa arquivos novos por diretório", () => {
|
|
105
|
+
const summary = buildTechnicalSummary({ classification });
|
|
106
|
+
expect(summary.highlights[0]).toEqual({
|
|
107
|
+
area: "Estrutura",
|
|
108
|
+
detail: "Novo diretório `src/bin-lookup` com 2 arquivo(s).",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("registra remoções e impacto", () => {
|
|
113
|
+
const impact: AstImpact = {
|
|
114
|
+
importers: { "src/bin-lookup/service.ts": ["src/a.ts"] },
|
|
115
|
+
affectedFiles: ["src/a.ts"],
|
|
116
|
+
exportedChanges: [
|
|
117
|
+
{
|
|
118
|
+
file: "src/bin-lookup/service.ts",
|
|
119
|
+
symbol: "lookup",
|
|
120
|
+
change: "removed",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
const summary = buildTechnicalSummary({
|
|
125
|
+
classification: {
|
|
126
|
+
files: {
|
|
127
|
+
"src/old.ts": { status: "deleted" },
|
|
128
|
+
"src/bin-lookup/service.ts": { status: "modified" },
|
|
129
|
+
},
|
|
130
|
+
churn: {},
|
|
131
|
+
},
|
|
132
|
+
impact,
|
|
133
|
+
});
|
|
134
|
+
const text = summary.highlights.map((item) => `${item.area}: ${item.detail}`);
|
|
135
|
+
expect(text.join("\n")).toContain("Remoções: `src/old.ts`");
|
|
136
|
+
expect(text.join("\n")).toContain("Impacto: 1 arquivo(s) importam os alterados");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("não gera destaques quando não há mudança estrutural", () => {
|
|
140
|
+
const summary = buildTechnicalSummary({
|
|
141
|
+
classification: { files: {}, churn: {} },
|
|
142
|
+
});
|
|
143
|
+
expect(summary.highlights).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("parseTechnicalSummary", () => {
|
|
148
|
+
test("aceita artefato válido e normaliza o source", () => {
|
|
149
|
+
const summary = parseTechnicalSummary({
|
|
150
|
+
source: "deterministic",
|
|
151
|
+
headSha: "abc",
|
|
152
|
+
overview: "Estrutura",
|
|
153
|
+
highlights: [{ area: "Contratos", detail: "`X`" }],
|
|
154
|
+
});
|
|
155
|
+
expect(summary?.source).toBe("deterministic");
|
|
156
|
+
expect(summary?.highlights[0].area).toBe("Contratos");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("rejeita conteúdo vazio ou inválido", () => {
|
|
160
|
+
expect(parseTechnicalSummary(null)).toBeNull();
|
|
161
|
+
expect(parseTechnicalSummary({})).toBeNull();
|
|
162
|
+
expect(
|
|
163
|
+
parseTechnicalSummary({ highlights: [{ area: "", detail: "" }] }),
|
|
164
|
+
).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("limita a seis destaques", () => {
|
|
168
|
+
const summary = parseTechnicalSummary({
|
|
169
|
+
highlights: Array.from({ length: 10 }, (_, index) => ({
|
|
170
|
+
area: `Área ${index}`,
|
|
171
|
+
detail: `Detalhe ${index}`,
|
|
172
|
+
})),
|
|
173
|
+
});
|
|
174
|
+
expect(summary?.highlights).toHaveLength(6);
|
|
175
|
+
});
|
|
176
|
+
});
|