feat(web-probe): add typed Kafka live fanout validation

This commit is contained in:
Codex
2026-07-10 11:52:05 +02:00
parent b9e804014b
commit a496268713
20 changed files with 1544 additions and 25 deletions
+49 -4
View File
@@ -63,6 +63,7 @@ export interface CheckOptions {
recoveryGuardrails: boolean;
rust: boolean;
scriptsTypecheckTimeoutMs: number;
scriptsTypecheckPath: string | null;
checkHeartbeatMs: number;
}
@@ -79,6 +80,7 @@ const defaultCheckOptions: CheckOptions = {
recoveryGuardrails: false,
rust: false,
scriptsTypecheckTimeoutMs: defaultScriptsTypecheckTimeoutMs,
scriptsTypecheckPath: null,
checkHeartbeatMs: defaultCheckHeartbeatMs,
};
@@ -95,7 +97,7 @@ export function checkHelp(): Record<string, unknown> {
ok: true,
command: "check",
usage: [
"bun scripts/cli.ts check [--syntax-only|--full|--files|--scripts-typecheck|--scripts-typecheck-timeout-ms N|--check-heartbeat-ms N|--components|--compose|--logs|--recovery-guardrails|--rust]",
"bun scripts/cli.ts check [--syntax-only|--full|--files|--scripts-typecheck|--scripts-typecheck-path PREFIX|--scripts-typecheck-timeout-ms N|--check-heartbeat-ms N|--components|--compose|--logs|--recovery-guardrails|--rust]",
"bun scripts/cli.ts check recovery-guardrails",
],
defaultMode: "syntax/config only; Rust is never compiled on the master server by default",
@@ -104,6 +106,7 @@ export function checkHelp(): Record<string, unknown> {
{ name: "--full", description: "Enable all non-Rust checks." },
{ name: "--files", description: "Verify required entrypoint files, including backend-core Cargo files." },
{ name: "--scripts-typecheck", description: "Run scripts TypeScript typecheck through the observed checker." },
{ name: "--scripts-typecheck-path PREFIX", description: "Keep the full typecheck gate, but return bounded primary diagnostics only for a selected path prefix." },
{ name: "--scripts-typecheck-timeout-ms N", description: `Bound scripts TypeScript typecheck duration; default ${defaultScriptsTypecheckTimeoutMs}.` },
{ name: "--check-heartbeat-ms N", description: `Emit unidesk.check.progress JSON lines for running command checks; default ${defaultCheckHeartbeatMs}.` },
{ name: "--components", description: "Run component TypeScript typecheck." },
@@ -139,6 +142,12 @@ export function parseCheckOptions(args: string[]): CheckOptions {
} else if (arg === "--scripts-typecheck-timeout-ms") {
options.scriptsTypecheckTimeoutMs = positiveIntegerOption(arg, args[index + 1], 600_000);
index += 1;
} else if (arg === "--scripts-typecheck-path") {
const value = args[index + 1];
if (!value || value.includes("\0") || value.length > 300) throw new Error(`${arg} requires a 1-300 character non-NUL prefix`);
options.scriptsTypecheck = true;
options.scriptsTypecheckPath = value;
index += 1;
} else if (arg === "--check-heartbeat-ms") {
options.checkHeartbeatMs = positiveIntegerOption(arg, args[index + 1], 60_000);
index += 1;
@@ -180,7 +189,7 @@ function emitCheckProgress(detail: Record<string, unknown>): void {
console.error(JSON.stringify({ event: "unidesk.check.progress", ...detail }));
}
async function commandItem(name: string, command: string[], timeoutMs = 30_000, env: NodeJS.ProcessEnv = process.env, heartbeatMs = defaultCheckHeartbeatMs): Promise<CheckItem> {
async function commandItem(name: string, command: string[], timeoutMs = 30_000, env: NodeJS.ProcessEnv = process.env, heartbeatMs = defaultCheckHeartbeatMs, diagnosticPath: string | null = null): Promise<CheckItem> {
const startedAt = Date.now();
emitCheckProgress({ phase: "started", name, command, timeoutMs, heartbeatMs });
const result = await runCommandObserved(command, repoRoot, {
@@ -213,12 +222,48 @@ async function commandItem(name: string, command: string[], timeoutMs = 30_000,
stderrBytes: result.stderrBytes,
stdoutTruncated: result.stdoutTruncated,
stderrTruncated: result.stderrTruncated,
stdoutTail: result.stdout.slice(-4000),
...(name === "typescript:scripts" ? { diagnostics: typescriptDiagnosticSummary(result.stdout, diagnosticPath) } : {}),
stdoutTail: name === "typescript:scripts" ? "" : result.stdout.slice(-4000),
stderrTail: result.stderr.slice(-4000),
},
};
}
function typescriptDiagnosticSummary(stdout: string, pathPrefix: string | null): Record<string, unknown> {
const rows = stdout.split(/\r?\n/u).flatMap((line) => {
const match = line.match(/^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/u);
if (match === null) return [];
return [{
path: match[1],
line: Number(match[2]),
column: Number(match[3]),
code: match[4],
message: (match[5] ?? "").slice(0, 240),
}];
});
const byPath = new Map<string, number>();
for (const row of rows) {
byPath.set(row.path, (byPath.get(row.path) ?? 0) + 1);
}
const selected = pathPrefix === null ? rows : rows.filter((row) => row.path.startsWith(pathPrefix));
const selectedPathCounts = [...byPath.entries()].filter(([path]) => pathPrefix === null || path.startsWith(pathPrefix));
const itemLimit = pathPrefix === null ? 5 : 8;
const pathLimit = pathPrefix === null ? 20 : 12;
return {
count: rows.length,
pathCount: byPath.size,
pathPrefix,
matchedCount: selected.length,
paths: selectedPathCounts.slice(0, pathLimit).map(([path, count]) => ({ path, count })),
items: selected.slice(0, itemLimit),
truncated: selected.length > itemLimit || selectedPathCounts.length > pathLimit,
next: selected.length > itemLimit || pathPrefix === null
? "bun scripts/cli.ts check --scripts-typecheck --scripts-typecheck-path <narrower-path-prefix>"
: null,
valuesRedacted: true,
};
}
function emitCommandHeartbeat(name: string, command: string[], progress: CommandProgress): void {
emitCheckProgress({
phase: "running",
@@ -425,7 +470,7 @@ export async function runChecks(config: UniDeskConfig, options: CheckOptions = d
items.push(skippedItem("files:required-entrypoints", "required file presence scan is opt-in", "--files or --full"));
}
if (options.scriptsTypecheck) {
items.push(await commandItem("typescript:scripts", ["bun", "--bun", "tsc", "-p", "scripts/tsconfig.json", "--noEmit", "--pretty", "false"], options.scriptsTypecheckTimeoutMs, process.env, options.checkHeartbeatMs));
items.push(await commandItem("typescript:scripts", ["bun", "--bun", "tsc", "-p", "scripts/tsconfig.json", "--noEmit", "--pretty", "false"], options.scriptsTypecheckTimeoutMs, process.env, options.checkHeartbeatMs, options.scriptsTypecheckPath));
} else {
items.push(skippedItem("typescript:scripts", "scripts TypeScript typecheck is opt-in", "--scripts-typecheck or --full"));
}