@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
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
* Constantes de apresentação do cliente (labels, ordem, emojis).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
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
|
-
/**
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
|
|
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
|
|
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;
|