Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions app/test/coverage-matrix-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,39 @@ Trailing prose.`;
expect(parsed.rows[0].id).toBe('4.4.4');
expect(parsed.errors).toHaveLength(1);
});

it('reports line-numbered errors for invalid ID and status', () => {
const md = `header line
| 1.1.1 | Good | RU | a.rs | ✅ | ok |
| 2.2.2 | Bad status | RU | b.rs | ⚠️ | nope |`;
const parsed = parseMatrix(md);
expect(parsed.errors[0]).toMatch(/^Line 3 \(2\.2\.2\): invalid status/);
});

it('handles a row with empty notes', () => {
const md = `| 6.6.6 | No notes | RU | a.rs | ✅ | |`;
const parsed = parseMatrix(md);
expect(parsed.rows).toHaveLength(1);
expect(parsed.rows[0].notes).toBe('');
});

it('matches rows with trailing whitespace after final pipe', () => {
const md = `| 7.7.7 | Trailing | RU | a.rs | ✅ | ok | `;
const parsed = parseMatrix(md);
expect(parsed.rows).toHaveLength(1);
expect(parsed.rows[0].id).toBe('7.7.7');
});

it('returns totalRows and uniqueRows from validateAgainstCatalog', () => {
const rows = [
{ id: '1.1.1', name: 'A', layer: 'RU', path: 'a', status: '✅', notes: '' },
{ id: '1.1.1', name: 'B', layer: 'RU', path: 'b', status: '✅', notes: '' },
{ id: '2.2.2', name: 'C', layer: 'RU', path: 'c', status: '✅', notes: '' },
];
const result = validateAgainstCatalog(rows, ['1.1.1', '2.2.2']);
expect(result.totalRows).toBe(3);
expect(result.uniqueRows).toBe(2);
expect(result.duplicates).toEqual(['1.1.1']);
expect(result.missingFromMatrix).toEqual([]);
});
});
87 changes: 68 additions & 19 deletions scripts/lib/coverage-matrix-parser.mjs
Original file line number Diff line number Diff line change
@@ -1,37 +1,86 @@
const ROW_REGEX = /^\| (\d+(?:\.\d+){2,3}) \| ([^|]+) \| ([^|]+) \| ([^|]+) \| ([^|]+) \| ([^|]+) \|/;
const ROW_REGEX =
/^\|\s*(\d+(?:\.\d+){2,3})\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]*?)\s*\|\s*$/u;

const ID_REGEX = /^\d+(?:\.\d+){2,3}$/;
Comment thread
senamakel marked this conversation as resolved.
const VALID_STATUS = new Set(['✅', '🟡', '❌', '🚫']);

const VALID_STATUS = new Set(["✅", "🟡", "❌", "🚫"]);

export function parseMatrix(markdown) {
if (typeof markdown !== "string") {
return {
rows: [],
errors: ["Input must be a string"],
};
}

const rows = [];
const errors = [];
if (typeof markdown !== 'string') {
return { rows, errors };
}
for (const line of markdown.split(/\r?\n/)) {
const match = line.match(ROW_REGEX);

const lines = markdown.split(/\r?\n/);

for (let i = 0; i < lines.length; i++) {
const line = lines[i];

const match = ROW_REGEX.exec(line);
if (!match) continue;
const [, id, name, layer, path, status, notes] = match.map((v) => (typeof v === 'string' ? v.trim() : v));

const [, id, name, layer, path, rawStatus, notes] = match;

const status = rawStatus.trim();

if (!ID_REGEX.test(id)) {
errors.push(`Invalid ID format: ${id}`);
errors.push(`Line ${i + 1}: Invalid ID format "${id}"`);
continue;
}

if (!VALID_STATUS.has(status)) {
errors.push(`Row ${id}: invalid status "${status}" (must be one of ${[...VALID_STATUS].join(' ')})`);
errors.push(`Line ${i + 1} (${id}): invalid status "${status}"`);
continue;
}
rows.push({ id, name, layer, path, status, notes });

rows.push({
id,
name: name.trim(),
layer: layer.trim(),
path: path.trim(),
status,
notes: notes.trim(),
});
}

return { rows, errors };
}

export function validateAgainstCatalog(parsedRows, catalogIds) {
const seen = new Map();
for (const row of parsedRows) {
seen.set(row.id, (seen.get(row.id) ?? 0) + 1);
export function validateAgainstCatalog(rows, catalogIds) {
const counts = new Map();

for (const { id } of rows) {
counts.set(id, (counts.get(id) ?? 0) + 1);
}

const duplicates = [];

for (const [id, count] of counts) {
if (count > 1) {
duplicates.push(id);
}
}

const catalogSet =
catalogIds instanceof Set ? catalogIds : new Set(catalogIds);

const missingFromMatrix = [];

for (const id of catalogSet) {
if (!counts.has(id)) {
missingFromMatrix.push(id);
}
}
const duplicates = [...seen.entries()].filter(([, count]) => count > 1).map(([id]) => id);
const present = new Set(seen.keys());
const missingFromMatrix = [...catalogIds].filter((id) => !present.has(id));
return { missingFromMatrix, duplicates };

return {
missingFromMatrix,
duplicates,
totalRows: rows.length,
uniqueRows: counts.size,
Comment thread
senamakel marked this conversation as resolved.
};
}
Loading