Files
Rental-operations-platform/scripts/validate-phase15-readiness.mjs

112 lines
3.4 KiB
JavaScript

import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fail, pass, repositoryRoot } from './lib.mjs';
function parseCsv(text) {
const rows = [];
let row = [];
let field = '';
let quoted = false;
for (let index = 0; index < text.length; index += 1) {
const character = text[index];
if (quoted) {
if (character === '"' && text[index + 1] === '"') {
field += '"';
index += 1;
} else if (character === '"') {
quoted = false;
} else {
field += character;
}
} else if (character === '"') {
quoted = true;
} else if (character === ',') {
row.push(field);
field = '';
} else if (character === '\n') {
row.push(field.replace(/\r$/, ''));
rows.push(row);
row = [];
field = '';
} else {
field += character;
}
}
if (quoted) throw new Error('Unterminated CSV quote.');
if (field || row.length) {
row.push(field.replace(/\r$/, ''));
rows.push(row);
}
return rows;
}
const failures = [];
for (const file of [
'docs/IMPLEMENTATION_ISSUE_REGISTER_v1.4.csv',
'docs/quality/PHASE_14_MANUAL_QA_RESULTS_v1.0.csv',
]) {
const rows = parseCsv(await readFile(path.join(repositoryRoot, file), 'utf8'));
const expected = rows[0]?.length ?? 0;
for (const [index, row] of rows.entries()) {
if (row.length !== expected) {
failures.push(`${file}:${index + 1} has ${row.length} columns; expected ${expected}.`);
}
}
}
const contentType = await readFile(
path.join(repositoryRoot, 'src/lib/security/content-type.ts'),
'utf8',
);
const route = await readFile(path.join(repositoryRoot, 'src/app/api/demo/route.ts'), 'utf8');
if (!contentType.includes("=== 'application/json'")) {
failures.push('Exact JSON media-type comparison is missing.');
}
if (!route.includes('isApplicationJson(contentType)')) {
failures.push('Demo API does not use the exact JSON media-type helper.');
}
if (route.includes("startsWith('application/json')")) {
failures.push('Demo API still accepts look-alike JSON media types by prefix.');
}
const componentVisual = await readFile(
path.join(repositoryRoot, 'tests/visual/component-system.visual.spec.ts'),
'utf8',
);
const integrationVisual = await readFile(
path.join(repositoryRoot, 'tests/visual/integrations.visual.spec.ts'),
'utf8',
);
if (!componentVisual.includes('hpc.theme.preference')) {
failures.push('Component visual test does not use the canonical theme storage key.');
}
if (componentVisual.includes('rdg-theme-preference')) {
failures.push('Component visual test still uses the stale theme storage key.');
}
for (const [name, text] of [
['component-system.visual.spec.ts', componentVisual],
['integrations.visual.spec.ts', integrationVisual],
]) {
if (!text.includes("project.name !== 'chromium'")) {
failures.push(`${name} is not restricted to canonical Chromium snapshots.`);
}
}
const workflow = await readFile(path.join(repositoryRoot, '.github/workflows/ci.yml'), 'utf8');
for (const command of [
'pnpm test:integration',
'pnpm test:security',
'pnpm test:privacy',
'pnpm test:browser',
'pnpm test:a11y',
'pnpm test:visual',
]) {
if (!workflow.includes(command)) failures.push(`CI omits required command: ${command}`);
}
if (failures.length) fail(failures);
else
pass(
'Phase 15 evidence CSVs, exact media-type control, deterministic visual setup, and CI test gates are present.',
);