feat(declare)!: parse role=declare blocks as YAML
Replace the tag/threshold/key/expr CSV body with YAML `vars:` and `tags:` maps. `parseDeclare` validates the body with an exported Zod schema and returns the same declaration and modifier shapes the reactivity engine already consumes. The breaking rename from `parseDeclareCsv` updates the registry, the journal re-exports, and the `tttk check` role checks, where R003 now reports schema errors with their path and R004 warns when a block declares nothing.
This commit is contained in:
@@ -2,8 +2,7 @@
|
||||
* Code-block role checks.
|
||||
*/
|
||||
|
||||
import { parse } from "csv-parse/browser/esm/sync";
|
||||
import { parseDeclareCsv } from "../../completions/declare-parser.js";
|
||||
import { parseDeclare, type DeclareResult } from "../../completions/declare-parser.js";
|
||||
import { diagnosticAt } from "../diagnostics.js";
|
||||
import { ELEMENT_NAMES, KNOWN_ROLES } from "../known.js";
|
||||
import type { RawFence } from "../scan.js";
|
||||
@@ -60,8 +59,25 @@ function checkSparkTable(fence: RawFence, ctx: CheckContext): void {
|
||||
|
||||
function checkDeclare(fence: RawFence, ctx: CheckContext): void {
|
||||
const { source, file, sink } = ctx;
|
||||
|
||||
if (/^csv$/i.test(fence.lang)) {
|
||||
sink.report(
|
||||
diagnosticAt(
|
||||
file,
|
||||
source,
|
||||
fence.offset,
|
||||
"TTTK-R003",
|
||||
"error",
|
||||
"role=declare blocks are YAML, not CSV",
|
||||
"use ```yaml role=declare with `vars:` and `tags:` maps",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let result: DeclareResult;
|
||||
try {
|
||||
parseDeclareCsv(fence.body, file);
|
||||
result = parseDeclare(fence.body);
|
||||
} catch (error) {
|
||||
sink.report(
|
||||
diagnosticAt(
|
||||
@@ -71,51 +87,22 @@ function checkDeclare(fence: RawFence, ctx: CheckContext): void {
|
||||
"TTTK-R003",
|
||||
"error",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
"role=declare needs `key` and `expr` columns; `tag` and `threshold` are optional",
|
||||
"role=declare is YAML: `vars:` maps `$name` to an expression; `tags:` maps a tag name to threshold maps",
|
||||
),
|
||||
);
|
||||
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;
|
||||
if (result.variables.length === 0 && result.tagModifiers.length === 0) {
|
||||
sink.report(
|
||||
diagnosticAt(
|
||||
file,
|
||||
source,
|
||||
source.lineStart(baseLine + info.lines - 1),
|
||||
fence.offset,
|
||||
"TTTK-R004",
|
||||
"warning",
|
||||
`declare row skipped: ${!key ? "empty key" : "empty expr"}`,
|
||||
"every declare row needs both a key and an expression",
|
||||
"role=declare block declares nothing",
|
||||
"add a `vars:` or `tags:` mapping; the block has no effect",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user