diff --git a/src/review/content-lane/duplicates.ts b/src/review/content-lane/duplicates.ts index 64fb1cdcbd..e1f2e2cdf5 100644 --- a/src/review/content-lane/duplicates.ts +++ b/src/review/content-lane/duplicates.ts @@ -40,6 +40,20 @@ export type ContentDuplicateReview = { relatedCandidates: ContentDuplicateMatch[]; }; +function stripYamlComment(value: string): string { + return value.replace(/\s+#.*$/, "").trim(); +} + +// A YAML block-scalar header indicator: `|` or `>` with an optional chomping (`+`/`-`) and/or a single indentation +// digit, in EITHER order — `|`, `>`, `|-`, `>+`, `|2`, `|2-`, `|-2`, `>2+`. Mirrors source-evidence.ts (#8016). +const BLOCK_SCALAR_INDICATOR = /^[|>](?:[+-]?\d?|\d[+-]?)$/; + +// True when a raw frontmatter value is a block-scalar header. Comment-tolerant: a header may carry a trailing inline +// comment (`| # sources below`), which normalizes away before the indicator is matched. +function isBlockScalarHeader(raw: string): boolean { + return BLOCK_SCALAR_INDICATOR.test(stripYamlComment(raw)); +} + function unquoteYamlScalar(value: string): string { const trimmed = value.trim(); if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) { @@ -74,7 +88,7 @@ export function parseSimpleFrontmatter(source: string): Record { /* v8 ignore next -- noUncheckedIndexedAccess fallback: capture group 2 (.*) always participates when head matches, so head[2] is never undefined */ const inline = (head[2] ?? "").trim(); i += 1; - if (/^[|>][+-]?\d*$/.test(inline)) { + if (isBlockScalarHeader(inline)) { // Block literal (`|`) / folded (`>`) scalar: gather the indented block that follows. const block: string[] = []; /* v8 ignore next -- noUncheckedIndexedAccess fallback: i < lines.length guards the index; split() elements are always strings */ @@ -129,7 +143,7 @@ export function findDuplicateFrontmatterKeys(source: string): string[] { /* v8 ignore next -- noUncheckedIndexedAccess fallback: capture group 2 (.*) always participates when head matches, so head[2] is never undefined */ const inline = (head[2] ?? "").trim(); i += 1; - if (/^[|>][+-]?\d*$/.test(inline)) { + if (isBlockScalarHeader(inline)) { /* v8 ignore next -- noUncheckedIndexedAccess fallback: i < lines.length guards the index; split() elements are always strings */ while (i < lines.length && ((lines[i] ?? "").trim() === "" || /^\s/.test(lines[i] ?? ""))) i += 1; } else if (inline === "") { diff --git a/test/unit/content-lane-duplicates.test.ts b/test/unit/content-lane-duplicates.test.ts index 9d63f1846b..1d6bbb8946 100644 --- a/test/unit/content-lane-duplicates.test.ts +++ b/test/unit/content-lane-duplicates.test.ts @@ -58,6 +58,27 @@ describe("parseSimpleFrontmatter", () => { expect(f.title).toBe("Real"); expect(Object.keys(f)).toEqual(["title"]); }); + + it("reads a |2- block-scalar header (digit-before-chomp order) and its indented body (#9290)", () => { + const src = ["---", "description: |2-", " line one", " line two", "title: T", "---", "", "body"].join("\n"); + const f = parseSimpleFrontmatter(src); + expect(f.description).toBe("line one\nline two"); + expect(f.title).toBe("T"); + }); + + it("reads a |- # comment block-scalar header and its indented body (#9290)", () => { + const src = ["---", "description: |- # trailing note", " hidden content", "title: T", "---", "", "body"].join("\n"); + const f = parseSimpleFrontmatter(src); + expect(f.description).toBe("hidden content"); + expect(f.title).toBe("T"); + }); + + it("reads a > # comment folded block-scalar header and joins its indented body (#9290)", () => { + const src = ["---", "description: > # folded note", " word one", " word two", "title: T", "---", "", "body"].join("\n"); + const f = parseSimpleFrontmatter(src); + expect(f.description).toBe("word one word two"); + expect(f.title).toBe("T"); + }); }); describe("findDuplicateFrontmatterKeys", () => { @@ -283,6 +304,49 @@ describe("findDuplicateFrontmatterKeys — block-scalar + sequence skipping", () // The blank + comment lines fail the key regex → the `if (!head) continue` branch runs; no dupes. expect(findDuplicateFrontmatterKeys(src)).toEqual([]); }); + + it("skips a |2- block-scalar body without false-flagging indented pseudo-keys (#9290)", () => { + const src = [ + "---", + "description: |2-", + " title: not-a-real-key", + " another: line", + "title: Real", + "title: DupReal", + "---", + "", + "body", + ].join("\n"); + expect(findDuplicateFrontmatterKeys(src)).toEqual(["title"]); + }); + + it("skips a |- # comment block-scalar body without false-flagging indented pseudo-keys (#9290)", () => { + const src = [ + "---", + "description: |- # trailing note", + " title: not-a-real-key", + "slug: a", + "slug: b", + "---", + "", + "body", + ].join("\n"); + expect(findDuplicateFrontmatterKeys(src)).toEqual(["slug"]); + }); + + it("skips a > # comment folded block-scalar body without false-flagging indented pseudo-keys (#9290)", () => { + const src = [ + "---", + "description: > # folded note", + " title: not-a-real-key", + "category: x", + "category: y", + "---", + "", + "body", + ].join("\n"); + expect(findDuplicateFrontmatterKeys(src)).toEqual(["category"]); + }); }); describe("normalizeUrl edge cases (via extractContentDuplicateSignals)", () => {