@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.
- package/README.md +35 -41
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/grammars/web-tree-sitter.wasm +0 -0
- package/package.json +11 -3
- package/src/cli/commands/command.ts +5 -0
- package/src/cli/commands/doctor-command.ts +116 -24
- package/src/cli/commands/hunt-command.ts +25 -0
- package/src/cli/commands/status-command.ts +46 -0
- package/src/cli/help.ts +18 -1
- package/src/cli/index.ts +13 -6
- package/src/core/ai/ai-client.ts +221 -0
- package/src/core/ai/ai-sdk-model-provider.ts +101 -0
- package/src/core/ai/errors.ts +36 -0
- package/src/core/ai/features/brief-enricher.ts +62 -0
- package/src/core/ai/features/classifier.ts +50 -0
- package/src/core/ai/features/context-summarizer.ts +31 -0
- package/src/core/ai/features/pre-analyzer.ts +51 -0
- package/src/core/ai/features/verifier.ts +45 -0
- package/src/core/ai/index.ts +56 -0
- package/src/core/ai/model-factory.ts +137 -0
- package/src/core/ai/prompts/base.ts +36 -0
- package/src/core/ai/prompts/brief.ts +87 -0
- package/src/core/ai/prompts/classification.ts +36 -0
- package/src/core/ai/prompts/context.ts +29 -0
- package/src/core/ai/prompts/pre-analysis.ts +49 -0
- package/src/core/ai/prompts/verify.ts +40 -0
- package/src/core/ai/schemas.ts +110 -0
- package/src/core/ai/types.ts +76 -0
- package/src/core/ai/usage-budget.ts +75 -0
- package/src/core/ai/usage-recorder.ts +79 -0
- package/src/core/assets.ts +16 -0
- package/src/core/ast/diff.ts +220 -0
- package/src/core/ast/impact.ts +127 -0
- package/src/core/ast/parser.ts +167 -0
- package/src/core/ast/service.ts +233 -0
- package/src/core/ast/symbols.ts +294 -0
- package/src/core/ast/types.ts +125 -0
- package/src/core/brief.ts +138 -0
- package/src/core/commands/findings.ts +120 -6
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/status.ts +63 -0
- package/src/core/commands/update.ts +5 -3
- package/src/core/config.ts +85 -0
- package/src/core/defaults.ts +27 -0
- package/src/core/domain/watchdog.ts +43 -5
- package/src/core/engine/houndsman-engine.ts +16 -1
- package/src/core/engine/review-scope.ts +77 -13
- package/src/core/engine/review-session.ts +27 -2
- package/src/core/env.ts +101 -0
- package/src/core/file-classifier.ts +25 -4
- package/src/core/findings-state.ts +21 -4
- package/src/core/github-review.ts +620 -57
- package/src/core/jev/jev-client.ts +8 -7
- package/src/core/jev/jev-verify.ts +2 -1
- package/src/core/logging/index.ts +17 -0
- package/src/core/logging/logger.ts +187 -0
- package/src/core/orchestrator.ts +454 -16
- package/src/core/paths.ts +3 -0
- package/src/core/project-config.ts +123 -1
- package/src/core/registry-check.ts +148 -6
- package/src/core/renderers/manifest-writer.ts +33 -0
- package/src/core/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/rule-text.ts +97 -0
- package/src/core/run-status.ts +39 -5
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/file/file-storage.ts +2 -0
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/sqlite/mappers.ts +2 -0
- package/src/core/storage/sqlite/schema.ts +9 -1
- package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
- package/src/core/storage/types.ts +15 -0
- package/src/core/storage/work-area.ts +12 -0
- package/src/core/types.ts +37 -1
- package/src/core/verify.ts +17 -5
- package/src/harness/README.md +15 -0
- package/src/harness/agent/agent-context.ts +33 -0
- package/src/harness/agent/agent-errors.ts +30 -0
- package/src/harness/agent/agent-factory.ts +45 -0
- package/src/harness/agent/agent-reporter.ts +34 -0
- package/src/harness/agent/agent-result.ts +16 -0
- package/src/harness/agent/agent-runner.ts +32 -0
- package/src/harness/agent/agent-scheduler.ts +37 -0
- package/src/harness/agent/review-agent.ts +391 -0
- package/src/harness/config/harness-config.ts +84 -0
- package/src/harness/hunt/hunt-harness.ts +83 -0
- package/src/harness/hunt/hunt-options.ts +75 -0
- package/src/harness/hunt/hunt-result.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +372 -0
- package/src/harness/hunt/review-planner.ts +98 -0
- package/src/harness/index.ts +36 -0
- package/src/harness/investigation/agent-state.ts +134 -0
- package/src/harness/investigation/evidence-verifier.ts +129 -0
- package/src/harness/investigation/evidence.ts +110 -0
- package/src/harness/investigation/finding-critic.ts +96 -0
- package/src/harness/investigation/investigation-planner.ts +118 -0
- package/src/harness/investigation/investigation-task.ts +10 -0
- package/src/harness/investigation/review-journal.ts +87 -0
- package/src/harness/investigation/rule-analyzer.ts +178 -0
- package/src/harness/investigation/rule-signals.ts +110 -0
- package/src/harness/prompts/agent-schemas.ts +57 -0
- package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
- package/src/harness/prompts/states/conclude-prompt.ts +16 -0
- package/src/harness/prompts/states/investigate-prompt.ts +37 -0
- package/src/harness/prompts/states/orient-prompt.ts +15 -0
- package/src/harness/prompts/states/verify-prompt.ts +22 -0
- package/src/harness/tools/agent-tool.ts +102 -0
- package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
- package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
- package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
- package/src/harness/tools/builtin/find-references-tool.ts +66 -0
- package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
- package/src/harness/tools/builtin/helpers.ts +115 -0
- package/src/harness/tools/builtin/impact-tool.ts +48 -0
- package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
- package/src/harness/tools/builtin/index.ts +59 -0
- package/src/harness/tools/builtin/list-files-tool.ts +85 -0
- package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
- package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
- package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
- package/src/harness/tools/builtin/search-code-tool.ts +65 -0
- package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
- package/src/harness/tools/path-guard.ts +90 -0
- package/src/harness/tools/result-limiter.ts +49 -0
- package/src/harness/tools/secret-redactor.ts +54 -0
- package/src/harness/tools/tool-cache.ts +50 -0
- package/src/harness/tools/tool-call-budget.ts +60 -0
- package/src/harness/tools/tool-context.ts +5 -0
- package/src/harness/tools/tool-middleware.ts +105 -0
- package/src/harness/tools/tool-policy.ts +51 -0
- package/src/harness/tools/tool-registry.ts +112 -0
- package/src/harness/tools/tool-result.ts +15 -0
- package/src/harness/tools/toolbox.ts +36 -0
- package/src/harness/validation/finding-validator.ts +119 -0
- package/src/index.ts +46 -0
- package/src/server/client/App.tsx +26 -9
- package/src/server/client/ArtifactsPanel.tsx +12 -5
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +14 -10
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +92 -12
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +49 -6
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/{constants.ts → utils/constants.ts} +61 -10
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +64 -0
- package/src/server/index.ts +66 -7
- package/src/server/run-repository.ts +9 -0
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/example-review.md +22 -14
- package/src/skill/hunt.md +45 -19
- package/src/skill/verify.md +3 -2
- package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
- package/src/tests/ai-core.test.ts +120 -0
- package/src/tests/ai-features.test.ts +183 -0
- package/src/tests/ast.test.ts +335 -0
- package/src/tests/baseline.test.ts +3 -0
- package/src/tests/code-highlight.test.ts +1 -1
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/findings-state.test.ts +50 -2
- package/src/tests/fixtures/fake-agent-tools.ts +69 -0
- package/src/tests/fixtures/fake-investigation.ts +64 -0
- package/src/tests/fixtures/fake-model-provider.ts +48 -0
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/harness-agent.test.ts +122 -0
- package/src/tests/harness-investigation.test.ts +206 -0
- package/src/tests/harness-tools.test.ts +148 -0
- package/src/tests/hunt-command.test.ts +212 -0
- package/src/tests/logger.test.ts +83 -0
- package/src/tests/markdown.test.ts +61 -1
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +73 -0
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +53 -0
- package/src/tests/server.test.ts +76 -21
- package/src/tests/storage.test.ts +63 -0
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- package/src/server/client/markdown.ts +0 -153
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
- /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
|
|
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 = {
|