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
+128
View File
@@ -144,6 +144,134 @@ describe("runCheck", () => {
expect(codes(runCheck(dir))).toContain("TTTK-C004");
});
test("D002 flags an unquoted attribute value containing a space", () => {
const dir = fixture({
"doc.md": ":md-pins[./map.png]{pins=A:30, 40}\n",
});
expect(codes(runCheck(dir))).toContain("TTTK-D002");
});
test("D002 accepts a quoted attribute value containing a space", () => {
const dir = fixture({
"doc.md": ':md-pins[./map.png]{pins="A:30, 40"}\n',
});
expect(codes(runCheck(dir))).not.toContain("TTTK-D002");
});
test("D002 accepts concatenated class shorthands and boolean flags", () => {
const dir = fixture({
"doc.md": ':md-border[./frame.png]{width="2mm" slice="10" .all.round}\n',
});
expect(codes(runCheck(dir))).not.toContain("TTTK-D002");
});
test("R004 flags a declare row with an empty key or expr", () => {
const dir = fixture({
"doc.md": "```csv role=declare\nkey,expr\n$hp,1\n,2\n```\n",
});
const report = runCheck(dir);
const diagnostic = report.diagnostics.find((d) => d.code === "TTTK-R004");
expect(diagnostic).toBeDefined();
expect(diagnostic!.line).toBe(4);
});
test("V001 flags a CSV that fails to parse", () => {
const dir = fixture({
"doc.md": "See :md-table[./bad.csv].\n",
"bad.csv": 'label,body\n1,"unterminated\n',
});
const report = runCheck(dir);
const diagnostic = report.diagnostics.find((d) => d.code === "TTTK-V001");
expect(diagnostic).toBeDefined();
expect(diagnostic!.file).toBe("bad.csv");
});
test("V002 flags a row whose column count differs from the header", () => {
const dir = fixture({
"doc.md": "See :md-table[./data.csv].\n",
"data.csv": "label,body\n1,a\n2,b,c\n",
});
const report = runCheck(dir);
const diagnostic = report.diagnostics.find((d) => d.code === "TTTK-V002");
expect(diagnostic).toBeDefined();
expect(diagnostic!.line).toBe(3);
});
test("V003 flags duplicate label values", () => {
const dir = fixture({
"doc.md": "See :md-table[./data.csv].\n",
"data.csv": "label,body\n1,a\n1,b\n",
});
const report = runCheck(dir);
const diagnostic = report.diagnostics.find((d) => d.code === "TTTK-V003");
expect(diagnostic).toBeDefined();
expect(diagnostic!.line).toBe(3);
});
test("V004 flags an unresolved {{prop}} in the body column", () => {
const dir = fixture({
"doc.md": "See :md-table[./data.csv].\n",
"data.csv": "label,body\n1,{{missing}}\n",
});
expect(codes(runCheck(dir))).toContain("TTTK-V004");
});
test("V004 accepts a {{prop}} naming a column or front-matter key", () => {
const dir = fixture({
"doc.md": "See :md-table[./data.csv].\n",
"data.csv": "---\nsystem: D&D\n---\nlabel,body,name\n1,{{name}} of {{system}}\n",
});
expect(codes(runCheck(dir))).not.toContain("TTTK-V004");
});
test("V005 flags a spark-table roll value that is not a number or range", () => {
const dir = fixture({
"doc.md": "```csv role=spark-table\nd6,result\nx,foo\n```\n",
});
const report = runCheck(dir);
const diagnostic = report.diagnostics.find((d) => d.code === "TTTK-V005");
expect(diagnostic).toBeDefined();
expect(diagnostic!.line).toBe(3);
});
test("a clean CSV produces no diagnostics", () => {
const dir = fixture({
"doc.md": "See :md-table[./data.csv].\n",
"data.csv": "label,body\n1,a\n2,b\n",
});
expect(runCheck(dir).diagnostics).toEqual([]);
});
test("a CSV referenced by two documents is only reported once", () => {
const dir = fixture({
"a.md": "See :md-table[./data.csv].\n",
"b.md": "See :md-table[./data.csv].\n",
"data.csv": "label,body\n1,a\n1,b\n",
});
const v003 = runCheck(dir).diagnostics.filter(
(d) => d.code === "TTTK-V003",
);
expect(v003).toHaveLength(1);
});
test("directives inside fenced blocks are not live content", () => {
const dir = fixture({
"doc.md": "```\n:md-table[./missing.csv]\n:md-card[x]\n```\n",