@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.
Files changed (107) hide show
  1. package/README.md +18 -6
  2. package/package.json +1 -1
  3. package/src/cli/arg-parser.ts +8 -0
  4. package/src/cli/commands/command.ts +23 -0
  5. package/src/cli/commands/context-command.ts +40 -0
  6. package/src/cli/commands/doctor-command.ts +60 -0
  7. package/src/cli/commands/findings-command.ts +38 -0
  8. package/src/cli/commands/help-command.ts +13 -0
  9. package/src/cli/commands/registry.ts +34 -0
  10. package/src/cli/commands/review-command.ts +21 -0
  11. package/src/cli/commands/server-command.ts +37 -0
  12. package/src/cli/commands/setup-command.ts +34 -0
  13. package/src/cli/commands/watchdog-command.ts +34 -0
  14. package/src/cli/help.ts +47 -0
  15. package/src/cli/index.ts +60 -268
  16. package/src/core/arg-parser.ts +48 -0
  17. package/src/core/commands/context.ts +29 -13
  18. package/src/core/commands/findings.ts +185 -260
  19. package/src/core/commands/review.ts +21 -39
  20. package/src/core/commands/scaffold.ts +189 -80
  21. package/src/core/commands/setup.ts +16 -11
  22. package/src/core/commands/update.ts +7 -7
  23. package/src/core/commands/watchdog.ts +8 -80
  24. package/src/core/config.ts +24 -25
  25. package/src/core/defaults.ts +20 -0
  26. package/src/core/diff-source.ts +1 -13
  27. package/src/core/doc-discoverer.ts +25 -15
  28. package/src/core/domain/budget.ts +42 -0
  29. package/src/core/domain/classification.ts +94 -0
  30. package/src/core/domain/diff.ts +53 -0
  31. package/src/core/domain/watchdog.ts +96 -0
  32. package/src/core/engine/carryover.ts +67 -0
  33. package/src/core/engine/houndsman-engine.ts +65 -0
  34. package/src/core/engine/review-scope.ts +95 -0
  35. package/src/core/engine/review-session.ts +79 -0
  36. package/src/core/file-classifier.ts +11 -67
  37. package/src/core/findings-state.ts +193 -100
  38. package/src/core/git-revision.ts +14 -40
  39. package/src/core/git-runner.ts +46 -0
  40. package/src/core/github-review.ts +4 -33
  41. package/src/core/jev/jev-candidates.ts +49 -35
  42. package/src/core/orchestrator.ts +247 -421
  43. package/src/core/paths.ts +26 -11
  44. package/src/core/pr-context.ts +39 -51
  45. package/src/core/project-config.ts +56 -0
  46. package/src/core/renderers/batch-presenter.ts +40 -0
  47. package/src/core/renderers/brief-renderer.ts +23 -0
  48. package/src/core/renderers/findings-presenter.ts +74 -0
  49. package/src/core/renderers/manifest-writer.ts +123 -0
  50. package/src/core/renderers/report-renderer.ts +66 -0
  51. package/src/core/renderers/verify-presenter.ts +50 -0
  52. package/src/core/renderers/watchdog-presenter.ts +100 -0
  53. package/src/core/report-builder.ts +12 -7
  54. package/src/core/run-layout.ts +8 -0
  55. package/src/core/run-status.ts +29 -59
  56. package/src/core/services/clock.ts +12 -0
  57. package/src/core/services/file-classifier.ts +34 -0
  58. package/src/core/services/fix-batch-builder.ts +13 -0
  59. package/src/core/services/gh-client.ts +54 -0
  60. package/src/core/services/git-repository.ts +139 -0
  61. package/src/core/services/project-files.ts +91 -0
  62. package/src/core/services/verifier.ts +58 -0
  63. package/src/core/storage/factory.ts +24 -0
  64. package/src/core/storage/file/file-storage.ts +269 -0
  65. package/src/core/storage/paths.ts +64 -0
  66. package/src/core/storage/repo-identity.ts +70 -0
  67. package/src/core/storage/sqlite/mappers.ts +160 -0
  68. package/src/core/storage/sqlite/schema.ts +108 -0
  69. package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
  70. package/src/core/storage/types.ts +143 -0
  71. package/src/core/storage/work-area.ts +191 -0
  72. package/src/core/verify.ts +2 -1
  73. package/src/core/watchdog-registry.ts +58 -16
  74. package/src/index.ts +57 -6
  75. package/src/server/artifacts.ts +8 -61
  76. package/src/server/client/ArtifactsPanel.tsx +137 -23
  77. package/src/server/client/FindingCard.tsx +37 -6
  78. package/src/server/client/MarkdownText.tsx +101 -0
  79. package/src/server/client/SubmitModal.tsx +9 -4
  80. package/src/server/client/code-highlight.ts +422 -0
  81. package/src/server/client/constants.ts +130 -24
  82. package/src/server/client/json-highlight.ts +55 -0
  83. package/src/server/client/markdown.ts +153 -0
  84. package/src/server/client/types.ts +23 -0
  85. package/src/server/index.ts +155 -245
  86. package/src/server/run-repository.ts +299 -0
  87. package/src/skill/OUTPUT-FORMAT.md +6 -4
  88. package/src/skill/README.md +22 -14
  89. package/src/skill/SETUP.md +4 -4
  90. package/src/skill/fix.md +5 -5
  91. package/src/skill/hunt.md +27 -18
  92. package/src/skill/train.md +1 -1
  93. package/src/skill/verify.md +5 -5
  94. package/src/tests/__snapshots__/baseline.test.ts.snap +825 -0
  95. package/src/tests/baseline.test.ts +555 -493
  96. package/src/tests/cli.test.ts +40 -19
  97. package/src/tests/code-highlight.test.ts +123 -0
  98. package/src/tests/domain.test.ts +328 -0
  99. package/src/tests/findings-commands.test.ts +153 -131
  100. package/src/tests/findings-state.test.ts +6 -3
  101. package/src/tests/fixtures/fake-project.ts +0 -1
  102. package/src/tests/infrastructure.test.ts +205 -0
  103. package/src/tests/markdown.test.ts +89 -0
  104. package/src/tests/review-scripts.test.ts +8 -5
  105. package/src/tests/run-status.test.ts +33 -23
  106. package/src/tests/server.test.ts +108 -75
  107. package/src/tests/storage.test.ts +386 -0
@@ -2,7 +2,15 @@
2
2
  * Constantes de apresentação do cliente (labels, ordem, emojis).
3
3
  */
4
4
 
5
- import type { Decision, ExecState, RunStatus, RunSummary, Severity } from "./types";
5
+ import type {
6
+ ArtifactKind,
7
+ ArtifactMeta,
8
+ Decision,
9
+ ExecState,
10
+ RunStatus,
11
+ RunSummary,
12
+ Severity,
13
+ } from "./types";
6
14
 
7
15
  /** Rótulo de exibição de cada severidade (o CSS dá a cor via `sev-<nivel>`). */
8
16
  export const SEVERITY_LABELS: Record<Severity, string> = {
@@ -58,34 +66,132 @@ export function describeRunStatus(
58
66
  return { kind: status, label: RUN_STATUS_LABELS[status], timeout: false };
59
67
  }
60
68
 
61
- /** Artefatos que podem ser lidos na aba "Artefatos". */
62
- export const ARTIFACTS = [
63
- "manifest.json",
64
- "classification.json",
65
- "candidates.json",
66
- "context-summary.json",
67
- "review.md",
68
- ] as const;
69
-
70
- export type ArtifactName = (typeof ARTIFACTS)[number];
71
-
72
- /** Descrição de cada artefato (mostrada ao selecioná-lo na aba). */
73
- export const ARTIFACT_INFO: Record<ArtifactName, string> = {
74
- "manifest.json":
75
- "Resumo da execução: watchdogs ativos, arquivos, orçamento, churn, Jev e contexto da PR.",
76
- "classification.json":
77
- "Classificação do diff: watchdogs ativados por arquivo, status (A/M/D/R) e churn por arquivo.",
78
- "candidates.json":
79
- "Indícios da pré-análise Jev (regra + arquivo + confiança) — não são achados.",
80
- "context-summary.json":
81
- "Resumo da PR (objetivo, motivação e alterações) gerado no Passo 0.",
82
- "review.md": "Relatório consolidado da revisão — o entregável.",
69
+ /** Ordem canônica dos tipos de artefato na lista da UI. */
70
+ const KIND_ORDER: ArtifactKind[] = [
71
+ "review",
72
+ "domain-review",
73
+ "verify",
74
+ "brief",
75
+ "manifest",
76
+ "classification",
77
+ "candidates",
78
+ "context",
79
+ "context-brief",
80
+ "context-summary",
81
+ "finding-report",
82
+ "other",
83
+ ];
84
+
85
+ /** Grupo visual de cada tipo (relatórios, briefs, núcleo, contexto…). */
86
+ const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: number }> =
87
+ {
88
+ review: { key: "report", label: "Relatórios", order: 0 },
89
+ "domain-review": { key: "report", label: "Relatórios", order: 0 },
90
+ verify: { key: "verify", label: "Verificação", order: 1 },
91
+ brief: { key: "brief", label: "Briefs (auditoria)", order: 2 },
92
+ manifest: { key: "core", label: "Núcleo da revisão", order: 3 },
93
+ classification: { key: "core", label: "Núcleo da revisão", order: 3 },
94
+ candidates: { key: "core", label: "Núcleo da revisão", order: 3 },
95
+ context: { key: "context", label: "Contexto da PR", order: 4 },
96
+ "context-brief": { key: "context", label: "Contexto da PR", order: 4 },
97
+ "context-summary": { key: "context", label: "Contexto da PR", order: 4 },
98
+ "finding-report": { key: "other", label: "Outros", order: 5 },
99
+ other: { key: "other", label: "Outros", order: 5 },
100
+ };
101
+
102
+ /** Rótulo curto do tipo (badge do item). */
103
+ export const ARTIFACT_KIND_LABELS: Record<ArtifactKind, string> = {
104
+ review: "relatório",
105
+ "domain-review": "domínio",
106
+ verify: "verify",
107
+ brief: "brief",
108
+ manifest: "manifesto",
109
+ classification: "classificação",
110
+ candidates: "jev",
111
+ context: "contexto",
112
+ "context-brief": "brief ctx",
113
+ "context-summary": "resumo ctx",
114
+ "finding-report": "findings",
115
+ other: "outro",
83
116
  };
84
117
 
118
+ /** Nome exibido (briefs perdem o prefixo `briefs/`). */
119
+ export function displayArtifactName(meta: ArtifactMeta): string {
120
+ return meta.name.startsWith("briefs/")
121
+ ? meta.name.slice("briefs/".length)
122
+ : meta.name;
123
+ }
124
+
125
+ /** Descrição curta do artefato (cabeçalho do painel). */
126
+ export function describeArtifact(meta: ArtifactMeta): string {
127
+ switch (meta.kind) {
128
+ case "review":
129
+ return "Relatório consolidado da revisão — o entregável.";
130
+ case "domain-review":
131
+ return "Relatório isolado do domínio: achados daquele watchdog.";
132
+ case "verify":
133
+ return "Resultado do `findings verify` (resolvidos, persistentes e incertos).";
134
+ case "brief":
135
+ return "Instruções enviadas ao subagente (auditoria do prompt).";
136
+ case "manifest":
137
+ return "Resumo da execução: watchdogs, arquivos, orçamento, churn, Jev e contexto.";
138
+ case "classification":
139
+ return "Classificação do diff: watchdogs ativados por arquivo, status (A/M/D/R) e churn.";
140
+ case "candidates":
141
+ return "Indícios da pré-análise Jev (regra + arquivo + confiança) — não são achados.";
142
+ case "context":
143
+ return "Payload bruto da PR coletado via `gh` (ou marcador de indisponível).";
144
+ case "context-brief":
145
+ return "Brief do subagente que resume o contexto da PR.";
146
+ case "context-summary":
147
+ return "Resumo da PR (objetivo, motivação e alterações) gerado no Passo 0.";
148
+ default:
149
+ return meta.name;
150
+ }
151
+ }
152
+
153
+ /** `true` se o conteúdo deve ser renderizado com destaque de JSON. */
154
+ export function isJsonArtifact(meta: ArtifactMeta | undefined): boolean {
155
+ if (!meta) return false;
156
+ return meta.contentType.includes("json") || meta.name.toLowerCase().endsWith(".json");
157
+ }
158
+
159
+ /** Agrupa os artefatos para a navegação lateral (ordem estável). */
160
+ export function groupArtifacts(
161
+ items: ArtifactMeta[],
162
+ ): { key: string; label: string; items: ArtifactMeta[] }[] {
163
+ const groups = new Map<
164
+ string,
165
+ { key: string; label: string; order: number; items: ArtifactMeta[] }
166
+ >();
167
+ for (const item of items) {
168
+ const meta = GROUP_BY_KIND[item.kind] ?? GROUP_BY_KIND.other;
169
+ const group = groups.get(meta.key) ?? {
170
+ key: meta.key,
171
+ label: meta.label,
172
+ order: meta.order,
173
+ items: [],
174
+ };
175
+ group.items.push(item);
176
+ groups.set(meta.key, group);
177
+ }
178
+ return [...groups.values()]
179
+ .sort((a, b) => a.order - b.order)
180
+ .map((group) => ({
181
+ key: group.key,
182
+ label: group.label,
183
+ items: group.items.sort(
184
+ (a, b) =>
185
+ KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind) ||
186
+ a.name.localeCompare(b.name),
187
+ ),
188
+ }));
189
+ }
190
+
85
191
  /** Legenda única de cada aba (explica o que a aba é; mostrada quando selecionada). */
86
192
  export const TAB_CAPTIONS = {
87
193
  findings:
88
194
  "Triagem dos achados da revisão: decida Executar, Ignorar ou deixe Pendente — o comentário orienta o auto-fix. Agente: houndsman findings list --decision pending --json · findings done <id>.",
89
195
  artifacts:
90
- "Artefatos brutos da run: manifesto, classificação, candidatos Jev, contexto da PR e o relatório consolidado (review.md).",
196
+ "Artefatos persistidos da run: relatórios, briefs de auditoria, manifesto, classificação, Jev, contexto da PR e verificação. JSON com destaque de sintaxe.",
91
197
  } as const;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Destaque de sintaxe JSON (cliente) — implementação própria e leve.
3
+ *
4
+ * Não puxa lib de highlight: tokeniza o texto já formatado e devolve spans.
5
+ * Funciona para qualquer JSON gerado pelo houndsman (manifest, classification,
6
+ * context, candidates, …).
7
+ */
8
+
9
+ export type JsonTokenType = "plain" | "key" | "string" | "number" | "literal" | "punct";
10
+
11
+ export type JsonToken = { text: string; type: JsonTokenType };
12
+
13
+ const TOKEN =
14
+ /("(?:\\.|[^"\\])*")(\s*:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|\b(true|false|null)\b|([{}[\],:])/g;
15
+
16
+ /** Pretty-print best-effort; devolve o original se não for JSON válido. */
17
+ export function formatJson(text: string): string {
18
+ try {
19
+ return JSON.stringify(JSON.parse(text), null, 2);
20
+ } catch {
21
+ return text;
22
+ }
23
+ }
24
+
25
+ /** Tokeniza o JSON preservando o texto original (inclusive espaços/quebras). */
26
+ export function highlightJson(text: string): JsonToken[] {
27
+ const tokens: JsonToken[] = [];
28
+ let last = 0;
29
+ for (const match of text.matchAll(TOKEN)) {
30
+ const index = match.index ?? 0;
31
+ if (index > last) {
32
+ tokens.push({ text: text.slice(last, index), type: "plain" });
33
+ }
34
+ if (match[1] !== undefined) {
35
+ tokens.push({
36
+ text: match[1],
37
+ type: match[2] !== undefined ? "key" : "string",
38
+ });
39
+ if (match[2] !== undefined) {
40
+ tokens.push({ text: match[2], type: "punct" });
41
+ }
42
+ } else if (match[3] !== undefined) {
43
+ tokens.push({ text: match[3], type: "number" });
44
+ } else if (match[4] !== undefined) {
45
+ tokens.push({ text: match[4], type: "literal" });
46
+ } else if (match[5] !== undefined) {
47
+ tokens.push({ text: match[5], type: "punct" });
48
+ }
49
+ last = index + match[0].length;
50
+ }
51
+ if (last < text.length) {
52
+ tokens.push({ text: text.slice(last), type: "plain" });
53
+ }
54
+ return tokens;
55
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Markdown mínimo do campo `action` (cliente).
3
+ *
4
+ * Suporta o suficiente para instruções curtas: parágrafos, quebras de linha,
5
+ * listas, blockquote, blocos de código e formatação inline (`code`, **bold**,
6
+ * *itálico*, links). Sem lib externa e sem HTML cru.
7
+ */
8
+
9
+ export type MarkdownBlock =
10
+ | { type: "paragraph"; text: string }
11
+ | { type: "list"; ordered: boolean; items: string[] }
12
+ | { type: "code"; language: string | null; code: string }
13
+ | { type: "quote"; text: string };
14
+
15
+ export type InlineToken =
16
+ | { type: "text"; text: string }
17
+ | { type: "code"; text: string }
18
+ | { type: "strong"; text: string }
19
+ | { type: "em"; text: string }
20
+ | { type: "link"; text: string; url: string };
21
+
22
+ const FENCE = /^\s*```(\S*)\s*$/;
23
+ const BULLET = /^\s*[-*+]\s+(.*)$/;
24
+ const ORDERED = /^\s*\d+\.\s+(.*)$/;
25
+ const QUOTE = /^\s*>\s?(.*)$/;
26
+
27
+ function isBlockStart(line: string): boolean {
28
+ return (
29
+ FENCE.test(line) || BULLET.test(line) || ORDERED.test(line) || QUOTE.test(line)
30
+ );
31
+ }
32
+
33
+ /** Divide o texto em blocos simples (sem aninhamento profundo). */
34
+ export function parseMarkdown(text: string): MarkdownBlock[] {
35
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
36
+ const blocks: MarkdownBlock[] = [];
37
+ let index = 0;
38
+
39
+ while (index < lines.length) {
40
+ const line = lines[index];
41
+ if (line.trim().length === 0) {
42
+ index++;
43
+ continue;
44
+ }
45
+
46
+ const fence = FENCE.exec(line);
47
+ if (fence) {
48
+ const language = fence[1] || null;
49
+ const code: string[] = [];
50
+ index++;
51
+ while (index < lines.length && !FENCE.test(lines[index])) {
52
+ code.push(lines[index]);
53
+ index++;
54
+ }
55
+ if (index < lines.length) index++;
56
+ blocks.push({ type: "code", language, code: code.join("\n") });
57
+ continue;
58
+ }
59
+
60
+ if (BULLET.test(line)) {
61
+ const items: string[] = [];
62
+ while (index < lines.length) {
63
+ const item = BULLET.exec(lines[index]);
64
+ if (!item) break;
65
+ items.push(item[1]);
66
+ index++;
67
+ }
68
+ blocks.push({ type: "list", ordered: false, items });
69
+ continue;
70
+ }
71
+
72
+ if (ORDERED.test(line)) {
73
+ const items: string[] = [];
74
+ while (index < lines.length) {
75
+ const item = ORDERED.exec(lines[index]);
76
+ if (!item) break;
77
+ items.push(item[1]);
78
+ index++;
79
+ }
80
+ blocks.push({ type: "list", ordered: true, items });
81
+ continue;
82
+ }
83
+
84
+ if (QUOTE.test(line)) {
85
+ const quote: string[] = [];
86
+ while (index < lines.length) {
87
+ const item = QUOTE.exec(lines[index]);
88
+ if (!item) break;
89
+ quote.push(item[1]);
90
+ index++;
91
+ }
92
+ blocks.push({ type: "quote", text: quote.join("\n") });
93
+ continue;
94
+ }
95
+
96
+ const paragraph: string[] = [line];
97
+ index++;
98
+ while (
99
+ index < lines.length &&
100
+ lines[index].trim().length > 0 &&
101
+ !isBlockStart(lines[index])
102
+ ) {
103
+ paragraph.push(lines[index]);
104
+ index++;
105
+ }
106
+ blocks.push({ type: "paragraph", text: paragraph.join("\n") });
107
+ }
108
+
109
+ return blocks;
110
+ }
111
+
112
+ /** Tokeniza formatação inline preservando o texto e a ordem dos marcadores. */
113
+ export function parseInline(text: string): InlineToken[] {
114
+ const tokens: InlineToken[] = [];
115
+ // Ordem importa: code/bold antes de itálico; `_` exige fronteira de palavra.
116
+ const pattern =
117
+ /`[^`\n]+`|\*\*[^*\n]+\*\*|__[^_\n]+__|\*[^*\n]+\*|(?<![\w])_[^_\n]+_(?![\w])|\[[^\]\n]+\]\([^)\s]+\)/g;
118
+ let last = 0;
119
+ for (const match of text.matchAll(pattern)) {
120
+ const start = match.index ?? 0;
121
+ if (start > last) {
122
+ tokens.push({ type: "text", text: text.slice(last, start) });
123
+ }
124
+ const raw = match[0];
125
+ if (raw.startsWith("`")) {
126
+ tokens.push({ type: "code", text: raw.slice(1, -1) });
127
+ } else if (raw.startsWith("**") || raw.startsWith("__")) {
128
+ tokens.push({ type: "strong", text: raw.slice(2, -2) });
129
+ } else if (raw.startsWith("*") || raw.startsWith("_")) {
130
+ tokens.push({ type: "em", text: raw.slice(1, -1) });
131
+ } else {
132
+ const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(raw);
133
+ if (link) {
134
+ tokens.push({ type: "link", text: link[1], url: link[2] });
135
+ } else {
136
+ tokens.push({ type: "text", text: raw });
137
+ }
138
+ }
139
+ last = start + raw.length;
140
+ }
141
+ if (last < text.length) {
142
+ tokens.push({ type: "text", text: text.slice(last) });
143
+ }
144
+ return tokens;
145
+ }
146
+
147
+ /** URL segura para links de markdown (evita `javascript:` e afins). */
148
+ export function safeUrl(url: string): string | null {
149
+ const value = url.trim();
150
+ if (/^(https?:|mailto:)/i.test(value)) return value;
151
+ if (value.startsWith("/") && !value.startsWith("//")) return value;
152
+ return null;
153
+ }
@@ -50,6 +50,29 @@ export type Summary = {
50
50
  bySeverity: Record<Severity, number>;
51
51
  };
52
52
 
53
+ /** Espécie de artefato persistido (espelha `core/storage/types.ts`). */
54
+ export type ArtifactKind =
55
+ | "brief"
56
+ | "classification"
57
+ | "manifest"
58
+ | "candidates"
59
+ | "context"
60
+ | "context-brief"
61
+ | "context-summary"
62
+ | "finding-report"
63
+ | "review"
64
+ | "domain-review"
65
+ | "verify"
66
+ | "other";
67
+
68
+ /** Metadados de artefato retornados pela API (sem conteúdo). */
69
+ export type ArtifactMeta = {
70
+ name: string;
71
+ kind: ArtifactKind;
72
+ contentType: string;
73
+ updatedAt: string;
74
+ };
75
+
53
76
  export type RunSummary = {
54
77
  slug: string;
55
78
  branch: string;