fix(#2788): audit-uat reads human_verification items from frontmatter (#2814)

* fix(#2788): audit-uat reads frontmatter human_verification array

parseVerificationItems only searched the body for a '## Human Verification'
section. gsd-verifier writes items to the frontmatter human_verification:
YAML array, so audit-uat returned total_items: 0 for all such files.

Two fixes:
1. Read frontmatter human_verification: array first (via extractFrontmatter);
   return those items if present (primary path for gsd-verifier output).
2. Relax the body-section heading regex to accept underscore separators and
   parenthetical suffixes (e.g. '## human_verification (action required)').

Regression test: tests/bug-2788-audit-uat-frontmatter.test.cjs

* fix(#2788): address CodeRabbit — trim whitespace entries, support hyphenated headings, bash fence
This commit is contained in:
Tom Boucher
2026-04-28 20:22:59 -04:00
committed by GitHub
parent eddb2a205b
commit a4e15d5616
2 changed files with 229 additions and 3 deletions

View File

@@ -188,11 +188,62 @@ function parseUatItems(content: string): Record<string, unknown>[] {
return items;
}
/**
* Parse frontmatter human_verification: YAML array entries into audit items.
*
* Fixes #2788: when gsd-verifier encodes human items in YAML frontmatter
* rather than the body, parseVerificationItems was returning [] because it
* only searched the body for a "## Human Verification" heading.
*/
function parseVerificationFrontmatterItems(fm: Record<string, unknown>): Record<string, unknown>[] {
const items: Record<string, unknown>[] = [];
const hvArray = fm.human_verification;
if (!Array.isArray(hvArray)) return items;
let i = 0;
for (const entry of hvArray) {
i++;
if (typeof entry === 'string') {
const name = entry.trim();
if (name.length > 0) {
items.push({ test: i, name, result: 'human_needed', category: 'human_uat' });
}
} else if (typeof entry === 'object' && entry !== null) {
const obj = entry as Record<string, unknown>;
// Accept any string property as the item name; prefer 'test' key.
const name = (obj.test as string | undefined) || (obj.name as string | undefined) || '';
if (name) {
const item: Record<string, unknown> = {
test: i,
name: String(name).trim(),
result: 'human_needed',
category: 'human_uat',
};
if (obj.expected) item.expected = String(obj.expected).trim();
if (obj.why_human) item.why_human = String(obj.why_human).trim();
items.push(item);
}
}
}
return items;
}
/** Port of `parseVerificationItems` from `uat.cjs`. */
function parseVerificationItems(content: string, status: string): Record<string, unknown>[] {
function parseVerificationItems(content: string, status: string, fm?: Record<string, unknown>): Record<string, unknown>[] {
const items: Record<string, unknown>[] = [];
if (status === 'human_needed') {
const hvSection = content.match(/##\s*Human Verification.*?\n([\s\S]*?)(?=\n##\s|\n---\s|$)/i);
// Check frontmatter human_verification: array first (#2788).
// gsd-verifier writes items here; body-section fallback is secondary.
if (fm) {
const fmItems = parseVerificationFrontmatterItems(fm);
if (fmItems.length > 0) return fmItems;
}
// Body fallback: match ## human_verification or ## Human Verification
// (case-insensitive, underscore or space, with optional parenthetical).
const hvSection = content.match(
/##\s*human[_\s-]verification[^\n]*\n([\s\S]*?)(?=\n##\s|\n---\s|$)/i
);
if (hvSection) {
const lines = hvSection[1].split('\n');
for (const line of lines) {
@@ -273,7 +324,7 @@ export const auditUat: QueryHandler = async (_args, projectDir, workstream) => {
const fm = extractFrontmatter(content);
const status = (fm.status || 'unknown') as string;
if (status === 'human_needed' || status === 'gaps_found') {
const items = parseVerificationItems(content, status);
const items = parseVerificationItems(content, status, fm);
if (items.length > 0) {
results.push({
phase: phaseNum,