From b81bce612af2533cc890ee4c68ad06be6304cccd Mon Sep 17 00:00:00 2001 From: Amir Hormati Date: Fri, 14 Aug 2026 10:45:45 -0700 Subject: [PATCH] mdcode: reject non-list frontmatter tags A JavaScript string is iterable, so `tags: a, b, c` was expanded into one Dataplex label per character rather than three labels. Throw instead, naming the file, so the author gets an error rather than silent corruption. Splitting the string on commas would be the other option, but a comma can appear inside a tag, so coercing guesses at a tag set the author did not write. Co-Authored-By: Claude Opus 4.7 --- toolbox/mdcode/src/libts/layouts/documents.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/toolbox/mdcode/src/libts/layouts/documents.ts b/toolbox/mdcode/src/libts/layouts/documents.ts index 84b4e791..c7b232e4 100644 --- a/toolbox/mdcode/src/libts/layouts/documents.ts +++ b/toolbox/mdcode/src/libts/layouts/documents.ts @@ -64,7 +64,7 @@ export class DocumentsLayout implements CatalogLayout { let body = ''; if (entryPath) { const content = await fs.promises.readFile(entryPath, 'utf8'); - const result = parseMarkdown(content); + const result = parseMarkdown(content, entryPath); parsed = result.entry; body = result.body; } @@ -182,7 +182,7 @@ function deriveParentLocalName(name: string): string | undefined { return [...parentDir, INDEX_NAME].join('/'); } -export function parseMarkdown(content: string): { entry: md.Entry|null; body: string } { +export function parseMarkdown(content: string, source?: string): { entry: md.Entry|null; body: string } { const lines = content.split(/\r?\n/); if (lines[0] !== '---') { return { entry: null, body: content }; @@ -203,7 +203,14 @@ export function parseMarkdown(content: string): { entry: md.Entry|null; body: st entry.resource = entry.resource ?? {} entry.resource.displayName = metadata.title; entry.resource.description = metadata.description; - if (metadata.tags) { + if (metadata.tags !== undefined && metadata.tags !== null) { + if (!Array.isArray(metadata.tags)) { + // A bare string is iterable, so without this the tags would silently + // become one label per character. + throw new Error( + `${source ?? 'frontmatter'}: "tags" must be a list, got ${JSON.stringify(metadata.tags)}`, + ); + } entry.resource.labels = entry.resource.labels ?? {}; for (const tag of metadata.tags) { entry.resource.labels[tag] = 'true';