feat(cli): Complete the check rule catalog

Add D002 (malformed attribute blocks), R004 (skipped declare rows), and V001-V005 (CSV data files). CSV checks run once per referenced file and also cover inline spark-table bodies.

Generate docs/reference.md from the known.ts catalog via npm run gen:reference, and add drift guards asserting the catalog matches the customElement call sites and the committed reference.
This commit is contained in:
2026-09-12 17:01:49 +08:00
parent ca6d8d1d14
commit 71542e7453
17 changed files with 1167 additions and 16 deletions
+45
View File
@@ -2,6 +2,7 @@
* Code-block role checks.
*/
import { parse } from "csv-parse/browser/esm/sync";
import { parseDeclareCsv } from "../../completions/declare-parser.js";
import { diagnosticAt } from "../diagnostics.js";
import { ELEMENT_NAMES, KNOWN_ROLES } from "../known.js";
@@ -73,6 +74,50 @@ function checkDeclare(fence: RawFence, ctx: CheckContext): void {
"role=declare needs `key` and `expr` columns; `tag` and `threshold` are optional",
),
);
return;
}
checkDeclareRows(fence, ctx);
}
/**
* R004 — `parseDeclareCsv` silently skips rows with an empty `key` or `expr`.
* Re-parse with line info so the skipped row can be localized.
*/
function checkDeclareRows(fence: RawFence, ctx: CheckContext): void {
const { source, file, sink } = ctx;
let records: Array<{ record: Record<string, string>; info: { lines: number } }>;
try {
records = parse(fence.body, {
columns: true,
trim: true,
skipEmptyLines: true,
info: true,
}) as unknown as typeof records;
} catch {
return; // R003 already reported the parse failure
}
if (records.length === 0) return;
const headers = Object.keys(records[0].record).map((h) => h.toLowerCase());
if (!headers.includes("key") || !headers.includes("expr")) return;
const baseLine = source.position(fence.bodyOffset).line;
for (const { record, info } of records) {
const key = record.key ?? "";
const expr = record.expr ?? "";
if (key && expr) continue;
sink.report(
diagnosticAt(
file,
source,
source.lineStart(baseLine + info.lines - 1),
"TTTK-R004",
"warning",
`declare row skipped: ${!key ? "empty key" : "empty expr"}`,
"every declare row needs both a key and an expression",
),
);
}
}