diff --git a/src/review/content-lane/source-evidence.ts b/src/review/content-lane/source-evidence.ts index 8f03b4cc18..2f2aaf0b32 100644 --- a/src/review/content-lane/source-evidence.ts +++ b/src/review/content-lane/source-evidence.ts @@ -162,7 +162,23 @@ function parseSimpleFrontmatter(source: string): Record { i += 1; } fields[key] = block.join(inline.startsWith(">") ? " " : "\n").trim(); - } else if (inline !== "") { + } else if (inline === "") { + // Block/flow sequence (or nested map) on the following indented lines: gather each `- item` so a + // scalar-only field authored as a YAML sequence is still captured, matching duplicates.ts's parser + // (#8016; this branch existed only there, so such a field was invisible to the source-evidence gate). + const items: string[] = []; + // `lines[i]` is bounded by the `i < lines.length` loop guard; the `?? ""` is an unreachable + // noUncheckedIndexedAccess fallback (same guard as the block-scalar loop above). + /* v8 ignore next */ + while (i < lines.length && /^\s/.test(lines[i] ?? "") && (lines[i] ?? "").trim() !== "") { + // `lines[i]` reuses the same in-bounds index already validated by `/^\s/.test` above; `?? ""` + // cannot fire (unreachable noUncheckedIndexedAccess fallback). + /* v8 ignore next */ + items.push((lines[i] ?? "").replace(/^\s*-\s*/, "").trim()); + i += 1; + } + fields[key] = items.join(", "); + } else { fields[key] = unquoteYamlScalar(inline); } } diff --git a/test/unit/content-lane-source-evidence.test.ts b/test/unit/content-lane-source-evidence.test.ts index 91c55d36dc..5738706730 100644 --- a/test/unit/content-lane-source-evidence.test.ts +++ b/test/unit/content-lane-source-evidence.test.ts @@ -89,6 +89,17 @@ describe("checkSubmittedSourceEvidence", () => { expect(report.status).toBe("failed"); }); + it("fetch-verifies a scalar-only source authored as a YAML sequence, not just a scalar (#8016)", async () => { + // documentationUrl is a scalar-only field (NOT in the list-field set that listSourceUrlValues rescues), + // so before the ported sequence branch this URL was dropped by the parser and the gate could never + // verify it. It must now be captured and fetch-checked exactly like a scalar value. + const url = "https://docs.acme.example/seq-guide"; + const src = ["---", "documentationUrl:", ` - ${url}`, "---", "", "body"].join("\n"); + const report = await checkSubmittedSourceEvidence(src, fakeFetch({ [url]: 200 })); + expect(report.urls.map((u) => u.url)).toContain(url); + expect(report.status).toBe("passed"); + }); + it("is retryable (not hard) on a 403/429/5xx canonical source", async () => { const src = mdx({ githubUrl: "https://github.com/acme/x" }); const report = await checkSubmittedSourceEvidence(src, fakeFetch({ "https://github.com/acme/x": 403 })); @@ -312,6 +323,18 @@ describe("extractSubmittedSourceUrls — frontmatter parsing edge cases", () => ); }); + it("captures a scalar-only source field authored as a YAML block sequence (#8016)", () => { + // A scalar field (documentationUrl) written as a sequence was silently dropped here — the sequence + // branch existed only in duplicates.ts — so the URL never reached the fetch-reachability gate. + // documentationUrl is NOT a list field, so listSourceUrlValues does not rescue it; only the ported + // parser branch captures it. + const src = ["---", "documentationUrl:", " - https://docs.acme.example/guide", "---", "", "body"].join("\n"); + const urls = extractSubmittedSourceUrls(src); + expect(urls.map((u) => `${u.field}:${u.url}`)).toContain( + "documentationUrl:https://docs.acme.example/guide", + ); + }); + it("does not surface any block-scalar header on a list field as a bogus URL (both indicator orders)", () => { // `retrievalSources` is a list field; a block-scalar header is not a URL. The old guard only skipped the bare // `|`/`>`, so `|-` leaked as the literal url "|-". YAML allows chomping and the indentation digit in EITHER