@payconductor/houndsman 0.4.0 → 0.6.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 (196) hide show
  1. package/README.md +35 -41
  2. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  3. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  4. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  5. package/assets/grammars/web-tree-sitter.wasm +0 -0
  6. package/package.json +11 -3
  7. package/src/cli/commands/command.ts +5 -0
  8. package/src/cli/commands/doctor-command.ts +116 -24
  9. package/src/cli/commands/hunt-command.ts +25 -0
  10. package/src/cli/commands/status-command.ts +46 -0
  11. package/src/cli/help.ts +18 -1
  12. package/src/cli/index.ts +13 -6
  13. package/src/core/ai/ai-client.ts +221 -0
  14. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  15. package/src/core/ai/errors.ts +36 -0
  16. package/src/core/ai/features/brief-enricher.ts +62 -0
  17. package/src/core/ai/features/classifier.ts +50 -0
  18. package/src/core/ai/features/context-summarizer.ts +31 -0
  19. package/src/core/ai/features/pre-analyzer.ts +51 -0
  20. package/src/core/ai/features/verifier.ts +45 -0
  21. package/src/core/ai/index.ts +56 -0
  22. package/src/core/ai/model-factory.ts +137 -0
  23. package/src/core/ai/prompts/base.ts +36 -0
  24. package/src/core/ai/prompts/brief.ts +87 -0
  25. package/src/core/ai/prompts/classification.ts +36 -0
  26. package/src/core/ai/prompts/context.ts +29 -0
  27. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  28. package/src/core/ai/prompts/verify.ts +40 -0
  29. package/src/core/ai/schemas.ts +110 -0
  30. package/src/core/ai/types.ts +76 -0
  31. package/src/core/ai/usage-budget.ts +75 -0
  32. package/src/core/ai/usage-recorder.ts +79 -0
  33. package/src/core/assets.ts +16 -0
  34. package/src/core/ast/diff.ts +220 -0
  35. package/src/core/ast/impact.ts +127 -0
  36. package/src/core/ast/parser.ts +167 -0
  37. package/src/core/ast/service.ts +233 -0
  38. package/src/core/ast/symbols.ts +294 -0
  39. package/src/core/ast/types.ts +125 -0
  40. package/src/core/brief.ts +138 -0
  41. package/src/core/commands/findings.ts +120 -6
  42. package/src/core/commands/review.ts +2 -0
  43. package/src/core/commands/scaffold.ts +159 -10
  44. package/src/core/commands/status.ts +63 -0
  45. package/src/core/commands/update.ts +5 -3
  46. package/src/core/config.ts +85 -0
  47. package/src/core/defaults.ts +27 -0
  48. package/src/core/domain/watchdog.ts +43 -5
  49. package/src/core/engine/houndsman-engine.ts +16 -1
  50. package/src/core/engine/review-scope.ts +77 -13
  51. package/src/core/engine/review-session.ts +27 -2
  52. package/src/core/env.ts +101 -0
  53. package/src/core/file-classifier.ts +25 -4
  54. package/src/core/findings-state.ts +21 -4
  55. package/src/core/github-review.ts +620 -57
  56. package/src/core/jev/jev-client.ts +8 -7
  57. package/src/core/jev/jev-verify.ts +2 -1
  58. package/src/core/logging/index.ts +17 -0
  59. package/src/core/logging/logger.ts +187 -0
  60. package/src/core/orchestrator.ts +454 -16
  61. package/src/core/paths.ts +3 -0
  62. package/src/core/project-config.ts +123 -1
  63. package/src/core/registry-check.ts +148 -6
  64. package/src/core/renderers/manifest-writer.ts +33 -0
  65. package/src/core/renderers/resume-renderer.ts +151 -0
  66. package/src/core/report-builder.ts +7 -4
  67. package/src/core/rule-text.ts +97 -0
  68. package/src/core/run-status.ts +39 -5
  69. package/src/core/services/file-classifier.ts +10 -3
  70. package/src/core/services/verifier.ts +14 -3
  71. package/src/core/storage/file/file-storage.ts +2 -0
  72. package/src/core/storage/paths.ts +6 -5
  73. package/src/core/storage/sqlite/mappers.ts +2 -0
  74. package/src/core/storage/sqlite/schema.ts +9 -1
  75. package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
  76. package/src/core/storage/types.ts +15 -0
  77. package/src/core/storage/work-area.ts +12 -0
  78. package/src/core/types.ts +37 -1
  79. package/src/core/verify.ts +17 -5
  80. package/src/harness/README.md +15 -0
  81. package/src/harness/agent/agent-context.ts +33 -0
  82. package/src/harness/agent/agent-errors.ts +30 -0
  83. package/src/harness/agent/agent-factory.ts +45 -0
  84. package/src/harness/agent/agent-reporter.ts +34 -0
  85. package/src/harness/agent/agent-result.ts +16 -0
  86. package/src/harness/agent/agent-runner.ts +32 -0
  87. package/src/harness/agent/agent-scheduler.ts +37 -0
  88. package/src/harness/agent/review-agent.ts +391 -0
  89. package/src/harness/config/harness-config.ts +84 -0
  90. package/src/harness/hunt/hunt-harness.ts +83 -0
  91. package/src/harness/hunt/hunt-options.ts +75 -0
  92. package/src/harness/hunt/hunt-result.ts +17 -0
  93. package/src/harness/hunt/hunt-session.ts +372 -0
  94. package/src/harness/hunt/review-planner.ts +98 -0
  95. package/src/harness/index.ts +36 -0
  96. package/src/harness/investigation/agent-state.ts +134 -0
  97. package/src/harness/investigation/evidence-verifier.ts +129 -0
  98. package/src/harness/investigation/evidence.ts +110 -0
  99. package/src/harness/investigation/finding-critic.ts +96 -0
  100. package/src/harness/investigation/investigation-planner.ts +118 -0
  101. package/src/harness/investigation/investigation-task.ts +10 -0
  102. package/src/harness/investigation/review-journal.ts +87 -0
  103. package/src/harness/investigation/rule-analyzer.ts +178 -0
  104. package/src/harness/investigation/rule-signals.ts +110 -0
  105. package/src/harness/prompts/agent-schemas.ts +57 -0
  106. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  107. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  108. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  109. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  110. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  111. package/src/harness/tools/agent-tool.ts +102 -0
  112. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  113. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  114. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  115. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  116. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  117. package/src/harness/tools/builtin/helpers.ts +115 -0
  118. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  119. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  120. package/src/harness/tools/builtin/index.ts +59 -0
  121. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  122. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  123. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  124. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  125. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  126. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  127. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  128. package/src/harness/tools/path-guard.ts +90 -0
  129. package/src/harness/tools/result-limiter.ts +49 -0
  130. package/src/harness/tools/secret-redactor.ts +54 -0
  131. package/src/harness/tools/tool-cache.ts +50 -0
  132. package/src/harness/tools/tool-call-budget.ts +60 -0
  133. package/src/harness/tools/tool-context.ts +5 -0
  134. package/src/harness/tools/tool-middleware.ts +105 -0
  135. package/src/harness/tools/tool-policy.ts +51 -0
  136. package/src/harness/tools/tool-registry.ts +112 -0
  137. package/src/harness/tools/tool-result.ts +15 -0
  138. package/src/harness/tools/toolbox.ts +36 -0
  139. package/src/harness/validation/finding-validator.ts +119 -0
  140. package/src/index.ts +46 -0
  141. package/src/server/client/App.tsx +26 -9
  142. package/src/server/client/ArtifactsPanel.tsx +12 -5
  143. package/src/server/client/AutoRefresh.tsx +1 -1
  144. package/src/server/client/Filters.tsx +7 -2
  145. package/src/server/client/FindingCard.tsx +14 -10
  146. package/src/server/client/FindingsList.tsx +2 -2
  147. package/src/server/client/MarkdownText.tsx +92 -12
  148. package/src/server/client/ResumePanel.tsx +368 -0
  149. package/src/server/client/RunList.tsx +3 -3
  150. package/src/server/client/SubmitModal.tsx +49 -6
  151. package/src/server/client/SummaryBar.tsx +1 -1
  152. package/src/server/client/icons.tsx +17 -0
  153. package/src/server/client/{constants.ts → utils/constants.ts} +61 -10
  154. package/src/server/client/utils/markdown.ts +319 -0
  155. package/src/server/client/{types.ts → utils/types.ts} +64 -0
  156. package/src/server/index.ts +66 -7
  157. package/src/server/run-repository.ts +9 -0
  158. package/src/skill/OUTPUT-FORMAT.md +12 -0
  159. package/src/skill/SETUP.md +6 -2
  160. package/src/skill/example-review.md +22 -14
  161. package/src/skill/hunt.md +45 -19
  162. package/src/skill/verify.md +3 -2
  163. package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
  164. package/src/tests/ai-core.test.ts +120 -0
  165. package/src/tests/ai-features.test.ts +183 -0
  166. package/src/tests/ast.test.ts +335 -0
  167. package/src/tests/baseline.test.ts +3 -0
  168. package/src/tests/code-highlight.test.ts +1 -1
  169. package/src/tests/env.test.ts +69 -0
  170. package/src/tests/fallback.test.ts +172 -0
  171. package/src/tests/findings-state.test.ts +50 -2
  172. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  173. package/src/tests/fixtures/fake-investigation.ts +64 -0
  174. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  175. package/src/tests/github-review.test.ts +409 -60
  176. package/src/tests/harness-agent.test.ts +122 -0
  177. package/src/tests/harness-investigation.test.ts +206 -0
  178. package/src/tests/harness-tools.test.ts +148 -0
  179. package/src/tests/hunt-command.test.ts +212 -0
  180. package/src/tests/logger.test.ts +83 -0
  181. package/src/tests/markdown.test.ts +61 -1
  182. package/src/tests/review-resume.test.ts +101 -0
  183. package/src/tests/review-scope.test.ts +302 -0
  184. package/src/tests/review-scripts.test.ts +73 -0
  185. package/src/tests/rule-analyzer.test.ts +61 -0
  186. package/src/tests/run-status-view.test.ts +94 -0
  187. package/src/tests/run-status.test.ts +53 -0
  188. package/src/tests/server.test.ts +76 -21
  189. package/src/tests/storage.test.ts +63 -0
  190. package/src/tests/tool-contracts.test.ts +82 -0
  191. package/src/tests/verify.test.ts +1 -1
  192. package/src/server/client/markdown.ts +0 -153
  193. /package/src/server/client/{api.ts → utils/api.ts} +0 -0
  194. /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
  195. /package/src/server/client/{format.ts → utils/format.ts} +0 -0
  196. /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
@@ -3,7 +3,7 @@
3
3
  * progresso da correção (feitos ÷ total).
4
4
  */
5
5
 
6
- import type { Summary } from "./types";
6
+ import type { Summary } from "./utils/types";
7
7
 
8
8
  function Kpi({
9
9
  label,
@@ -51,6 +51,14 @@ export const ExternalLink = (p: IconProps) => (
51
51
  </Svg>
52
52
  );
53
53
 
54
+ /** Marca do GitHub (contorno) — usada em achados já publicados na PR. */
55
+ export const GitHub = (p: IconProps) => (
56
+ <Svg {...p}>
57
+ <path d="M9 19c-4.3 1.3-4.3-2.2-6-2.7" />
58
+ <path d="M15 22v-3.9a3.4 3.4 0 0 0-.9-2.6c3-.3 6.1-1.5 6.1-6.8a5.3 5.3 0 0 0-1.4-3.7 4.9 4.9 0 0 0-.1-3.6s-1.1-.4-3.7 1.4a12.7 12.7 0 0 0-6.8 0C5.6 1 4.5 1.4 4.5 1.4a4.9 4.9 0 0 0-.1 3.6A5.3 5.3 0 0 0 3 8.7c0 5.2 3.1 6.4 6.1 6.8a3.4 3.4 0 0 0-.9 2.6V22" />
59
+ </Svg>
60
+ );
61
+
54
62
  export const Sparkles = (p: IconProps) => (
55
63
  <Svg {...p}>
56
64
  <path d="M11 3.5l1.7 4.3 4.3 1.7-4.3 1.7L11 15.5 9.3 11.2 5 9.5l4.3-1.7z" />
@@ -126,6 +134,15 @@ export const Layers = (p: IconProps) => (
126
134
  </Svg>
127
135
  );
128
136
 
137
+ /** Documento com linhas — aba de resumo. */
138
+ export const FileText = (p: IconProps) => (
139
+ <Svg {...p}>
140
+ <path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" />
141
+ <path d="M14 3v5h5" />
142
+ <path d="M9 13h6M9 17h4" />
143
+ </Svg>
144
+ );
145
+
129
146
  /** Olho — "quem auditou o trecho" (watchdogs envolvidos). */
130
147
  export const Watch = (p: IconProps) => (
131
148
  <Svg {...p}>
@@ -39,15 +39,22 @@ export const RUN_STATUS_LABELS: Record<RunStatus, string> = {
39
39
  fix_done: "Correção pronta",
40
40
  };
41
41
 
42
- /** Limiar de inatividade para exibir timeout (display-only; não é status). */
42
+ /** Limiar de inatividade da correção para exibir timeout (display-only). */
43
43
  export const STATUS_TIMEOUT_MS = 10 * 60 * 1000;
44
44
 
45
+ /** Revisão despacha subagentes em ondas longas — janela maior que a correção. */
46
+ export const REVIEW_TIMEOUT_MS = 30 * 60 * 1000;
47
+
45
48
  /** Intervalo do polling da UI (o badge conta o tempo desde a última sync). */
46
49
  export const AUTO_REFRESH_MS = 10_000;
47
50
 
48
51
  export type RunStatusView = { kind: string; label: string; timeout: boolean };
49
52
 
50
- /** View do status de uma run — label; vira timeout quando ativo e sem ping recente. */
53
+ /**
54
+ * View do status de uma run — timeout só existe quando há um heartbeat de
55
+ * execução (`reviewing`/`fixing`). `updatedAt` muda com triagem/sync e não
56
+ * serve para provar que a skill está rodando.
57
+ */
51
58
  export function describeRunStatus(
52
59
  run: RunSummary,
53
60
  now: number = Date.now(),
@@ -55,8 +62,16 @@ export function describeRunStatus(
55
62
  const status = run.status;
56
63
  if (!status) return null;
57
64
  const active = status === "reviewing" || status === "fixing";
58
- const updated = run.updatedAt ? Date.parse(run.updatedAt) : Number.NaN;
59
- if (active && Number.isFinite(updated) && now - updated > STATUS_TIMEOUT_MS) {
65
+ if (!active) {
66
+ return { kind: status, label: RUN_STATUS_LABELS[status], timeout: false };
67
+ }
68
+
69
+ // Sem heartbeat não há execução conhecida: evita o falso "timeout".
70
+ const heartbeat = run.heartbeatAt ? Date.parse(run.heartbeatAt) : Number.NaN;
71
+ if (!Number.isFinite(heartbeat)) return null;
72
+
73
+ const timeoutMs = status === "reviewing" ? REVIEW_TIMEOUT_MS : STATUS_TIMEOUT_MS;
74
+ if (now - heartbeat > timeoutMs) {
60
75
  return {
61
76
  kind: "timeout",
62
77
  label: status === "reviewing" ? "Timeout na revisão" : "Timeout na correção",
@@ -69,12 +84,19 @@ export function describeRunStatus(
69
84
  /** Ordem canônica dos tipos de artefato na lista da UI. */
70
85
  const KIND_ORDER: ArtifactKind[] = [
71
86
  "review",
87
+ "resume",
72
88
  "domain-review",
73
89
  "verify",
74
90
  "brief",
75
91
  "manifest",
76
92
  "classification",
77
93
  "candidates",
94
+ "ast",
95
+ "ast-impact",
96
+ "ai",
97
+ "ai-usage",
98
+ "ai-brief",
99
+ "harness-transcript",
78
100
  "context",
79
101
  "context-brief",
80
102
  "context-summary",
@@ -86,28 +108,42 @@ const KIND_ORDER: ArtifactKind[] = [
86
108
  const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: number }> =
87
109
  {
88
110
  review: { key: "report", label: "Relatórios", order: 0 },
111
+ resume: { key: "report", label: "Relatórios", order: 0 },
89
112
  "domain-review": { key: "report", label: "Relatórios", order: 0 },
90
113
  verify: { key: "verify", label: "Verificação", order: 1 },
91
114
  brief: { key: "brief", label: "Briefs (auditoria)", order: 2 },
92
115
  manifest: { key: "core", label: "Núcleo da revisão", order: 3 },
93
116
  classification: { key: "core", label: "Núcleo da revisão", order: 3 },
94
117
  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 },
118
+ ast: { key: "core", label: "Núcleo da revisão", order: 3 },
119
+ "ast-impact": { key: "core", label: "Núcleo da revisão", order: 3 },
120
+ ai: { key: "ai", label: "IA e harness", order: 4 },
121
+ "ai-usage": { key: "ai", label: "IA e harness", order: 4 },
122
+ "ai-brief": { key: "ai", label: "IA e harness", order: 4 },
123
+ "harness-transcript": { key: "ai", label: "IA e harness", order: 4 },
124
+ context: { key: "context", label: "Contexto da PR", order: 5 },
125
+ "context-brief": { key: "context", label: "Contexto da PR", order: 5 },
126
+ "context-summary": { key: "context", label: "Contexto da PR", order: 5 },
127
+ "finding-report": { key: "other", label: "Outros", order: 6 },
128
+ other: { key: "other", label: "Outros", order: 6 },
100
129
  };
101
130
 
102
131
  /** Rótulo curto do tipo (badge do item). */
103
132
  export const ARTIFACT_KIND_LABELS: Record<ArtifactKind, string> = {
104
133
  review: "relatório",
134
+ resume: "resumo",
105
135
  "domain-review": "domínio",
106
136
  verify: "verify",
107
137
  brief: "brief",
108
138
  manifest: "manifesto",
109
139
  classification: "classificação",
110
140
  candidates: "jev",
141
+ ast: "ast",
142
+ "ast-impact": "impacto",
143
+ ai: "ia",
144
+ "ai-usage": "uso ia",
145
+ "ai-brief": "plano ia",
146
+ "harness-transcript": "transcript",
111
147
  context: "contexto",
112
148
  "context-brief": "brief ctx",
113
149
  "context-summary": "resumo ctx",
@@ -127,6 +163,8 @@ export function describeArtifact(meta: ArtifactMeta): string {
127
163
  switch (meta.kind) {
128
164
  case "review":
129
165
  return "Relatório consolidado da revisão — o entregável.";
166
+ case "resume":
167
+ return "Resumo final da revisão: visão de negócio e pontos técnicos diretos (leitura humana).";
130
168
  case "domain-review":
131
169
  return "Relatório isolado do domínio: achados daquele watchdog.";
132
170
  case "verify":
@@ -139,6 +177,10 @@ export function describeArtifact(meta: ArtifactMeta): string {
139
177
  return "Classificação do diff: watchdogs ativados por arquivo, status (A/M/D/R) e churn.";
140
178
  case "candidates":
141
179
  return "Indícios da pré-análise Jev (regra + arquivo + confiança) — não são achados.";
180
+ case "ast":
181
+ return "Fatos estruturais e delta por arquivo (símbolos, imports, exports e assinaturas).";
182
+ case "ast-impact":
183
+ return "Impacto reverso: arquivos que importam os alterados e exports públicos modificados.";
142
184
  case "context":
143
185
  return "Payload bruto da PR coletado via `gh` (ou marcador de indisponível).";
144
186
  case "context-brief":
@@ -156,6 +198,14 @@ export function isJsonArtifact(meta: ArtifactMeta | undefined): boolean {
156
198
  return meta.contentType.includes("json") || meta.name.toLowerCase().endsWith(".json");
157
199
  }
158
200
 
201
+ /** `true` se o conteúdo deve ser renderizado como markdown. */
202
+ export function isMarkdownArtifact(meta: ArtifactMeta | undefined): boolean {
203
+ if (!meta) return false;
204
+ return (
205
+ meta.contentType.includes("markdown") || meta.name.toLowerCase().endsWith(".md")
206
+ );
207
+ }
208
+
159
209
  /** Agrupa os artefatos para a navegação lateral (ordem estável). */
160
210
  export function groupArtifacts(
161
211
  items: ArtifactMeta[],
@@ -190,8 +240,9 @@ export function groupArtifacts(
190
240
 
191
241
  /** Legenda única de cada aba (explica o que a aba é; mostrada quando selecionada). */
192
242
  export const TAB_CAPTIONS = {
243
+ resume: "Resumo principal da revisão: o que muda em termos de negócio, números diretos (arquivos, churn, domínios) e os pontos técnicos que exigem ação.",
193
244
  findings:
194
245
  "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>.",
195
246
  artifacts:
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.",
247
+ "Artefatos persistidos da run: relatórios, briefs de auditoria, manifesto, classificação, Jev, contexto da PR e verificação. JSON com destaque de sintaxe; markdown renderizado com highlight de código.",
197
248
  } as const;
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Markdown mínimo do cliente (campo `action` do finding e artefatos).
3
+ *
4
+ * Suporta o suficiente para instruções curtas e relatórios: parágrafos,
5
+ * quebras de linha, listas, blockquote, títulos, tabelas, blocos de código e
6
+ * formatação inline (`code`, **bold**, *itálico*, links). Sem lib externa e
7
+ * sem HTML cru.
8
+ */
9
+
10
+ /** Alinhamento de coluna em tabela markdown. */
11
+ export type MarkdownAlign = "left" | "center" | "right";
12
+
13
+ export type MarkdownBlock =
14
+ | { type: "paragraph"; text: string }
15
+ | { type: "list"; ordered: boolean; items: string[] }
16
+ | { type: "code"; language: string | null; code: string }
17
+ | { type: "quote"; text: string }
18
+ | { type: "heading"; level: number; text: string }
19
+ | {
20
+ type: "table";
21
+ align: (MarkdownAlign | undefined)[];
22
+ header: string[];
23
+ rows: string[][];
24
+ }
25
+ | { type: "hr" };
26
+
27
+ export type InlineToken =
28
+ | { type: "text"; text: string }
29
+ | { type: "code"; text: string }
30
+ | { type: "strong"; text: string }
31
+ | { type: "em"; text: string }
32
+ | { type: "link"; text: string; url: string }
33
+ | { type: "path"; text: string };
34
+
35
+ const FENCE = /^\s*```(\S*)\s*$/;
36
+ const BULLET = /^\s*[-*+]\s+(.*)$/;
37
+ const ORDERED = /^\s*\d+\.\s+(.*)$/;
38
+ const QUOTE = /^\s*>\s?(.*)$/;
39
+ const HEADING = /^(#{1,6})\s+(.*)$/;
40
+ const HR = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
41
+ const TABLE_SEP = /^\s*\|?\s*:?-{2,}:?\s*(?:\|\s*:?-{2,}:?\s*)+\|?\s*$/;
42
+
43
+ function isBlockStart(line: string): boolean {
44
+ return (
45
+ FENCE.test(line) ||
46
+ HEADING.test(line) ||
47
+ HR.test(line) ||
48
+ BULLET.test(line) ||
49
+ ORDERED.test(line) ||
50
+ QUOTE.test(line)
51
+ );
52
+ }
53
+
54
+ /** Quebra uma linha de tabela em células (remove os pipes das pontas). */
55
+ function splitTableRow(line: string): string[] {
56
+ return line
57
+ .trim()
58
+ .replace(/^\|/, "")
59
+ .replace(/\|$/, "")
60
+ .split("|")
61
+ .map((cell) => cell.trim());
62
+ }
63
+
64
+ /** `true` quando há um cabeçalho seguido da linha separadora de tabela. */
65
+ function isTableStart(lines: string[], index: number): boolean {
66
+ if (index + 1 >= lines.length) return false;
67
+ if (!lines[index].includes("|")) return false;
68
+ return TABLE_SEP.test(lines[index + 1]);
69
+ }
70
+
71
+ /** Alinhamentos declarados na linha separadora (`:---`, `:---:`, `---:`). */
72
+ function parseAlignments(line: string): (MarkdownAlign | undefined)[] {
73
+ return splitTableRow(line).map((cell) => {
74
+ const left = cell.startsWith(":");
75
+ const right = cell.endsWith(":");
76
+ if (left && right) return "center";
77
+ if (right) return "right";
78
+ if (left) return "left";
79
+ return undefined;
80
+ });
81
+ }
82
+
83
+ /** Célula numérica ou razão (`12`, `3/4`) — colunas assim vão para a direita. */
84
+ const NUMERIC_CELL = /^[\d\s/]+$/;
85
+
86
+ /** Resolve o alinhamento: explícito > coluna só de números/barra > padrão. */
87
+ function resolveAlignments(
88
+ align: (MarkdownAlign | undefined)[],
89
+ rows: string[][],
90
+ ): (MarkdownAlign | undefined)[] {
91
+ return align.map((value, column) => {
92
+ if (value) return value;
93
+ let filled = 0;
94
+ for (const row of rows) {
95
+ const cell = (row[column] ?? "").trim();
96
+ if (cell.length === 0) continue;
97
+ if (!NUMERIC_CELL.test(cell) || !/\d/.test(cell)) return undefined;
98
+ filled++;
99
+ }
100
+ return filled > 0 ? "right" : undefined;
101
+ });
102
+ }
103
+
104
+ /** Divide o texto em blocos simples (sem aninhamento profundo). */
105
+ export function parseMarkdown(text: string): MarkdownBlock[] {
106
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
107
+ const blocks: MarkdownBlock[] = [];
108
+ let index = 0;
109
+
110
+ while (index < lines.length) {
111
+ const line = lines[index];
112
+ if (line.trim().length === 0) {
113
+ index++;
114
+ continue;
115
+ }
116
+
117
+ const fence = FENCE.exec(line);
118
+ if (fence) {
119
+ const language = fence[1] || null;
120
+ const code: string[] = [];
121
+ index++;
122
+ while (index < lines.length && !FENCE.test(lines[index])) {
123
+ code.push(lines[index]);
124
+ index++;
125
+ }
126
+ if (index < lines.length) index++;
127
+ blocks.push({ type: "code", language, code: code.join("\n") });
128
+ continue;
129
+ }
130
+
131
+ const heading = HEADING.exec(line);
132
+ if (heading) {
133
+ blocks.push({
134
+ type: "heading",
135
+ level: heading[1].length,
136
+ text: heading[2].trim(),
137
+ });
138
+ index++;
139
+ continue;
140
+ }
141
+
142
+ if (HR.test(line)) {
143
+ blocks.push({ type: "hr" });
144
+ index++;
145
+ continue;
146
+ }
147
+
148
+ if (isTableStart(lines, index)) {
149
+ const header = splitTableRow(line);
150
+ const align = parseAlignments(lines[index + 1]);
151
+ const rows: string[][] = [];
152
+ index += 2;
153
+ while (
154
+ index < lines.length &&
155
+ lines[index].trim().length > 0 &&
156
+ lines[index].includes("|")
157
+ ) {
158
+ rows.push(splitTableRow(lines[index]));
159
+ index++;
160
+ }
161
+ blocks.push({
162
+ type: "table",
163
+ align: resolveAlignments(align, rows),
164
+ header,
165
+ rows,
166
+ });
167
+ continue;
168
+ }
169
+
170
+ if (BULLET.test(line)) {
171
+ const items: string[] = [];
172
+ while (index < lines.length) {
173
+ const item = BULLET.exec(lines[index]);
174
+ if (!item) break;
175
+ items.push(item[1]);
176
+ index++;
177
+ }
178
+ blocks.push({ type: "list", ordered: false, items });
179
+ continue;
180
+ }
181
+
182
+ if (ORDERED.test(line)) {
183
+ const items: string[] = [];
184
+ while (index < lines.length) {
185
+ const item = ORDERED.exec(lines[index]);
186
+ if (!item) break;
187
+ items.push(item[1]);
188
+ index++;
189
+ }
190
+ blocks.push({ type: "list", ordered: true, items });
191
+ continue;
192
+ }
193
+
194
+ if (QUOTE.test(line)) {
195
+ const quote: string[] = [];
196
+ while (index < lines.length) {
197
+ const item = QUOTE.exec(lines[index]);
198
+ if (!item) break;
199
+ quote.push(item[1]);
200
+ index++;
201
+ }
202
+ blocks.push({ type: "quote", text: quote.join("\n") });
203
+ continue;
204
+ }
205
+
206
+ const paragraph: string[] = [line];
207
+ index++;
208
+ while (
209
+ index < lines.length &&
210
+ lines[index].trim().length > 0 &&
211
+ !isBlockStart(lines[index]) &&
212
+ !isTableStart(lines, index)
213
+ ) {
214
+ paragraph.push(lines[index]);
215
+ index++;
216
+ }
217
+ blocks.push({ type: "paragraph", text: paragraph.join("\n") });
218
+ }
219
+
220
+ return blocks;
221
+ }
222
+
223
+ /** Blocos de uma seção `## <título>` (sem o heading); vazio se ausente. */
224
+ export function sectionBlocks(text: string, title: string): MarkdownBlock[] {
225
+ const wanted = title.trim().toLowerCase();
226
+ const blocks: MarkdownBlock[] = [];
227
+ let inside = false;
228
+ for (const block of parseMarkdown(text)) {
229
+ if (block.type === "heading") {
230
+ if (inside) break;
231
+ inside = block.level >= 2 && block.text.trim().toLowerCase() === wanted;
232
+ continue;
233
+ }
234
+ if (inside) blocks.push(block);
235
+ }
236
+ return blocks;
237
+ }
238
+
239
+ /** Extensão reconhecida como arquivo (evita destacar texto comum). */
240
+ const FILE_EXTENSION =
241
+ /\.(?:[cm]?[jt]sx?|json|md|ya?ml|toml|css|scss|html?|py|go|rs|java|kt|cs|php|rb|sh|sql|txt|lock)$/i;
242
+
243
+ /** Caminho/arquivo dentro de um texto corrido (`src/foo.ts`, `review.md`). */
244
+ const PATH_TOKEN =
245
+ /(?:[\w.-]+[\\/])*[\w-]+\.(?:[cm]?[jt]sx?|json|md|ya?ml|toml|css|scss|html?|py|go|rs|java|kt|cs|php|rb|sh|sql|txt|lock)\b/g;
246
+
247
+ /** `true` se o texto parece um caminho/arquivo (sem espaços e com extensão). */
248
+ export function isFilePath(value: string): boolean {
249
+ const text = value.trim();
250
+ if (!text || /\s/.test(text)) return false;
251
+ if (/^(?:https?:|mailto:|#)/i.test(text)) return false;
252
+ const base = text.split(/[\\/]/).pop() ?? text;
253
+ if (FILE_EXTENSION.test(base)) return true;
254
+ return (
255
+ text.startsWith("./") ||
256
+ text.startsWith("../") ||
257
+ text.startsWith("/") ||
258
+ /^[A-Za-z]:[\\/]/.test(text)
259
+ );
260
+ }
261
+
262
+ /** Quebra texto corrido em tokens, destacando caminhos de arquivo. */
263
+ function pushTextTokens(tokens: InlineToken[], text: string): void {
264
+ let last = 0;
265
+ for (const match of text.matchAll(PATH_TOKEN)) {
266
+ const index = match.index ?? 0;
267
+ if (index > last) {
268
+ tokens.push({ type: "text", text: text.slice(last, index) });
269
+ }
270
+ tokens.push({ type: "path", text: match[0] });
271
+ last = index + match[0].length;
272
+ }
273
+ if (last < text.length) {
274
+ tokens.push({ type: "text", text: text.slice(last) });
275
+ }
276
+ }
277
+
278
+ /** Tokeniza formatação inline preservando o texto e a ordem dos marcadores. */
279
+ export function parseInline(text: string): InlineToken[] {
280
+ const tokens: InlineToken[] = [];
281
+ // Ordem importa: code/bold antes de itálico; `_` exige fronteira de palavra.
282
+ const pattern =
283
+ /`[^`\n]+`|\*\*[^*\n]+\*\*|__[^_\n]+__|\*[^*\n]+\*|(?<![\w])_[^_\n]+_(?![\w])|\[[^\]\n]+\]\([^)\s]+\)/g;
284
+ let last = 0;
285
+ for (const match of text.matchAll(pattern)) {
286
+ const start = match.index ?? 0;
287
+ if (start > last) {
288
+ pushTextTokens(tokens, text.slice(last, start));
289
+ }
290
+ const raw = match[0];
291
+ if (raw.startsWith("`")) {
292
+ tokens.push({ type: "code", text: raw.slice(1, -1) });
293
+ } else if (raw.startsWith("**") || raw.startsWith("__")) {
294
+ tokens.push({ type: "strong", text: raw.slice(2, -2) });
295
+ } else if (raw.startsWith("*") || raw.startsWith("_")) {
296
+ tokens.push({ type: "em", text: raw.slice(1, -1) });
297
+ } else {
298
+ const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(raw);
299
+ if (link) {
300
+ tokens.push({ type: "link", text: link[1], url: link[2] });
301
+ } else {
302
+ pushTextTokens(tokens, raw);
303
+ }
304
+ }
305
+ last = start + raw.length;
306
+ }
307
+ if (last < text.length) {
308
+ pushTextTokens(tokens, text.slice(last));
309
+ }
310
+ return tokens;
311
+ }
312
+
313
+ /** URL segura para links de markdown (evita `javascript:` e afins). */
314
+ export function safeUrl(url: string): string | null {
315
+ const value = url.trim();
316
+ if (/^(https?:|mailto:)/i.test(value)) return value;
317
+ if (value.startsWith("/") && !value.startsWith("//")) return value;
318
+ return null;
319
+ }
@@ -61,8 +61,15 @@ export type ArtifactKind =
61
61
  | "context-summary"
62
62
  | "finding-report"
63
63
  | "review"
64
+ | "resume"
64
65
  | "domain-review"
65
66
  | "verify"
67
+ | "ast"
68
+ | "ast-impact"
69
+ | "ai"
70
+ | "ai-usage"
71
+ | "ai-brief"
72
+ | "harness-transcript"
66
73
  | "other";
67
74
 
68
75
  /** Metadados de artefato retornados pela API (sem conteúdo). */
@@ -77,6 +84,9 @@ export type RunSummary = {
77
84
  slug: string;
78
85
  branch: string;
79
86
  updatedAt?: string;
87
+ startedAt?: string;
88
+ /** Último sinal de vida de uma execução ativa (revisão/correção). */
89
+ heartbeatAt?: string;
80
90
  status?: RunStatus | null;
81
91
  summary: Summary;
82
92
  };
@@ -87,6 +97,52 @@ export type FindingsResponse = {
87
97
  summary: Summary;
88
98
  };
89
99
 
100
+ /** Manifesto da run (subset consumido pela aba de resumo). */
101
+ export type RunManifest = {
102
+ branch?: string;
103
+ base?: string;
104
+ head?: string;
105
+ generatedAt?: string;
106
+ churn?: { additions: number; deletions: number };
107
+ watchdogs?: {
108
+ id: string;
109
+ name: string;
110
+ files: string[];
111
+ overflow: number;
112
+ status: "pending" | "done";
113
+ }[];
114
+ context?: {
115
+ summarized: boolean;
116
+ source?: {
117
+ number?: number;
118
+ title?: string;
119
+ url?: string;
120
+ base?: string;
121
+ head?: string;
122
+ } | null;
123
+ };
124
+ excludedCount?: number;
125
+ deleted?: string[];
126
+ unclassified?: string[];
127
+ };
128
+
129
+ /** Detalhe da run (`GET /api/runs/:slug`). */
130
+ export type RunDetail = {
131
+ slug: string;
132
+ branch: string;
133
+ manifest: RunManifest | null;
134
+ watchdogs: string[];
135
+ unclassified: string[];
136
+ deleted: string[];
137
+ };
138
+
139
+ /** `classification.json` — só o necessário para os cards do resumo. */
140
+ export type ClassificationArtifact = {
141
+ files?: Record<string, { status: string; oldPath?: string }>;
142
+ churn?: Record<string, { additions: number; deletions: number }>;
143
+ watchdogs?: Record<string, string[]>;
144
+ };
145
+
90
146
  /** Candidato ao envio da revisão (espelha `core/github-review.ts`). */
91
147
  export type SubmitCandidate = {
92
148
  id: string;
@@ -134,6 +190,14 @@ export type SubmitOutcome = {
134
190
  failed: { id: string; reason: string }[];
135
191
  skipped: { id: string; reason: string }[];
136
192
  general?: { commentId: number; url?: string; ids: string[] };
193
+ review?: {
194
+ id?: number;
195
+ url?: string;
196
+ created: boolean;
197
+ finalized: boolean;
198
+ event?: "COMMENT" | "REQUEST_CHANGES";
199
+ error?: string;
200
+ };
137
201
  };
138
202
 
139
203
  export type SubmitResponse = {