@payconductor/houndsman 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +195 -3
- package/assets/houndsman.png +0 -0
- package/package.json +63 -5
- package/src/cli/index.ts +210 -0
- package/src/core/assets.ts +20 -0
- package/src/core/brief.ts +162 -0
- package/src/core/commands/context.ts +39 -0
- package/src/core/commands/review.ts +50 -0
- package/src/core/commands/scaffold.ts +373 -0
- package/src/core/commands/setup.ts +75 -0
- package/src/core/commands/test.ts +20 -0
- package/src/core/commands/update.ts +30 -0
- package/src/core/commands/watchdog.ts +105 -0
- package/src/core/config-loader.ts +87 -0
- package/src/core/config.ts +59 -0
- package/src/core/doc-discoverer.ts +33 -0
- package/src/core/file-classifier.ts +247 -0
- package/src/core/jev/index.ts +12 -0
- package/src/core/jev/jev-candidates.ts +105 -0
- package/src/core/jev/jev-client.ts +99 -0
- package/src/core/jev/jev-detector.ts +203 -0
- package/src/core/jev/jev-questions.ts +92 -0
- package/src/core/jev/jev-types.ts +91 -0
- package/src/core/orchestrator.ts +444 -0
- package/src/core/paths.ts +30 -0
- package/src/core/pr-context.ts +271 -0
- package/src/core/registry-check.ts +145 -0
- package/src/core/report-builder.ts +357 -0
- package/src/core/types.ts +137 -0
- package/src/core/watchdog-registry.ts +43 -0
- package/src/index.ts +56 -0
- package/src/skill/AUDIT-PROTOCOL.md +96 -0
- package/src/skill/OUTPUT-FORMAT.md +70 -0
- package/src/skill/README.md +116 -0
- package/src/skill/SETUP.md +99 -0
- package/src/skill/example-review.md +58 -0
- package/src/skill/houndsman-update.md +251 -0
- package/src/skill/houndsman.md +239 -0
- package/src/tests/fixtures/fake-project.ts +64 -0
- package/src/tests/review-scripts.test.ts +851 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coleta/leitura do contexto da PR via GitHub CLI (`gh`) — biblioteca.
|
|
3
|
+
*
|
|
4
|
+
* Usado pelo subcomando `context` (`cli/index.ts`). O `gh` é
|
|
5
|
+
* OPCIONAL: sem binário, sem login ou sem PR para a branch, grava
|
|
6
|
+
* `context.json` com `{ available: false, reason }` e sai com código 0.
|
|
7
|
+
*
|
|
8
|
+
* Artefatos (em --dir, default .houndsman/.runs/<branch-slug>/):
|
|
9
|
+
* context.json payload bruto do `gh pr view` (ou marcador de indisponível)
|
|
10
|
+
* context-brief.md instruções para o subagente resumir o contexto
|
|
11
|
+
*
|
|
12
|
+
* O subagente lê o brief e grava `context-summary.json`, consumido pelo
|
|
13
|
+
* orchestrator (briefs dos watchdogs + relatório final).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { join, relative } from "node:path";
|
|
18
|
+
import {
|
|
19
|
+
type ContextSummary,
|
|
20
|
+
parseContextSummary,
|
|
21
|
+
sanitizeBranch,
|
|
22
|
+
} from "./report-builder";
|
|
23
|
+
|
|
24
|
+
export const CONTEXT_FILE = "context.json";
|
|
25
|
+
export const CONTEXT_SUMMARY_FILE = "context-summary.json";
|
|
26
|
+
export const CONTEXT_BRIEF_FILE = "context-brief.md";
|
|
27
|
+
|
|
28
|
+
const GH_JSON_FIELDS =
|
|
29
|
+
"number,title,body,url,baseRefName,headRefName,author,commits,files";
|
|
30
|
+
|
|
31
|
+
export type ContextUnavailableReason =
|
|
32
|
+
| "gh-missing"
|
|
33
|
+
| "gh-unauthenticated"
|
|
34
|
+
| "no-pr"
|
|
35
|
+
| "error";
|
|
36
|
+
|
|
37
|
+
/** Payload bruto gravado em `context.json` (inclusive quando indisponível). */
|
|
38
|
+
export type PrContextRaw = {
|
|
39
|
+
available: boolean;
|
|
40
|
+
reason?: ContextUnavailableReason;
|
|
41
|
+
detail?: string;
|
|
42
|
+
collectedAt: string;
|
|
43
|
+
source?: {
|
|
44
|
+
number?: number;
|
|
45
|
+
title?: string;
|
|
46
|
+
url?: string;
|
|
47
|
+
base?: string;
|
|
48
|
+
head?: string;
|
|
49
|
+
author?: string;
|
|
50
|
+
};
|
|
51
|
+
body?: string;
|
|
52
|
+
commits?: string[];
|
|
53
|
+
files?: { path: string; additions?: number; deletions?: number }[];
|
|
54
|
+
stats?: { files: number; additions: number; deletions: number };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type ContextPaths = {
|
|
58
|
+
dir: string;
|
|
59
|
+
context: string;
|
|
60
|
+
summary: string;
|
|
61
|
+
brief: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Caminhos dos artefatos de contexto para uma branch. */
|
|
65
|
+
export function contextPaths(dir: string, branch: string): ContextPaths {
|
|
66
|
+
const dirPath = join(dir, sanitizeBranch(branch));
|
|
67
|
+
return {
|
|
68
|
+
dir: dirPath,
|
|
69
|
+
context: join(dirPath, CONTEXT_FILE),
|
|
70
|
+
summary: join(dirPath, CONTEXT_SUMMARY_FILE),
|
|
71
|
+
brief: join(dirPath, CONTEXT_BRIEF_FILE),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Caminho relativo à raiz, sempre com `/` (portável/legível). */
|
|
76
|
+
function relFromRoot(absPath: string): string {
|
|
77
|
+
return relative(process.cwd(), absPath).replace(/\\/g, "/");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Detecta a branch atual; encerra com erro se não houver. */
|
|
81
|
+
export function currentBranch(): string {
|
|
82
|
+
const result = Bun.spawnSync(["git", "branch", "--show-current"], {
|
|
83
|
+
stdout: "pipe",
|
|
84
|
+
stderr: "pipe",
|
|
85
|
+
});
|
|
86
|
+
const branch = new TextDecoder().decode(result.stdout).trim();
|
|
87
|
+
if (!branch) {
|
|
88
|
+
console.error("Não foi possível detectar a branch. Use --branch <nome>.");
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
return branch;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runGh(args: string[]): { ok: boolean; stdout: string; stderr: string } {
|
|
95
|
+
const result = Bun.spawnSync(["gh", ...args], {
|
|
96
|
+
stdout: "pipe",
|
|
97
|
+
stderr: "pipe",
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
ok: result.exitCode === 0,
|
|
101
|
+
stdout: new TextDecoder().decode(result.stdout).trim(),
|
|
102
|
+
stderr: new TextDecoder().decode(result.stderr).trim(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type GhPrView = {
|
|
107
|
+
number?: number;
|
|
108
|
+
title?: string;
|
|
109
|
+
body?: string;
|
|
110
|
+
url?: string;
|
|
111
|
+
baseRefName?: string;
|
|
112
|
+
headRefName?: string;
|
|
113
|
+
author?: { login?: string; name?: string };
|
|
114
|
+
commits?: { messageHeadline?: string; message?: string }[];
|
|
115
|
+
files?: { path?: string; additions?: number; deletions?: number }[];
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function unavailable(reason: ContextUnavailableReason, detail: string): PrContextRaw {
|
|
119
|
+
return {
|
|
120
|
+
available: false,
|
|
121
|
+
reason,
|
|
122
|
+
detail,
|
|
123
|
+
collectedAt: new Date().toISOString(),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Lê a PR da branch via `gh` e devolve o payload bruto (nunca lança). */
|
|
128
|
+
export function collectPrContext(branch: string): PrContextRaw {
|
|
129
|
+
const collectedAt = new Date().toISOString();
|
|
130
|
+
|
|
131
|
+
if (!Bun.which("gh")) {
|
|
132
|
+
return unavailable("gh-missing", "GitHub CLI (gh) não encontrado no PATH.");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const auth = runGh(["auth", "status"]);
|
|
136
|
+
if (!auth.ok) {
|
|
137
|
+
return unavailable(
|
|
138
|
+
"gh-unauthenticated",
|
|
139
|
+
auth.stderr || "gh não autenticado (rode `gh auth login`).",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const view = runGh(["pr", "view", branch, "--json", GH_JSON_FIELDS]);
|
|
144
|
+
if (!view.ok) {
|
|
145
|
+
const noPr = /no pull requests? found|not found|could not find/i.test(
|
|
146
|
+
view.stderr,
|
|
147
|
+
);
|
|
148
|
+
return unavailable(noPr ? "no-pr" : "error", view.stderr || "gh pr view falhou.");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let parsed: GhPrView;
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(view.stdout) as GhPrView;
|
|
154
|
+
} catch {
|
|
155
|
+
return unavailable("error", "Resposta inválida do gh (JSON malformado).");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const files = (parsed.files ?? [])
|
|
159
|
+
.filter((f): f is { path: string; additions?: number; deletions?: number } =>
|
|
160
|
+
Boolean(f?.path),
|
|
161
|
+
)
|
|
162
|
+
.map((f) => ({
|
|
163
|
+
path: f.path,
|
|
164
|
+
additions: f.additions,
|
|
165
|
+
deletions: f.deletions,
|
|
166
|
+
}));
|
|
167
|
+
const commits = (parsed.commits ?? [])
|
|
168
|
+
.map((c) => c.messageHeadline ?? c.message ?? "")
|
|
169
|
+
.filter((m) => m.length > 0);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
available: true,
|
|
173
|
+
collectedAt,
|
|
174
|
+
source: {
|
|
175
|
+
number: parsed.number,
|
|
176
|
+
title: parsed.title,
|
|
177
|
+
url: parsed.url,
|
|
178
|
+
base: parsed.baseRefName,
|
|
179
|
+
head: parsed.headRefName,
|
|
180
|
+
author: parsed.author?.name ?? parsed.author?.login,
|
|
181
|
+
},
|
|
182
|
+
body: parsed.body?.trim() || undefined,
|
|
183
|
+
commits: commits.length > 0 ? commits : undefined,
|
|
184
|
+
files: files.length > 0 ? files : undefined,
|
|
185
|
+
stats: {
|
|
186
|
+
files: files.length,
|
|
187
|
+
additions: files.reduce((sum, f) => sum + (f.additions ?? 0), 0),
|
|
188
|
+
deletions: files.reduce((sum, f) => sum + (f.deletions ?? 0), 0),
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Lê o `context-summary.json` (schema do subagente); null se ausente/inválido. */
|
|
194
|
+
export function readContextSummary(filePath: string): ContextSummary | null {
|
|
195
|
+
if (!existsSync(filePath)) return null;
|
|
196
|
+
try {
|
|
197
|
+
return parseContextSummary(JSON.parse(readFileSync(filePath, "utf8")));
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Instruções prontas para o subagente que resume o contexto da PR. */
|
|
204
|
+
export function renderContextBrief(
|
|
205
|
+
context: PrContextRaw,
|
|
206
|
+
branch: string,
|
|
207
|
+
paths: ContextPaths,
|
|
208
|
+
): string {
|
|
209
|
+
const lines: string[] = [];
|
|
210
|
+
|
|
211
|
+
lines.push("# Brief — Contexto da PR (subagente)", "");
|
|
212
|
+
lines.push(`**Branch:** ${branch}`);
|
|
213
|
+
lines.push(`**Contexto bruto:** \`${relFromRoot(paths.context)}\``);
|
|
214
|
+
lines.push(`**Output obrigatório:** \`${relFromRoot(paths.summary)}\``, "");
|
|
215
|
+
|
|
216
|
+
lines.push("## Objetivo", "");
|
|
217
|
+
lines.push(
|
|
218
|
+
"Resumir a PR para abrir o relatório de revisão em **Objetivo**,",
|
|
219
|
+
"**Motivação** e **Resumo das alterações** (pt-BR).",
|
|
220
|
+
"",
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
lines.push("## Instruções", "");
|
|
224
|
+
if (!context.available) {
|
|
225
|
+
lines.push(
|
|
226
|
+
`O contexto está **indisponível** (\`${context.reason}\`). Grave o output com`,
|
|
227
|
+
'`{ "available": false }` e encerre. Não invente conteúdo.',
|
|
228
|
+
"",
|
|
229
|
+
);
|
|
230
|
+
} else {
|
|
231
|
+
lines.push("1. Leia o `context.json` (title, body, commits, files).");
|
|
232
|
+
lines.push("2. Sintetize em pt-BR: objetivo, motivação e resumo das alterações.");
|
|
233
|
+
lines.push("3. Grave o output no schema abaixo. Não escreva markdown livre.");
|
|
234
|
+
lines.push("");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
lines.push("## Schema do output (`context-summary.json`)", "");
|
|
238
|
+
lines.push("```json");
|
|
239
|
+
lines.push(JSON.stringify(CONTEXT_SUMMARY_TEMPLATE, null, 2));
|
|
240
|
+
lines.push("```", "");
|
|
241
|
+
|
|
242
|
+
lines.push("## Regras", "");
|
|
243
|
+
lines.push(
|
|
244
|
+
"- **Seja curto** — o resumo abre o relatório e entra em **todos** os briefs.",
|
|
245
|
+
);
|
|
246
|
+
lines.push("- Baseie-se **somente** no `context.json` — não invente intenção.");
|
|
247
|
+
lines.push("- `objective`: 1 frase (≤ 200 caracteres) sobre o que a PR entrega.");
|
|
248
|
+
lines.push("- `motivation`: 1 frase (≤ 240 caracteres) sobre o porquê.");
|
|
249
|
+
lines.push(
|
|
250
|
+
"- `changeSummary`: no máximo 4 bullets (≤ 160 caracteres cada), focados em comportamento — não liste arquivos.",
|
|
251
|
+
);
|
|
252
|
+
lines.push(
|
|
253
|
+
"- NÃO copie a descrição da PR: sintetize. Copie `source` do contexto bruto.",
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return lines.join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const CONTEXT_SUMMARY_TEMPLATE = {
|
|
260
|
+
available: true,
|
|
261
|
+
source: {
|
|
262
|
+
number: 1,
|
|
263
|
+
title: "Título da PR",
|
|
264
|
+
url: "https://github.com/org/repo/pull/1",
|
|
265
|
+
base: "main",
|
|
266
|
+
head: "feature/x",
|
|
267
|
+
},
|
|
268
|
+
objective: "O que a PR entrega, em 1 frase.",
|
|
269
|
+
motivation: "Por que a mudança é necessária, em 1-2 frases.",
|
|
270
|
+
changeSummary: ["Bullet de comportamento 1", "Bullet de comportamento 2"],
|
|
271
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validações de consistência do motor de revisão (biblioteca).
|
|
3
|
+
*
|
|
4
|
+
* Usado pelo subcomando `check` (`cli/index.ts`). Tudo é **inferido** do
|
|
5
|
+
* `HoundsmanConfig` — não há caminhos de arquivo do projeto na config.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { HoundsmanConfig } from "./config";
|
|
9
|
+
import { JevQuestionFactory } from "./jev";
|
|
10
|
+
import { renderReport, sanitizeBranch } from "./report-builder";
|
|
11
|
+
import type { WatchdogMeta } from "./types";
|
|
12
|
+
|
|
13
|
+
/** `id` da definição bate com a chave; `description` não vazio. */
|
|
14
|
+
function checkDefinitions(config: HoundsmanConfig, problems: string[]): void {
|
|
15
|
+
for (const id of config.watchdogIds) {
|
|
16
|
+
const def = config.watchdogs[id];
|
|
17
|
+
if (!def) {
|
|
18
|
+
problems.push(`Watchdog '${id}' sem definição em watchdogs`);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (def.id !== id) {
|
|
22
|
+
problems.push(`watchdogs['${id}'].id = '${def.id}' (esperado '${id}')`);
|
|
23
|
+
}
|
|
24
|
+
if (!def.description.trim()) {
|
|
25
|
+
problems.push(`Watchdog '${id}' sem 'description'`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Todo watchdog precisa declarar ao menos um arquivo em `classify`. */
|
|
31
|
+
function checkClassification(config: HoundsmanConfig, problems: string[]): void {
|
|
32
|
+
for (const id of config.watchdogIds) {
|
|
33
|
+
const def = config.watchdogs[id];
|
|
34
|
+
if (!def) continue;
|
|
35
|
+
if (!def.classify || def.classify.length === 0) {
|
|
36
|
+
problems.push(`Watchdog '${id}' não declara nenhum padrão em 'classify'`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Smoke test dos helpers críticos de renderização. */
|
|
42
|
+
function checkRendering(config: HoundsmanConfig, problems: string[]): void {
|
|
43
|
+
if (sanitizeBranch("feature/x") !== "feature-x") {
|
|
44
|
+
problems.push("sanitizeBranch não está normalizando branches");
|
|
45
|
+
}
|
|
46
|
+
const sampleId = config.watchdogIds[0] ?? "watchdog";
|
|
47
|
+
const report = {
|
|
48
|
+
watchdogId: sampleId,
|
|
49
|
+
reviewedFiles: [],
|
|
50
|
+
skipped: [],
|
|
51
|
+
findings: [],
|
|
52
|
+
};
|
|
53
|
+
const md = renderReport("main", [report], [], {});
|
|
54
|
+
if (!md.includes("✅ Aprovado")) {
|
|
55
|
+
problems.push("renderReport não marcou status aprovado sem achados");
|
|
56
|
+
}
|
|
57
|
+
const withContext = renderReport(
|
|
58
|
+
"main",
|
|
59
|
+
[report],
|
|
60
|
+
[],
|
|
61
|
+
{},
|
|
62
|
+
{
|
|
63
|
+
available: true,
|
|
64
|
+
objective: "Entregar X",
|
|
65
|
+
changeSummary: ["Faz A"],
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
if (!withContext.includes("## Contexto da PR")) {
|
|
69
|
+
problems.push("renderReport não incluiu a seção de contexto");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Valida as specs Jev da config e conta as perguntas derivadas. */
|
|
74
|
+
function checkJevSpecs(config: HoundsmanConfig, problems: string[]): number {
|
|
75
|
+
const factory = new JevQuestionFactory(
|
|
76
|
+
config.watchdogs as Record<string, WatchdogMeta>,
|
|
77
|
+
);
|
|
78
|
+
let generated = 0;
|
|
79
|
+
const keys = new Set<string>();
|
|
80
|
+
|
|
81
|
+
for (const id of config.watchdogIds) {
|
|
82
|
+
const def = config.watchdogs[id];
|
|
83
|
+
if (!def) continue;
|
|
84
|
+
def.rules.forEach((rule, index) => {
|
|
85
|
+
const key = `${id}::rule-${index}`;
|
|
86
|
+
if (!rule.text.trim()) {
|
|
87
|
+
problems.push(`Regra sem texto em ${id} (rule-${index})`);
|
|
88
|
+
}
|
|
89
|
+
const spec = factory.specOf(rule);
|
|
90
|
+
if (!spec) return;
|
|
91
|
+
const question = factory.fromSpec(rule.text, spec);
|
|
92
|
+
if (!question) {
|
|
93
|
+
problems.push(`Spec Jev inválida em ${id} (rule-${index}): ${rule.text}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
generated++;
|
|
97
|
+
if (keys.has(key)) problems.push(`Chave Jev duplicada: ${key}`);
|
|
98
|
+
keys.add(key);
|
|
99
|
+
if (!question.instructions.trim()) {
|
|
100
|
+
problems.push(`Pergunta Jev sem instrução: ${key}`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Jev é opcional (falha-aberto): não ter perguntas não é erro de configuração.
|
|
106
|
+
return generated;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Prefixos de polaridade aceitos no texto da regra (§ Convenções de escrita). */
|
|
110
|
+
const POLARITY_PREFIX = /^(promover|evitar|não|nao|nunca)\b/i;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Aviso **não bloqueante** para regra sem polaridade explícita: o texto deve
|
|
114
|
+
* começar com `Promover:` (DEVE acontecer) ou `Evitar:`/`Não`/`Nunca` (NÃO DEVE).
|
|
115
|
+
*/
|
|
116
|
+
function checkRulePolarity(config: HoundsmanConfig, warnings: string[]): void {
|
|
117
|
+
for (const id of config.watchdogIds) {
|
|
118
|
+
const def = config.watchdogs[id];
|
|
119
|
+
if (!def) continue;
|
|
120
|
+
def.rules.forEach((rule, index) => {
|
|
121
|
+
const text = rule.text.trim();
|
|
122
|
+
if (!text || POLARITY_PREFIX.test(text)) return;
|
|
123
|
+
warnings.push(
|
|
124
|
+
`Regra sem polaridade explícita em ${id} (rule-${index}): ${rule.text}`,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function checkRegistry(config: HoundsmanConfig): {
|
|
131
|
+
problems: string[];
|
|
132
|
+
warnings: string[];
|
|
133
|
+
jevQuestions: number;
|
|
134
|
+
} {
|
|
135
|
+
const problems: string[] = [];
|
|
136
|
+
const warnings: string[] = [];
|
|
137
|
+
|
|
138
|
+
checkDefinitions(config, problems);
|
|
139
|
+
checkClassification(config, problems);
|
|
140
|
+
checkRendering(config, problems);
|
|
141
|
+
checkRulePolarity(config, warnings);
|
|
142
|
+
const jevQuestions = checkJevSpecs(config, problems);
|
|
143
|
+
|
|
144
|
+
return { problems, warnings, jevQuestions };
|
|
145
|
+
}
|