feat(recipe-studio, validators): tweak OXC validator with lint suppression support and improve error normalization logic

This commit is contained in:
Shine1i 2026-03-06 09:40:53 +01:00
commit 5e5feb5c00
3 changed files with 155 additions and 116 deletions

View file

@ -267,6 +267,7 @@ def _run_oxc_batch(
"error_count": 1,
"error_message": "Invalid OXC result entry.",
"severity": None,
"code": None,
"labels": [],
"codeframe": None,
"warning_count": 0,
@ -277,6 +278,7 @@ def _run_oxc_batch(
error_count_raw = item.get("error_count")
message_raw = item.get("error_message")
severity_raw = item.get("severity")
code_raw = item.get("code")
labels_raw = item.get("labels")
codeframe_raw = item.get("codeframe")
warning_count_raw = item.get("warning_count")
@ -286,6 +288,7 @@ def _run_oxc_batch(
"error_count": int(error_count_raw) if isinstance(error_count_raw, int) else 0,
"error_message": str(message_raw or ""),
"severity": str(severity_raw) if isinstance(severity_raw, str) else None,
"code": str(code_raw) if isinstance(code_raw, str) else None,
"labels": labels_raw if isinstance(labels_raw, list) else [],
"codeframe": str(codeframe_raw) if isinstance(codeframe_raw, str) else None,
"warning_count": int(warning_count_raw)
@ -303,6 +306,7 @@ def _fallback_results(row_count: int, message: str) -> list[dict[str, Any]]:
"error_count": 1,
"error_message": message,
"severity": None,
"code": None,
"labels": [],
"codeframe": None,
"warning_count": 0,

View file

@ -16,6 +16,7 @@ const VALIDATION_MODES = new Set(["syntax", "lint", "syntax+lint"]);
const CODE_SHAPES = new Set(["auto", "module", "snippet"]);
const SNIPPET_PREFIX = "(() => {\n";
const SNIPPET_SUFFIX = "\n})();\nexport {};\n";
const OXLINT_SUPPRESSED_RULES = ["no-unused-vars", "no-new-array"];
const TOOL_DIR = dirname(fileURLToPath(import.meta.url));
function mapLang(value) {
@ -112,6 +113,7 @@ function remapDiagnosticOffsets(diagnostic, offset) {
function normalizeParserError(error) {
if (typeof error === "string") {
return {
code: null,
message: error.trim() || "Unknown parser error",
severity: null,
labels: [],
@ -120,12 +122,14 @@ function normalizeParserError(error) {
}
if (!error || typeof error !== "object") {
return {
code: null,
message: "Unknown parser error",
severity: null,
labels: [],
codeframe: null,
};
}
const code = typeof error.code === "string" ? error.code : null;
const message = String(error.message || error.reason || "").trim() || "Unknown parser error";
const severity = typeof error.severity === "string" ? error.severity : null;
const labels = Array.isArray(error.labels)
@ -146,6 +150,7 @@ function normalizeParserError(error) {
: [];
const codeframe = typeof error.codeframe === "string" ? error.codeframe : null;
return {
code,
message,
severity,
labels,
@ -190,6 +195,7 @@ function normalizeLintDiagnostic(diagnostic) {
const code = typeof diagnostic.code === "string" ? diagnostic.code : null;
return {
code,
message: code ? `${code}: ${message}` : message,
severity,
labels,
@ -203,6 +209,7 @@ function makeResult({
warningCount = 0,
message = "",
severity = null,
code = null,
labels = [],
codeframe = null,
}) {
@ -212,6 +219,7 @@ function makeResult({
warning_count: Number.isInteger(warningCount) ? warningCount : 0,
error_message: String(message || ""),
severity: typeof severity === "string" ? severity : null,
code: typeof code === "string" ? code : null,
labels: Array.isArray(labels) ? labels : [],
codeframe: typeof codeframe === "string" ? codeframe : null,
};
@ -225,6 +233,7 @@ function syntaxResultFromErrors(errors) {
warningCount: 0,
message: errors.slice(0, 3).map((error) => error.message).join(" | "),
severity: first ? first.severity : null,
code: first ? first.code : null,
labels: first ? first.labels : [],
codeframe: first ? first.codeframe : null,
});
@ -377,7 +386,13 @@ function runLintBatch(entries) {
}
const oxlintBin = join(TOOL_DIR, "node_modules", ".bin", "oxlint");
const exec = spawnSync(oxlintBin, ["--format", "json", tempDir], {
const oxlintArgs = [
...OXLINT_SUPPRESSED_RULES.flatMap((rule) => ["-A", rule]),
"--format",
"json",
tempDir,
];
const exec = spawnSync(oxlintBin, oxlintArgs, {
encoding: "utf8",
cwd: TOOL_DIR,
});
@ -452,6 +467,7 @@ function runLintBatch(entries) {
.map((diag) => diag.message)
.join(" | "),
severity: top ? top.severity : null,
code: top ? top.code : null,
labels: top ? top.labels : [],
codeframe: top ? top.codeframe : null,
}),
@ -555,4 +571,3 @@ main().catch((error) => {
process.stderr.write(String(error?.stack || error));
process.exit(1);
});

View file

@ -284,9 +284,32 @@ export function ExecutionsView({
return formatDuration(selectedExecution.createdAt, selectedExecution.finishedAt);
}, [selectedExecution]);
const showSummaryCards = selectedExecution?.status === "completed";
const showProgressPanel =
selectedExecution?.status === "completed" ||
(selectedExecution ? isExecutionInProgress(selectedExecution.status) : false);
const hasProgressSnapshot = Boolean(
selectedExecution?.progress &&
(typeof selectedExecution.progress.done === "number" ||
typeof selectedExecution.progress.total === "number" ||
typeof selectedExecution.progress.percent === "number" ||
typeof selectedExecution.progress.rate === "number" ||
typeof selectedExecution.progress.eta_sec === "number"),
) || Boolean(
selectedExecution?.column_progress &&
(typeof selectedExecution.column_progress.done === "number" ||
typeof selectedExecution.column_progress.total === "number" ||
typeof selectedExecution.column_progress.percent === "number"),
) || Boolean(
selectedExecution?.batch &&
(typeof selectedExecution.batch.idx === "number" ||
typeof selectedExecution.batch.total === "number"),
);
const selectedStatus = selectedExecution?.status ?? null;
const isSelectedExecutionInProgress = selectedStatus
? isExecutionInProgress(selectedStatus)
: false;
const showProgressPanel = Boolean(selectedExecution) && (
selectedStatus === "completed" ||
isSelectedExecutionInProgress ||
hasProgressSnapshot
);
const progressComplete = selectedExecution?.status === "completed";
const progressPercent = selectedExecution?.progress?.percent ?? (progressComplete ? 100 : 0);
const batchTotal = selectedExecution?.batch?.total ?? null;
@ -400,118 +423,115 @@ export function ExecutionsView({
</div>
)}
{(selectedExecution.status === "completed" ||
isExecutionInProgress(selectedExecution.status)) && (
<Tabs value={detailTab} onValueChange={setDetailTab}>
<div className="flex items-center justify-between gap-2">
<TabsList className="border border-border/60 bg-card/40">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="columns">Columns</TabsTrigger>
<TabsTrigger value="data">Data</TabsTrigger>
<TabsTrigger value="raw">Raw</TabsTrigger>
</TabsList>
{canCancel && (
<Button
type="button"
size="sm"
variant="outline"
onClick={() => onCancelExecution(selectedExecution.id)}
>
Cancel
</Button>
)}
</div>
<TabsContent value="overview">
<ExecutionOverviewTab
execution={selectedExecution}
showSummaryCards={showSummaryCards}
recordsMetric={recordsMetric}
totalMetric={totalMetric}
runDuration={runDuration}
columnCount={columnCount}
llmColumnCount={llmColumnCount}
nullRate={nullRate}
sideEffects={sideEffects}
lowUniquenessColumns={lowUniquenessColumns}
modelUsageRows={modelUsageRows}
terminalLines={terminalLines}
terminalRef={terminalRef}
onTerminalScroll={(event) => {
const element = event.currentTarget;
const distanceFromBottom =
element.scrollHeight - element.scrollTop - element.clientHeight;
shouldStickTerminalToBottomRef.current =
distanceFromBottom <= TERMINAL_STICKY_BOTTOM_THRESHOLD_PX;
}}
/>
</TabsContent>
<TabsContent value="columns">
<ExecutionColumnsTab analysisColumns={analysisColumns} />
</TabsContent>
<TabsContent value="data">
<ExecutionDataTab
execution={selectedExecution}
datasetColumnNames={datasetColumnNames}
hiddenDatasetColumns={hiddenDatasetColumns}
canPageDataset={canPageDataset}
currentDatasetPage={currentDatasetPage}
totalPages={totalPages}
tableColumns={tableColumns}
datasetRowsForTable={datasetRowsForTable}
visibleDatasetColumnNames={visibleDatasetColumnNames}
expandedDatasetRows={expandedDatasetRows}
selectedExecutionIdSafe={selectedExecutionIdSafe}
onSetHiddenColumns={(updater) => {
<Tabs value={detailTab} onValueChange={setDetailTab}>
<div className="flex items-center justify-between gap-2">
<TabsList className="border border-border/60 bg-card/40">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="columns">Columns</TabsTrigger>
<TabsTrigger value="data">Data</TabsTrigger>
<TabsTrigger value="raw">Raw</TabsTrigger>
</TabsList>
{canCancel && (
<Button
type="button"
size="sm"
variant="outline"
onClick={() => onCancelExecution(selectedExecution.id)}
>
Cancel
</Button>
)}
</div>
<TabsContent value="overview">
<ExecutionOverviewTab
execution={selectedExecution}
showSummaryCards={showSummaryCards}
recordsMetric={recordsMetric}
totalMetric={totalMetric}
runDuration={runDuration}
columnCount={columnCount}
llmColumnCount={llmColumnCount}
nullRate={nullRate}
sideEffects={sideEffects}
lowUniquenessColumns={lowUniquenessColumns}
modelUsageRows={modelUsageRows}
terminalLines={terminalLines}
terminalRef={terminalRef}
onTerminalScroll={(event) => {
const element = event.currentTarget;
const distanceFromBottom =
element.scrollHeight - element.scrollTop - element.clientHeight;
shouldStickTerminalToBottomRef.current =
distanceFromBottom <= TERMINAL_STICKY_BOTTOM_THRESHOLD_PX;
}}
/>
</TabsContent>
<TabsContent value="columns">
<ExecutionColumnsTab analysisColumns={analysisColumns} />
</TabsContent>
<TabsContent value="data">
<ExecutionDataTab
execution={selectedExecution}
datasetColumnNames={datasetColumnNames}
hiddenDatasetColumns={hiddenDatasetColumns}
canPageDataset={canPageDataset}
currentDatasetPage={currentDatasetPage}
totalPages={totalPages}
tableColumns={tableColumns}
datasetRowsForTable={datasetRowsForTable}
visibleDatasetColumnNames={visibleDatasetColumnNames}
expandedDatasetRows={expandedDatasetRows}
selectedExecutionIdSafe={selectedExecutionIdSafe}
onSetHiddenColumns={(updater) => {
const selectedId = selectedExecution.id;
setHiddenDatasetColumnsByExecution((current) => {
const currentColumns = current[selectedId] ?? [];
return {
...current,
[selectedId]: updater(currentColumns),
};
});
}}
onPrevPage={() => {
if (selectedExecution.kind === "preview") {
const selectedId = selectedExecution.id;
setHiddenDatasetColumnsByExecution((current) => {
const currentColumns = current[selectedId] ?? [];
return {
...current,
[selectedId]: updater(currentColumns),
};
});
}}
onPrevPage={() => {
if (selectedExecution.kind === "preview") {
const selectedId = selectedExecution.id;
setPreviewDatasetPageByExecution((current) => ({
...current,
[selectedId]: Math.max(1, currentDatasetPage - 1),
}));
return;
}
onLoadDatasetPage(selectedExecution.id, currentDatasetPage - 1);
}}
onNextPage={() => {
if (selectedExecution.kind === "preview") {
const selectedId = selectedExecution.id;
setPreviewDatasetPageByExecution((current) => ({
...current,
[selectedId]: Math.min(totalPages, currentDatasetPage + 1),
}));
return;
}
onLoadDatasetPage(selectedExecution.id, currentDatasetPage + 1);
}}
onToggleRowExpanded={(rowId) => {
setExpandedDatasetRowsByExecution((current) => {
const rows = current[selectedExecution.id] ?? {};
return {
...current,
[selectedExecution.id]: {
...rows,
[rowId]: !rows[rowId],
},
};
});
}}
/>
</TabsContent>
<TabsContent value="raw">
<ExecutionRawTab rawExecution={rawExecution} />
</TabsContent>
</Tabs>
)}
setPreviewDatasetPageByExecution((current) => ({
...current,
[selectedId]: Math.max(1, currentDatasetPage - 1),
}));
return;
}
onLoadDatasetPage(selectedExecution.id, currentDatasetPage - 1);
}}
onNextPage={() => {
if (selectedExecution.kind === "preview") {
const selectedId = selectedExecution.id;
setPreviewDatasetPageByExecution((current) => ({
...current,
[selectedId]: Math.min(totalPages, currentDatasetPage + 1),
}));
return;
}
onLoadDatasetPage(selectedExecution.id, currentDatasetPage + 1);
}}
onToggleRowExpanded={(rowId) => {
setExpandedDatasetRowsByExecution((current) => {
const rows = current[selectedExecution.id] ?? {};
return {
...current,
[selectedExecution.id]: {
...rows,
[rowId]: !rows[rowId],
},
};
});
}}
/>
</TabsContent>
<TabsContent value="raw">
<ExecutionRawTab rawExecution={rawExecution} />
</TabsContent>
</Tabs>
</div>
)}
</section>