@payconductor/houndsman 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -6
- package/package.json +1 -1
- package/src/cli/arg-parser.ts +8 -0
- package/src/cli/commands/command.ts +23 -0
- package/src/cli/commands/context-command.ts +40 -0
- package/src/cli/commands/doctor-command.ts +60 -0
- package/src/cli/commands/findings-command.ts +38 -0
- package/src/cli/commands/help-command.ts +13 -0
- package/src/cli/commands/registry.ts +34 -0
- package/src/cli/commands/review-command.ts +21 -0
- package/src/cli/commands/server-command.ts +37 -0
- package/src/cli/commands/setup-command.ts +34 -0
- package/src/cli/commands/watchdog-command.ts +34 -0
- package/src/cli/help.ts +47 -0
- package/src/cli/index.ts +60 -268
- package/src/core/arg-parser.ts +48 -0
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +185 -260
- package/src/core/commands/review.ts +21 -39
- package/src/core/commands/scaffold.ts +189 -80
- package/src/core/commands/setup.ts +16 -11
- package/src/core/commands/update.ts +7 -7
- package/src/core/commands/watchdog.ts +8 -80
- package/src/core/config.ts +24 -25
- package/src/core/defaults.ts +20 -0
- package/src/core/diff-source.ts +1 -13
- package/src/core/doc-discoverer.ts +25 -15
- package/src/core/domain/budget.ts +42 -0
- package/src/core/domain/classification.ts +94 -0
- package/src/core/domain/diff.ts +53 -0
- package/src/core/domain/watchdog.ts +96 -0
- package/src/core/engine/carryover.ts +67 -0
- package/src/core/engine/houndsman-engine.ts +65 -0
- package/src/core/engine/review-scope.ts +95 -0
- package/src/core/engine/review-session.ts +79 -0
- package/src/core/file-classifier.ts +11 -67
- package/src/core/findings-state.ts +193 -100
- package/src/core/git-revision.ts +14 -40
- package/src/core/git-runner.ts +46 -0
- package/src/core/github-review.ts +4 -33
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +247 -421
- package/src/core/paths.ts +26 -11
- package/src/core/pr-context.ts +39 -51
- package/src/core/project-config.ts +56 -0
- package/src/core/renderers/batch-presenter.ts +40 -0
- package/src/core/renderers/brief-renderer.ts +23 -0
- package/src/core/renderers/findings-presenter.ts +74 -0
- package/src/core/renderers/manifest-writer.ts +123 -0
- package/src/core/renderers/report-renderer.ts +66 -0
- package/src/core/renderers/verify-presenter.ts +50 -0
- package/src/core/renderers/watchdog-presenter.ts +100 -0
- package/src/core/report-builder.ts +12 -7
- package/src/core/run-layout.ts +8 -0
- package/src/core/run-status.ts +29 -59
- package/src/core/services/clock.ts +12 -0
- package/src/core/services/file-classifier.ts +34 -0
- package/src/core/services/fix-batch-builder.ts +13 -0
- package/src/core/services/gh-client.ts +54 -0
- package/src/core/services/git-repository.ts +139 -0
- package/src/core/services/project-files.ts +91 -0
- package/src/core/services/verifier.ts +58 -0
- package/src/core/storage/factory.ts +24 -0
- package/src/core/storage/file/file-storage.ts +269 -0
- package/src/core/storage/paths.ts +64 -0
- package/src/core/storage/repo-identity.ts +70 -0
- package/src/core/storage/sqlite/mappers.ts +160 -0
- package/src/core/storage/sqlite/schema.ts +108 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
- package/src/core/storage/types.ts +143 -0
- package/src/core/storage/work-area.ts +191 -0
- package/src/core/verify.ts +2 -1
- package/src/core/watchdog-registry.ts +58 -16
- package/src/index.ts +57 -6
- package/src/server/artifacts.ts +8 -61
- package/src/server/client/ArtifactsPanel.tsx +137 -23
- package/src/server/client/FindingCard.tsx +37 -6
- package/src/server/client/MarkdownText.tsx +101 -0
- package/src/server/client/SubmitModal.tsx +9 -4
- package/src/server/client/code-highlight.ts +422 -0
- package/src/server/client/constants.ts +130 -24
- package/src/server/client/json-highlight.ts +55 -0
- package/src/server/client/markdown.ts +153 -0
- package/src/server/client/types.ts +23 -0
- package/src/server/index.ts +155 -245
- package/src/server/run-repository.ts +299 -0
- package/src/skill/OUTPUT-FORMAT.md +6 -4
- package/src/skill/README.md +22 -14
- package/src/skill/SETUP.md +4 -4
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +27 -18
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +5 -5
- package/src/tests/__snapshots__/baseline.test.ts.snap +825 -0
- package/src/tests/baseline.test.ts +555 -493
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +328 -0
- package/src/tests/findings-commands.test.ts +153 -131
- package/src/tests/findings-state.test.ts +6 -3
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/infrastructure.test.ts +205 -0
- package/src/tests/markdown.test.ts +89 -0
- package/src/tests/review-scripts.test.ts +8 -5
- package/src/tests/run-status.test.ts +33 -23
- package/src/tests/server.test.ts +108 -75
- package/src/tests/storage.test.ts +386 -0
|
@@ -4,49 +4,31 @@
|
|
|
4
4
|
* Biblioteca: NÃO usa `process.argv`/`process.exit` (o CLI decide o exit code).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { ArgParser } from "../arg-parser";
|
|
7
8
|
import { defaultOptions, type ReviewOptions } from "../orchestrator";
|
|
8
|
-
import { ARTIFACT_DIR } from "../paths";
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"--
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
/** Converte argv (sem o subcomando) nas opções de prepare/finalize. */
|
|
11
|
+
export function parseReviewArgs(args: string[]): ReviewOptions {
|
|
12
|
+
const parser = ArgParser.from(args);
|
|
13
|
+
if (parser.has("--dir")) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"--dir foi removido; configure `storage` no houndsman.config.ts.",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const opts = defaultOptions();
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
20
|
+
const base = parser.value("--base");
|
|
21
|
+
const head = parser.value("--head");
|
|
22
|
+
const branch = parser.value("--branch");
|
|
23
|
+
if (base) opts.base = base;
|
|
24
|
+
if (head) opts.head = head;
|
|
25
|
+
if (branch) opts.branch = branch;
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
else if (flag === "--full") opts.full = true;
|
|
34
|
-
}
|
|
27
|
+
if (parser.has("--finalize") || parser.has("--report-only")) opts.finalize = true;
|
|
28
|
+
if (parser.has("--fail-on-blocking")) opts.failOnBlocking = true;
|
|
29
|
+
if (parser.has("--clean")) opts.clean = true;
|
|
30
|
+
if (parser.has("--no-jev")) opts.noJev = true;
|
|
31
|
+
if (parser.has("--full")) opts.full = true;
|
|
35
32
|
|
|
36
|
-
/** Converte argv (sem o subcomando) nas opções de prepare/finalize. */
|
|
37
|
-
export function parseReviewArgs(
|
|
38
|
-
args: string[],
|
|
39
|
-
artifactDir: string = ARTIFACT_DIR,
|
|
40
|
-
): ReviewOptions {
|
|
41
|
-
const opts = defaultOptions(artifactDir);
|
|
42
|
-
for (let i = 0; i < args.length; i++) {
|
|
43
|
-
const arg = args[i];
|
|
44
|
-
if (VALUE_FLAGS.has(arg)) {
|
|
45
|
-
applyValue(opts, arg, args[i + 1]);
|
|
46
|
-
i++;
|
|
47
|
-
} else if (BOOLEAN_FLAGS.has(arg)) {
|
|
48
|
-
applyBoolean(opts, arg);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
33
|
return opts;
|
|
52
34
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* **do usuário**:
|
|
6
6
|
*
|
|
7
7
|
* .houndsman/
|
|
8
|
-
* ├── .gitignore AUTO — ignora core/
|
|
8
|
+
* ├── .gitignore AUTO — ignora core/ (gerado)
|
|
9
9
|
* ├── core/types.ts AUTO — snapshot dos tipos + helpers
|
|
10
10
|
* ├── core/skill/*.md AUTO — docs do skill (com {{projectName}} resolvido)
|
|
11
11
|
* ├── watchdogs/*.ts USUÁRIO
|
|
@@ -16,13 +16,7 @@
|
|
|
16
16
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { basename, join } from "node:path";
|
|
18
18
|
import { SKILL_DIR, SKILL_FILE } from "../assets";
|
|
19
|
-
import {
|
|
20
|
-
ARTIFACT_DIR,
|
|
21
|
-
CORE_DIR,
|
|
22
|
-
CORE_SKILL_DIR,
|
|
23
|
-
OUTPUT_FORMAT_PATH,
|
|
24
|
-
PROJECT_DIR,
|
|
25
|
-
} from "../paths";
|
|
19
|
+
import { CORE_DIR, CORE_SKILL_DIR, OUTPUT_FORMAT_PATH, PROJECT_DIR } from "../paths";
|
|
26
20
|
|
|
27
21
|
export type Harness = "agents" | "claude";
|
|
28
22
|
|
|
@@ -87,9 +81,9 @@ import type { HoundsmanConfig } from "./${CORE_DIR}/types";
|
|
|
87
81
|
/**
|
|
88
82
|
* Configuração do houndsman para ${projectName}.
|
|
89
83
|
*
|
|
90
|
-
*
|
|
91
|
-
* output em ${OUTPUT_FORMAT_PATH}. Ajuste
|
|
92
|
-
* docs conforme o projeto.
|
|
84
|
+
* Runs, findings e artefatos vivem no storage global (SQLite por default) —
|
|
85
|
+
* fora do projeto. O schema de output fica em ${OUTPUT_FORMAT_PATH}. Ajuste
|
|
86
|
+
* watchdogs, exclusões e mapeamentos de docs conforme o projeto.
|
|
93
87
|
*/
|
|
94
88
|
export const houndsmanConfig = {
|
|
95
89
|
projectName: "${projectName}",
|
|
@@ -182,10 +176,16 @@ export type HoundsmanConcurrency = {
|
|
|
182
176
|
jev?: number;
|
|
183
177
|
};
|
|
184
178
|
|
|
179
|
+
/** Backend de persistência (reviews, findings e artefatos). */
|
|
180
|
+
export type HoundsmanStorage = {
|
|
181
|
+
provider?: "sqlite" | "file";
|
|
182
|
+
path?: string;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
185
|
/** Contrato de configuração do motor. */
|
|
186
186
|
export type HoundsmanConfig<TId extends string = string> = {
|
|
187
187
|
projectName: string;
|
|
188
|
-
|
|
188
|
+
storage?: HoundsmanStorage;
|
|
189
189
|
watchdogIds?: readonly TId[];
|
|
190
190
|
watchdogs: Record<TId, WatchdogDefinition>;
|
|
191
191
|
exclusions: readonly RegExp[];
|
|
@@ -267,10 +267,9 @@ export function isExcluded(filePath: string): boolean {
|
|
|
267
267
|
const DOC_MAPPINGS_TEMPLATE = `/**
|
|
268
268
|
* Mapeamento de padrões nos paths → documentação relevante.
|
|
269
269
|
* Consumido pelo motor via \`HoundsmanConfig.docMappings\`.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
];
|
|
270
|
+
*/\`core/\` é regenerado por \`houndsman update\`.
|
|
271
|
+
# Runs, findings e artefatos vivem no storage global (fora do projeto).
|
|
272
|
+
core
|
|
274
273
|
`;
|
|
275
274
|
|
|
276
275
|
const PROJECT_GITIGNORE = `# Diretórios gerados pelo houndsman — \`core/\` é regenerado por \`houndsman update\`
|
|
@@ -280,83 +279,193 @@ core/
|
|
|
280
279
|
`;
|
|
281
280
|
|
|
282
281
|
// ---------------------------------------------------------------------------
|
|
283
|
-
//
|
|
282
|
+
// Templates (arquivo → conteúdo)
|
|
284
283
|
// ---------------------------------------------------------------------------
|
|
285
284
|
|
|
286
|
-
/**
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
285
|
+
/** Contexto de render de um template. */
|
|
286
|
+
type TemplateContext = { projectName: string };
|
|
287
|
+
|
|
288
|
+
/** Um arquivo da camada (caminho relativo + conteúdo). */
|
|
289
|
+
interface ScaffoldTemplate {
|
|
290
|
+
readonly path: string;
|
|
291
|
+
render(context: TemplateContext): string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
class ConfigTemplate implements ScaffoldTemplate {
|
|
295
|
+
readonly path = "houndsman.config.ts";
|
|
296
|
+
|
|
297
|
+
render(context: TemplateContext): string {
|
|
298
|
+
return CONFIG_TEMPLATE(context.projectName);
|
|
296
299
|
}
|
|
297
|
-
mkdirSync(join(full, ".."), { recursive: true });
|
|
298
|
-
writeFileSync(full, content);
|
|
299
|
-
ctx.created.push(relativePath);
|
|
300
300
|
}
|
|
301
301
|
|
|
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);
|
|
302
|
+
class WatchdogExampleTemplate implements ScaffoldTemplate {
|
|
303
|
+
readonly path = join(PROJECT_DIR, "watchdogs", "example.ts");
|
|
304
|
+
|
|
305
|
+
render(): string {
|
|
306
|
+
return WATCHDOG_EXAMPLE;
|
|
307
|
+
}
|
|
312
308
|
}
|
|
313
309
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// ---------------------------------------------------------------------------
|
|
310
|
+
class WatchdogsIndexTemplate implements ScaffoldTemplate {
|
|
311
|
+
readonly path = join(PROJECT_DIR, "watchdogs", "index.ts");
|
|
317
312
|
|
|
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);
|
|
313
|
+
render(): string {
|
|
314
|
+
return WATCHDOGS_INDEX;
|
|
315
|
+
}
|
|
325
316
|
}
|
|
326
317
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
318
|
+
class ExclusionsTemplate implements ScaffoldTemplate {
|
|
319
|
+
readonly path = join(PROJECT_DIR, "exclusions.ts");
|
|
320
|
+
|
|
321
|
+
render(): string {
|
|
322
|
+
return EXCLUSIONS_TEMPLATE;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
class DocMappingsTemplate implements ScaffoldTemplate {
|
|
327
|
+
readonly path = join(PROJECT_DIR, "doc-mappings.ts");
|
|
328
|
+
|
|
329
|
+
render(): string {
|
|
330
|
+
return DOC_MAPPINGS_TEMPLATE;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
class TypesTemplate implements ScaffoldTemplate {
|
|
335
|
+
readonly path = join(CORE_DIR, "types.ts");
|
|
336
|
+
|
|
337
|
+
render(): string {
|
|
338
|
+
return TYPES_TEMPLATE;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
class GitignoreTemplate implements ScaffoldTemplate {
|
|
343
|
+
readonly path = join(PROJECT_DIR, ".gitignore");
|
|
344
|
+
|
|
345
|
+
render(): string {
|
|
346
|
+
return PROJECT_GITIGNORE;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
class SkillDocTemplate implements ScaffoldTemplate {
|
|
351
|
+
private readonly fileName: string;
|
|
352
|
+
|
|
353
|
+
constructor(fileName: string) {
|
|
354
|
+
this.fileName = fileName;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
get path(): string {
|
|
358
|
+
return join(CORE_SKILL_DIR, this.fileName);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
render(context: TemplateContext): string {
|
|
362
|
+
return readFileSync(join(SKILL_DIR, this.fileName), "utf8").replaceAll(
|
|
363
|
+
"{{projectName}}",
|
|
364
|
+
context.projectName,
|
|
345
365
|
);
|
|
346
366
|
}
|
|
367
|
+
}
|
|
347
368
|
|
|
348
|
-
|
|
369
|
+
class HarnessPointerTemplate implements ScaffoldTemplate {
|
|
370
|
+
private readonly harness: Harness;
|
|
371
|
+
|
|
372
|
+
constructor(harness: Harness) {
|
|
373
|
+
this.harness = harness;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
get path(): string {
|
|
377
|
+
return join(`.${this.harness}`, "skills", "houndsman", SKILL_FILE);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
render(context: TemplateContext): string {
|
|
381
|
+
return readFileSync(join(SKILL_DIR, SKILL_FILE), "utf8").replaceAll(
|
|
382
|
+
"{{projectName}}",
|
|
383
|
+
context.projectName,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
349
386
|
}
|
|
350
387
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Scaffolder
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
/** Escreve as camadas do projeto (`setup`) e sincroniza os auto-gerados (`update`). */
|
|
393
|
+
export class ProjectScaffolder {
|
|
394
|
+
private readonly ctx: ScaffoldContext;
|
|
395
|
+
|
|
396
|
+
constructor(ctx: ScaffoldContext) {
|
|
397
|
+
this.ctx = ctx;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Arquivos do usuário: config, watchdogs, exclusões e mapeamentos. */
|
|
401
|
+
writeUserLayer(): void {
|
|
402
|
+
const context = this.context();
|
|
403
|
+
const templates: ScaffoldTemplate[] = [
|
|
404
|
+
new ConfigTemplate(),
|
|
405
|
+
new WatchdogExampleTemplate(),
|
|
406
|
+
new WatchdogsIndexTemplate(),
|
|
407
|
+
new ExclusionsTemplate(),
|
|
408
|
+
new DocMappingsTemplate(),
|
|
409
|
+
];
|
|
410
|
+
for (const template of templates) this.writeUserFile(template, context);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Arquivos auto-gerados (sempre sobrescritos). */
|
|
414
|
+
syncGenerated(): void {
|
|
415
|
+
const context = this.context();
|
|
416
|
+
this.writeGeneratedFile(new TypesTemplate(), context);
|
|
417
|
+
this.writeGeneratedFile(new GitignoreTemplate(), context);
|
|
418
|
+
|
|
419
|
+
// Docs do skill vão para `.houndsman/core/skill/`. O `SKILL.md` é o entry
|
|
420
|
+
// point dos harnesses, não um doc da camada.
|
|
421
|
+
for (const name of readdirSync(SKILL_DIR)) {
|
|
422
|
+
if (!name.endsWith(".md") || name === SKILL_FILE) continue;
|
|
423
|
+
this.writeGeneratedFile(new SkillDocTemplate(name), context);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
for (const harness of this.ctx.harnesses) {
|
|
427
|
+
this.writeGeneratedFile(new HarnessPointerTemplate(harness), context);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private context(): TemplateContext {
|
|
432
|
+
return { projectName: this.ctx.projectName };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Usuário: só escreve se ausente (ou `force`). */
|
|
436
|
+
private writeUserFile(template: ScaffoldTemplate, context: TemplateContext): void {
|
|
437
|
+
const full = join(this.ctx.cwd, template.path);
|
|
438
|
+
if (existsSync(full) && !this.ctx.force) {
|
|
439
|
+
this.ctx.skipped.push(template.path);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
443
|
+
writeFileSync(full, template.render(context));
|
|
444
|
+
this.ctx.created.push(template.path);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Auto-gerado: sempre sobrescreve. */
|
|
448
|
+
private writeGeneratedFile(
|
|
449
|
+
template: ScaffoldTemplate,
|
|
450
|
+
context: TemplateContext,
|
|
451
|
+
): void {
|
|
452
|
+
const full = join(this.ctx.cwd, template.path);
|
|
453
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
454
|
+
writeFileSync(full, template.render(context));
|
|
455
|
+
this.ctx.generated.push(template.path);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
// Fachada (compatibilidade)
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
|
|
463
|
+
/** Arquivos do usuário (setup). */
|
|
464
|
+
export function writeUserLayer(ctx: ScaffoldContext): void {
|
|
465
|
+
new ProjectScaffolder(ctx).writeUserLayer();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Arquivos auto-gerados (setup/update). */
|
|
469
|
+
export function syncGeneratedFiles(ctx: ScaffoldContext): void {
|
|
470
|
+
new ProjectScaffolder(ctx).syncGenerated();
|
|
362
471
|
}
|
|
@@ -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 = {
|
|
@@ -35,6 +27,20 @@ export type HoundsmanConcurrency = {
|
|
|
35
27
|
jev?: number;
|
|
36
28
|
};
|
|
37
29
|
|
|
30
|
+
/** Backend de persistência (reviews, findings e artefatos). */
|
|
31
|
+
export type HoundsmanStorage = {
|
|
32
|
+
/**
|
|
33
|
+
* Provider de storage (default: `sqlite`). `file` mantém o formato antigo
|
|
34
|
+
* em arquivos, mas sob o diretório global do houndsman.
|
|
35
|
+
*/
|
|
36
|
+
provider?: "sqlite" | "file";
|
|
37
|
+
/**
|
|
38
|
+
* Caminho do banco (sqlite) ou raiz das runs (file). Default:
|
|
39
|
+
* `<HOUNDSMAN_HOME>/houndsman.db` / `<HOUNDSMAN_HOME>/runs`.
|
|
40
|
+
*/
|
|
41
|
+
path?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
38
44
|
/**
|
|
39
45
|
* Configuração completa do motor de revisão para um projeto.
|
|
40
46
|
*
|
|
@@ -43,8 +49,8 @@ export type HoundsmanConcurrency = {
|
|
|
43
49
|
export type HoundsmanConfig<TId extends string = string> = {
|
|
44
50
|
/** Nome legível do projeto (usado em prompts e docs). */
|
|
45
51
|
projectName: string;
|
|
46
|
-
/**
|
|
47
|
-
|
|
52
|
+
/** Persistência das runs e artefatos (default: SQLite global). */
|
|
53
|
+
storage?: HoundsmanStorage;
|
|
48
54
|
/**
|
|
49
55
|
* Ids dos watchdogs — **derivados** das chaves de `watchdogs` por
|
|
50
56
|
* `defineHoundsmanConfig` (não informar manualmente).
|
|
@@ -80,22 +86,15 @@ export function defineHoundsmanConfig<TId extends string>(
|
|
|
80
86
|
return { ...input, watchdogIds: Object.keys(input.watchdogs) as TId[] };
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
/** Diretório dos artefatos — o valor do projeto ou o default da camada. */
|
|
84
|
-
export function resolveArtifactDir(config: HoundsmanConfig): string {
|
|
85
|
-
return config.artifactDir ?? ARTIFACT_DIR;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
89
|
/**
|
|
89
90
|
* Teto de arquivos por brief. Valor ausente/não-positivo cai no default —
|
|
90
91
|
* mantém o contexto do subagente limitado em PRs grandes.
|
|
91
92
|
*/
|
|
92
93
|
export function resolveMaxFilesPerBrief(config: HoundsmanConfig): number {
|
|
93
|
-
|
|
94
|
-
return value !== undefined && value > 0 ? value : DEFAULT_MAX_FILES_PER_BRIEF;
|
|
94
|
+
return new ProjectConfig(config).maxFilesPerBrief;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/** Concorrência das chamadas Jev (valor ausente/não-positivo cai no default). */
|
|
98
98
|
export function resolveJevConcurrency(config: HoundsmanConfig): number {
|
|
99
|
-
|
|
100
|
-
return value !== undefined && value > 0 ? value : DEFAULT_JEV_CONCURRENCY;
|
|
99
|
+
return new ProjectConfig(config).jevConcurrency;
|
|
101
100
|
}
|