@payconductor/houndsman 0.1.4 → 0.3.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/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 +38 -0
- package/src/cli/commands/doctor-command.ts +55 -0
- package/src/cli/commands/findings-command.ts +28 -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 +22 -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 +43 -0
- package/src/cli/index.ts +62 -260
- package/src/core/arg-parser.ts +48 -0
- package/src/core/commands/findings.ts +163 -147
- package/src/core/commands/review.ts +18 -35
- package/src/core/commands/scaffold.ts +174 -64
- 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 +9 -19
- 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 +51 -0
- package/src/core/engine/review-scope.ts +95 -0
- package/src/core/engine/review-session.ts +51 -0
- package/src/core/file-classifier.ts +11 -67
- package/src/core/findings-state.ts +189 -38
- package/src/core/git-revision.ts +14 -40
- package/src/core/git-runner.ts +46 -0
- package/src/core/github-review.ts +423 -0
- package/src/core/orchestrator.ts +116 -337
- package/src/core/paths.ts +9 -0
- package/src/core/pr-context.ts +28 -54
- package/src/core/project-config.ts +57 -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 +112 -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 +108 -0
- package/src/core/run-status.ts +6 -11
- 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 +123 -0
- package/src/core/services/project-files.ts +79 -0
- package/src/core/services/verifier.ts +58 -0
- package/src/core/verify.ts +2 -1
- package/src/core/watchdog-registry.ts +58 -16
- package/src/index.ts +31 -1
- package/src/server/client/App.tsx +174 -49
- package/src/server/client/ArtifactsPanel.tsx +2 -2
- package/src/server/client/AutoRefresh.tsx +38 -0
- package/src/server/client/Filters.tsx +72 -53
- package/src/server/client/FindingCard.tsx +212 -71
- package/src/server/client/FindingsList.tsx +8 -6
- package/src/server/client/RunList.tsx +30 -7
- package/src/server/client/SubmitModal.tsx +418 -0
- package/src/server/client/SummaryBar.tsx +65 -12
- package/src/server/client/constants.ts +9 -12
- package/src/server/client/format.ts +40 -0
- package/src/server/client/icons.tsx +143 -0
- package/src/server/client/types.ts +69 -0
- package/src/server/index.ts +316 -222
- package/src/server/run-repository.ts +291 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +814 -0
- package/src/tests/baseline.test.ts +513 -0
- package/src/tests/cli.test.ts +167 -0
- package/src/tests/domain.test.ts +328 -0
- package/src/tests/github-review.test.ts +320 -0
- package/src/tests/infrastructure.test.ts +210 -0
- package/src/tests/server.test.ts +117 -0
|
@@ -280,83 +280,193 @@ core/
|
|
|
280
280
|
`;
|
|
281
281
|
|
|
282
282
|
// ---------------------------------------------------------------------------
|
|
283
|
-
//
|
|
283
|
+
// Templates (arquivo → conteúdo)
|
|
284
284
|
// ---------------------------------------------------------------------------
|
|
285
285
|
|
|
286
|
-
/**
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
286
|
+
/** Contexto de render de um template. */
|
|
287
|
+
type TemplateContext = { projectName: string };
|
|
288
|
+
|
|
289
|
+
/** Um arquivo da camada (caminho relativo + conteúdo). */
|
|
290
|
+
interface ScaffoldTemplate {
|
|
291
|
+
readonly path: string;
|
|
292
|
+
render(context: TemplateContext): string;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
class ConfigTemplate implements ScaffoldTemplate {
|
|
296
|
+
readonly path = "houndsman.config.ts";
|
|
297
|
+
|
|
298
|
+
render(context: TemplateContext): string {
|
|
299
|
+
return CONFIG_TEMPLATE(context.projectName);
|
|
296
300
|
}
|
|
297
|
-
mkdirSync(join(full, ".."), { recursive: true });
|
|
298
|
-
writeFileSync(full, content);
|
|
299
|
-
ctx.created.push(relativePath);
|
|
300
301
|
}
|
|
301
302
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const full = join(ctx.cwd, relativePath);
|
|
309
|
-
mkdirSync(join(full, ".."), { recursive: true });
|
|
310
|
-
writeFileSync(full, content);
|
|
311
|
-
ctx.generated.push(relativePath);
|
|
303
|
+
class WatchdogExampleTemplate implements ScaffoldTemplate {
|
|
304
|
+
readonly path = join(PROJECT_DIR, "watchdogs", "example.ts");
|
|
305
|
+
|
|
306
|
+
render(): string {
|
|
307
|
+
return WATCHDOG_EXAMPLE;
|
|
308
|
+
}
|
|
312
309
|
}
|
|
313
310
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// ---------------------------------------------------------------------------
|
|
311
|
+
class WatchdogsIndexTemplate implements ScaffoldTemplate {
|
|
312
|
+
readonly path = join(PROJECT_DIR, "watchdogs", "index.ts");
|
|
317
313
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
writeUserFile(ctx, join(PROJECT_DIR, "watchdogs", "example.ts"), WATCHDOG_EXAMPLE);
|
|
322
|
-
writeUserFile(ctx, join(PROJECT_DIR, "watchdogs", "index.ts"), WATCHDOGS_INDEX);
|
|
323
|
-
writeUserFile(ctx, join(PROJECT_DIR, "exclusions.ts"), EXCLUSIONS_TEMPLATE);
|
|
324
|
-
writeUserFile(ctx, join(PROJECT_DIR, "doc-mappings.ts"), DOC_MAPPINGS_TEMPLATE);
|
|
314
|
+
render(): string {
|
|
315
|
+
return WATCHDOGS_INDEX;
|
|
316
|
+
}
|
|
325
317
|
}
|
|
326
318
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
319
|
+
class ExclusionsTemplate implements ScaffoldTemplate {
|
|
320
|
+
readonly path = join(PROJECT_DIR, "exclusions.ts");
|
|
321
|
+
|
|
322
|
+
render(): string {
|
|
323
|
+
return EXCLUSIONS_TEMPLATE;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
class DocMappingsTemplate implements ScaffoldTemplate {
|
|
328
|
+
readonly path = join(PROJECT_DIR, "doc-mappings.ts");
|
|
329
|
+
|
|
330
|
+
render(): string {
|
|
331
|
+
return DOC_MAPPINGS_TEMPLATE;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
class TypesTemplate implements ScaffoldTemplate {
|
|
336
|
+
readonly path = join(CORE_DIR, "types.ts");
|
|
337
|
+
|
|
338
|
+
render(): string {
|
|
339
|
+
return TYPES_TEMPLATE;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
class GitignoreTemplate implements ScaffoldTemplate {
|
|
344
|
+
readonly path = join(PROJECT_DIR, ".gitignore");
|
|
345
|
+
|
|
346
|
+
render(): string {
|
|
347
|
+
return PROJECT_GITIGNORE;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
class SkillDocTemplate implements ScaffoldTemplate {
|
|
352
|
+
private readonly fileName: string;
|
|
353
|
+
|
|
354
|
+
constructor(fileName: string) {
|
|
355
|
+
this.fileName = fileName;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
get path(): string {
|
|
359
|
+
return join(CORE_SKILL_DIR, this.fileName);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
render(context: TemplateContext): string {
|
|
363
|
+
return readFileSync(join(SKILL_DIR, this.fileName), "utf8").replaceAll(
|
|
364
|
+
"{{projectName}}",
|
|
365
|
+
context.projectName,
|
|
345
366
|
);
|
|
346
367
|
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
class HarnessPointerTemplate implements ScaffoldTemplate {
|
|
371
|
+
private readonly harness: Harness;
|
|
347
372
|
|
|
348
|
-
|
|
373
|
+
constructor(harness: Harness) {
|
|
374
|
+
this.harness = harness;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
get path(): string {
|
|
378
|
+
return join(`.${this.harness}`, "skills", "houndsman", SKILL_FILE);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
render(context: TemplateContext): string {
|
|
382
|
+
return readFileSync(join(SKILL_DIR, SKILL_FILE), "utf8").replaceAll(
|
|
383
|
+
"{{projectName}}",
|
|
384
|
+
context.projectName,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
349
387
|
}
|
|
350
388
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Scaffolder
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
/** Escreve as camadas do projeto (`setup`) e sincroniza os auto-gerados (`update`). */
|
|
394
|
+
export class ProjectScaffolder {
|
|
395
|
+
private readonly ctx: ScaffoldContext;
|
|
396
|
+
|
|
397
|
+
constructor(ctx: ScaffoldContext) {
|
|
398
|
+
this.ctx = ctx;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Arquivos do usuário: config, watchdogs, exclusões e mapeamentos. */
|
|
402
|
+
writeUserLayer(): void {
|
|
403
|
+
const context = this.context();
|
|
404
|
+
const templates: ScaffoldTemplate[] = [
|
|
405
|
+
new ConfigTemplate(),
|
|
406
|
+
new WatchdogExampleTemplate(),
|
|
407
|
+
new WatchdogsIndexTemplate(),
|
|
408
|
+
new ExclusionsTemplate(),
|
|
409
|
+
new DocMappingsTemplate(),
|
|
410
|
+
];
|
|
411
|
+
for (const template of templates) this.writeUserFile(template, context);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Arquivos auto-gerados (sempre sobrescritos). */
|
|
415
|
+
syncGenerated(): void {
|
|
416
|
+
const context = this.context();
|
|
417
|
+
this.writeGeneratedFile(new TypesTemplate(), context);
|
|
418
|
+
this.writeGeneratedFile(new GitignoreTemplate(), context);
|
|
419
|
+
|
|
420
|
+
// Docs do skill vão para `.houndsman/core/skill/`. O `SKILL.md` é o entry
|
|
421
|
+
// point dos harnesses, não um doc da camada.
|
|
422
|
+
for (const name of readdirSync(SKILL_DIR)) {
|
|
423
|
+
if (!name.endsWith(".md") || name === SKILL_FILE) continue;
|
|
424
|
+
this.writeGeneratedFile(new SkillDocTemplate(name), context);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (const harness of this.ctx.harnesses) {
|
|
428
|
+
this.writeGeneratedFile(new HarnessPointerTemplate(harness), context);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private context(): TemplateContext {
|
|
433
|
+
return { projectName: this.ctx.projectName };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Usuário: só escreve se ausente (ou `force`). */
|
|
437
|
+
private writeUserFile(template: ScaffoldTemplate, context: TemplateContext): void {
|
|
438
|
+
const full = join(this.ctx.cwd, template.path);
|
|
439
|
+
if (existsSync(full) && !this.ctx.force) {
|
|
440
|
+
this.ctx.skipped.push(template.path);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
444
|
+
writeFileSync(full, template.render(context));
|
|
445
|
+
this.ctx.created.push(template.path);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Auto-gerado: sempre sobrescreve. */
|
|
449
|
+
private writeGeneratedFile(
|
|
450
|
+
template: ScaffoldTemplate,
|
|
451
|
+
context: TemplateContext,
|
|
452
|
+
): void {
|
|
453
|
+
const full = join(this.ctx.cwd, template.path);
|
|
454
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
455
|
+
writeFileSync(full, template.render(context));
|
|
456
|
+
this.ctx.generated.push(template.path);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// Fachada (compatibilidade)
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
/** Arquivos do usuário (setup). */
|
|
465
|
+
export function writeUserLayer(ctx: ScaffoldContext): void {
|
|
466
|
+
new ProjectScaffolder(ctx).writeUserLayer();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Arquivos auto-gerados (setup/update). */
|
|
470
|
+
export function syncGeneratedFiles(ctx: ScaffoldContext): void {
|
|
471
|
+
new ProjectScaffolder(ctx).syncGenerated();
|
|
362
472
|
}
|
|
@@ -49,27 +49,32 @@ function ensurePackageScript(ctx: ScaffoldContext): void {
|
|
|
49
49
|
ctx.created.push("package.json#scripts.houndsman");
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** Cria a configuração inicial do projeto. */
|
|
53
|
-
export async function runSetupCommand(options: SetupOptions): Promise<
|
|
52
|
+
/** Cria a configuração inicial do projeto. Devolve as mensagens do CLI. */
|
|
53
|
+
export async function runSetupCommand(options: SetupOptions): Promise<string[]> {
|
|
54
54
|
const ctx = createScaffoldContext(
|
|
55
55
|
options.cwd,
|
|
56
56
|
options.args,
|
|
57
57
|
options.args.includes("--force"),
|
|
58
58
|
);
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
const messages = [
|
|
61
|
+
`→ Configurando houndsman em ${ctx.cwd} (projeto: ${ctx.projectName})`,
|
|
62
|
+
];
|
|
61
63
|
|
|
62
64
|
writeUserLayer(ctx);
|
|
63
65
|
syncGeneratedFiles(ctx);
|
|
64
66
|
ensurePackageScript(ctx);
|
|
65
67
|
|
|
66
|
-
for (const path of ctx.created)
|
|
67
|
-
for (const path of ctx.skipped)
|
|
68
|
-
for (const path of ctx.generated)
|
|
68
|
+
for (const path of ctx.created) messages.push(` + ${path}`);
|
|
69
|
+
for (const path of ctx.skipped) messages.push(` = ${path} (mantido)`);
|
|
70
|
+
for (const path of ctx.generated) messages.push(` ~ ${path} (auto)`);
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
messages.push(
|
|
73
|
+
"\nPróximos passos:",
|
|
74
|
+
" 1. Ajuste `houndsman.config.ts` e `.houndsman/watchdogs/`.",
|
|
75
|
+
" 2. Valide: bun run houndsman doctor",
|
|
76
|
+
" 3. Prepare a revisão: bun run houndsman",
|
|
77
|
+
" · Sincronizar arquivos auto-gerados: bun run houndsman update",
|
|
78
|
+
);
|
|
79
|
+
return messages;
|
|
75
80
|
}
|
|
@@ -13,18 +13,18 @@ export type UpdateOptions = {
|
|
|
13
13
|
args: string[];
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
/** Sincroniza os arquivos auto-gerados da camada de projeto. */
|
|
17
|
-
export async function runUpdateCommand(options: UpdateOptions): Promise<
|
|
16
|
+
/** Sincroniza os arquivos auto-gerados da camada de projeto. Devolve mensagens. */
|
|
17
|
+
export async function runUpdateCommand(options: UpdateOptions): Promise<string[]> {
|
|
18
18
|
const ctx = createScaffoldContext(options.cwd, options.args, false);
|
|
19
|
-
|
|
20
|
-
console.log(`→ Sincronizando arquivos auto-gerados em ${ctx.cwd}`);
|
|
19
|
+
const messages = [`→ Sincronizando arquivos auto-gerados em ${ctx.cwd}`];
|
|
21
20
|
|
|
22
21
|
syncGeneratedFiles(ctx);
|
|
23
22
|
|
|
24
|
-
for (const path of ctx.generated)
|
|
23
|
+
for (const path of ctx.generated) messages.push(` ~ ${path} (auto)`);
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
messages.push(
|
|
26
|
+
"\nArquivos do usuário não foram tocados:",
|
|
28
27
|
" .houndsman/watchdogs/ · exclusions.ts · doc-mappings.ts · houndsman.config.ts",
|
|
29
28
|
);
|
|
29
|
+
return messages;
|
|
30
30
|
}
|
|
@@ -6,12 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { HoundsmanConfig } from "../config";
|
|
9
|
-
import {
|
|
10
|
-
type Rule,
|
|
11
|
-
SEVERITY_EMOJI,
|
|
12
|
-
severityRank,
|
|
13
|
-
type WatchdogDefinition,
|
|
14
|
-
} from "../types";
|
|
9
|
+
import { WatchdogPresenter } from "../renderers/watchdog-presenter";
|
|
15
10
|
import { findWatchdog, getWatchdogContext } from "../watchdog-registry";
|
|
16
11
|
|
|
17
12
|
export type WatchdogContextOptions = {
|
|
@@ -21,7 +16,7 @@ export type WatchdogContextOptions = {
|
|
|
21
16
|
json?: boolean;
|
|
22
17
|
};
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
const presenter = new WatchdogPresenter();
|
|
25
20
|
|
|
26
21
|
/** Renderiza o contexto de um watchdog (markdown ou JSON). `null` se o id não existe. */
|
|
27
22
|
export function renderWatchdogContext(
|
|
@@ -29,77 +24,10 @@ export function renderWatchdogContext(
|
|
|
29
24
|
id: string,
|
|
30
25
|
opts: WatchdogContextOptions = {},
|
|
31
26
|
): string | null {
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
: renderWatchdogMarkdown(ctx, def, withRules);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Payload serializável (regras incluídas apenas com `withRules`). */
|
|
42
|
-
function serializeWatchdogContext(
|
|
43
|
-
ctx: WatchdogContext,
|
|
44
|
-
def: WatchdogDefinition,
|
|
45
|
-
withRules: boolean,
|
|
46
|
-
): string {
|
|
47
|
-
const payload = withRules ? { ...ctx, rules: def.rules } : { ...ctx };
|
|
48
|
-
return JSON.stringify(payload, null, 2);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Lista de itens em bloco `## <título>` (vazio se não houver itens). */
|
|
52
|
-
function section(title: string, items: readonly string[] | undefined): string[] {
|
|
53
|
-
if (!items || items.length === 0) return [];
|
|
54
|
-
return [`## ${title}`, "", ...items.map((item) => `- ${item}`), ""];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Regras com emoji de severidade em bloco `## <título>`. */
|
|
58
|
-
function ruleSection(title: string, rules: readonly Rule[]): string[] {
|
|
59
|
-
return [
|
|
60
|
-
`## ${title}`,
|
|
61
|
-
"",
|
|
62
|
-
...rules.map(
|
|
63
|
-
(rule) =>
|
|
64
|
-
`- ${SEVERITY_EMOJI[rule.severity]} **${rule.severity}** — ${rule.text}`,
|
|
65
|
-
),
|
|
66
|
-
"",
|
|
67
|
-
];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Render markdown legível do contexto do watchdog. */
|
|
71
|
-
function renderWatchdogMarkdown(
|
|
72
|
-
ctx: WatchdogContext,
|
|
73
|
-
def: WatchdogDefinition,
|
|
74
|
-
withRules: boolean,
|
|
75
|
-
): string {
|
|
76
|
-
const lines: string[] = [
|
|
77
|
-
`# ${ctx.name} (\`${ctx.id}\`)`,
|
|
78
|
-
"",
|
|
79
|
-
`**Descrição:** ${ctx.description}`,
|
|
80
|
-
`**Escopo:** ${ctx.scope}`,
|
|
81
|
-
"",
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
lines.push(...section("Prioridade de leitura", ctx.reviewFocus));
|
|
85
|
-
lines.push(...section("Não carregar", ctx.exclude));
|
|
86
|
-
lines.push(
|
|
87
|
-
...section(
|
|
88
|
-
"Docs de contexto",
|
|
89
|
-
ctx.contextFiles.map((doc) => `\`${doc}\``),
|
|
90
|
-
),
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
if (withRules) {
|
|
94
|
-
lines.push(
|
|
95
|
-
...ruleSection(
|
|
96
|
-
"Regras",
|
|
97
|
-
[...def.rules].sort(
|
|
98
|
-
(a, b) => severityRank(a.severity) - severityRank(b.severity),
|
|
99
|
-
),
|
|
100
|
-
),
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return lines.join("\n").trimEnd();
|
|
27
|
+
const definition = findWatchdog(config, id);
|
|
28
|
+
if (!definition) return null;
|
|
29
|
+
return presenter.render(getWatchdogContext(config, definition.id), definition, {
|
|
30
|
+
withRules: opts.withRules ?? false,
|
|
31
|
+
json: opts.json ?? false,
|
|
32
|
+
});
|
|
105
33
|
}
|
package/src/core/config.ts
CHANGED
|
@@ -6,22 +6,14 @@
|
|
|
6
6
|
* (camada `houndsman/`) — único ponto de troca ao portar para outro repositório.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { ProjectConfig } from "./project-config";
|
|
10
10
|
import type { WatchdogDefinition } from "./types";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* concentrar centenas de arquivos em um único domínio; o teto mantém o contexto
|
|
18
|
-
* do subagente limitado — o excedente vira "fora do orçamento" (cobertura
|
|
19
|
-
* honesta), nunca some silenciosamente.
|
|
20
|
-
*/
|
|
21
|
-
export const DEFAULT_MAX_FILES_PER_BRIEF = 40;
|
|
22
|
-
|
|
23
|
-
/** Concorrência default das chamadas simultâneas ao Jev. */
|
|
24
|
-
export const DEFAULT_JEV_CONCURRENCY = 5;
|
|
12
|
+
export {
|
|
13
|
+
DEFAULT_ALWAYS_INCLUDE_DOCS,
|
|
14
|
+
DEFAULT_JEV_CONCURRENCY,
|
|
15
|
+
DEFAULT_MAX_FILES_PER_BRIEF,
|
|
16
|
+
} from "./defaults";
|
|
25
17
|
|
|
26
18
|
/** Limites de triage para PRs grandes. */
|
|
27
19
|
export type HoundsmanBudgets = {
|
|
@@ -82,7 +74,7 @@ export function defineHoundsmanConfig<TId extends string>(
|
|
|
82
74
|
|
|
83
75
|
/** Diretório dos artefatos — o valor do projeto ou o default da camada. */
|
|
84
76
|
export function resolveArtifactDir(config: HoundsmanConfig): string {
|
|
85
|
-
return config.artifactDir
|
|
77
|
+
return new ProjectConfig(config).artifactDir;
|
|
86
78
|
}
|
|
87
79
|
|
|
88
80
|
/**
|
|
@@ -90,12 +82,10 @@ export function resolveArtifactDir(config: HoundsmanConfig): string {
|
|
|
90
82
|
* mantém o contexto do subagente limitado em PRs grandes.
|
|
91
83
|
*/
|
|
92
84
|
export function resolveMaxFilesPerBrief(config: HoundsmanConfig): number {
|
|
93
|
-
|
|
94
|
-
return value !== undefined && value > 0 ? value : DEFAULT_MAX_FILES_PER_BRIEF;
|
|
85
|
+
return new ProjectConfig(config).maxFilesPerBrief;
|
|
95
86
|
}
|
|
96
87
|
|
|
97
88
|
/** Concorrência das chamadas Jev (valor ausente/não-positivo cai no default). */
|
|
98
89
|
export function resolveJevConcurrency(config: HoundsmanConfig): number {
|
|
99
|
-
|
|
100
|
-
return value !== undefined && value > 0 ? value : DEFAULT_JEV_CONCURRENCY;
|
|
90
|
+
return new ProjectConfig(config).jevConcurrency;
|
|
101
91
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defaults do motor (biblioteca) — valores canônicos compartilhados.
|
|
3
|
+
*
|
|
4
|
+
* Vivem fora de `config.ts` para que `ProjectConfig` possa usá-los sem criar
|
|
5
|
+
* ciclo de importação com os adaptadores públicos.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Documentos sempre incluídos nos briefs (default). */
|
|
9
|
+
export const DEFAULT_ALWAYS_INCLUDE_DOCS: readonly string[] = ["AGENTS.md"];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Teto default de arquivos por brief. Um PR grande (milhares de linhas) pode
|
|
13
|
+
* concentrar centenas de arquivos em um único domínio; o teto mantém o contexto
|
|
14
|
+
* do subagente limitado — o excedente vira "fora do orçamento" (cobertura
|
|
15
|
+
* honesta), nunca some silenciosamente.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_MAX_FILES_PER_BRIEF = 40;
|
|
18
|
+
|
|
19
|
+
/** Concorrência default das chamadas simultâneas ao Jev. */
|
|
20
|
+
export const DEFAULT_JEV_CONCURRENCY = 5;
|
package/src/core/diff-source.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Jev) e por `classifyRange` (fallback standalone).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { runGit } from "./git-runner";
|
|
12
13
|
import type { ChurnMap, FileChurn } from "./types";
|
|
13
14
|
|
|
14
15
|
export type DiffSource = {
|
|
@@ -26,19 +27,6 @@ export type CollectDiffOptions = {
|
|
|
26
27
|
/** Teto default de caracteres por arquivo no patch (compartilhado com o git-revision). */
|
|
27
28
|
export const DEFAULT_MAX_PER_FILE_CHARS = 8_000;
|
|
28
29
|
|
|
29
|
-
/** Executa `git` com `core.quotepath=false` (paths literais) e devolve o stdout. */
|
|
30
|
-
function runGit(args: string[]): string {
|
|
31
|
-
const result = Bun.spawnSync(["git", "-c", "core.quotepath=false", ...args], {
|
|
32
|
-
stdout: "pipe",
|
|
33
|
-
stderr: "pipe",
|
|
34
|
-
});
|
|
35
|
-
if (result.exitCode !== 0) {
|
|
36
|
-
const stderr = new TextDecoder().decode(result.stderr).trim();
|
|
37
|
-
throw new Error(`git ${args.join(" ")} falhou: ${stderr}`);
|
|
38
|
-
}
|
|
39
|
-
return new TextDecoder().decode(result.stdout);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
30
|
/** Novo caminho do arquivo a partir do cabeçalho de um segmento do patch. */
|
|
43
31
|
function extractPath(segment: string): string | undefined {
|
|
44
32
|
// Só o cabeçalho (antes do primeiro hunk) pode conter as linhas `---`/`+++` —
|
|
@@ -6,28 +6,38 @@
|
|
|
6
6
|
* (`project/doc-mappings.ts`) e chegam via `HoundsmanConfig`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import type { HoundsmanConfig } from "./config";
|
|
10
|
+
import { ProjectConfig } from "./project-config";
|
|
10
11
|
|
|
11
|
-
/** Descobre
|
|
12
|
-
export
|
|
13
|
-
|
|
12
|
+
/** Descobre documentação relevante para um conjunto de arquivos alterados. */
|
|
13
|
+
export class DocDiscoverer {
|
|
14
|
+
private readonly alwaysInclude: readonly string[];
|
|
15
|
+
private readonly mappings: readonly (readonly [RegExp, readonly string[]])[];
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
constructor(config: HoundsmanConfig) {
|
|
18
|
+
const project = new ProjectConfig(config);
|
|
19
|
+
this.alwaysInclude = project.alwaysIncludeDocs;
|
|
20
|
+
this.mappings = project.docMappings;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
if (!normalized) continue;
|
|
23
|
+
discover(filePaths: readonly string[]): string[] {
|
|
24
|
+
const docs = new Set<string>(this.alwaysInclude);
|
|
22
25
|
|
|
23
|
-
for (const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
for (const filePath of filePaths) {
|
|
27
|
+
const normalized = filePath.trim().toLowerCase();
|
|
28
|
+
if (!normalized) continue;
|
|
29
|
+
|
|
30
|
+
for (const [pattern, docPaths] of this.mappings) {
|
|
31
|
+
if (!pattern.test(normalized)) continue;
|
|
32
|
+
for (const docPath of docPaths) docs.add(docPath);
|
|
28
33
|
}
|
|
29
34
|
}
|
|
35
|
+
|
|
36
|
+
return [...docs].sort();
|
|
30
37
|
}
|
|
38
|
+
}
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
/** Descobre docs relevantes com base nos paths + docs sempre incluídos. */
|
|
41
|
+
export function discoverDocs(filePaths: string[], config: HoundsmanConfig): string[] {
|
|
42
|
+
return new DocDiscoverer(config).discover(filePaths);
|
|
33
43
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orçamento por brief (domínio) — funções puras de triagem de PRs grandes.
|
|
3
|
+
*
|
|
4
|
+
* Separado de `file-classifier` para que `Classification` possa aplicar o
|
|
5
|
+
* orçamento sem depender do serviço de classificação.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ChurnMap } from "../types";
|
|
9
|
+
|
|
10
|
+
/** Churn total (adições + remoções) de um arquivo; 0 quando ausente. */
|
|
11
|
+
export function churnOf(filePath: string, churn: ChurnMap | undefined): number {
|
|
12
|
+
const entry = churn?.[filePath];
|
|
13
|
+
return entry ? entry.additions + entry.deletions : 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Ordena os arquivos por risco (churn desc; empate pelo caminho). */
|
|
17
|
+
export function sortByRisk(files: readonly string[], churn?: ChurnMap): string[] {
|
|
18
|
+
return [...files].sort((a, b) => {
|
|
19
|
+
const delta = churnOf(b, churn) - churnOf(a, churn);
|
|
20
|
+
return delta !== 0 ? delta : a.localeCompare(b);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Resultado da aplicação do orçamento: o que revisar e o que ficou de fora. */
|
|
25
|
+
export type BudgetSelection = { selected: string[]; overflow: string[] };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Aplica o orçamento por brief: mantém os `maxFiles` de maior risco e devolve o
|
|
29
|
+
* restante em `overflow` (nunca descarta silenciosamente). `maxFiles` não-positivo
|
|
30
|
+
* ou não-finito desativa o orçamento.
|
|
31
|
+
*/
|
|
32
|
+
export function selectByBudget(
|
|
33
|
+
files: readonly string[],
|
|
34
|
+
churn: ChurnMap | undefined,
|
|
35
|
+
maxFiles: number,
|
|
36
|
+
): BudgetSelection {
|
|
37
|
+
const ordered = sortByRisk(files, churn);
|
|
38
|
+
if (!Number.isFinite(maxFiles) || maxFiles <= 0 || ordered.length <= maxFiles) {
|
|
39
|
+
return { selected: ordered, overflow: [] };
|
|
40
|
+
}
|
|
41
|
+
return { selected: ordered.slice(0, maxFiles), overflow: ordered.slice(maxFiles) };
|
|
42
|
+
}
|