Files
gpt-load/web/scripts/check-accessibility-evidence.mjs
T

121 lines
4.7 KiB
JavaScript

import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const expectedRows = [
'linux-chromium-automated',
'linux-webkit-automated',
'macos-chrome-manual',
'macos-safari-manual',
'safari-voiceover-manual',
'windows-chrome-nvda-external',
]
const allowedStatuses = new Set(['PASS', 'NOT RUN', 'EXTERNAL'])
const allowedModes = new Set([
'automated',
'manual-geometry-functional',
'manual-at',
'external-at',
])
function assert(condition, message) {
if (!condition) throw new Error(`accessibility evidence: ${message}`)
}
function nonBlank(value) {
return typeof value === 'string' && value.trim().length > 0
}
function validSHA(value, length) {
return typeof value === 'string' && new RegExp(`^[0-9a-f]{${length}}$`).test(value)
}
export function validateAccessibilityEvidence(matrix) {
assert(matrix && typeof matrix === 'object', 'matrix must be an object')
assert(matrix.schema_version === 1, 'unsupported schema version')
assert(validSHA(matrix.source_sha, 40), 'source SHA must be a full commit SHA')
assert(nonBlank(matrix.generated_at), 'generated_at is required')
assert(Array.isArray(matrix.rows), 'rows must be an array')
assert(matrix.rows.length === expectedRows.length, 'matrix must contain the approved six rows')
assert(
JSON.stringify(matrix.rows.map((row) => row.id)) === JSON.stringify(expectedRows),
'matrix rows must use the approved order and identifiers',
)
for (const row of matrix.rows) {
assert(allowedModes.has(row.mode), `${row.id} has an unsupported mode`)
assert(allowedStatuses.has(row.status), `${row.id} has an unsupported status`)
assert(row.source_sha === matrix.source_sha, `${row.id} source SHA does not match the matrix`)
assert(nonBlank(row.operator), `${row.id} operator is required`)
assert(nonBlank(row.environment?.os_name), `${row.id} OS name is required`)
assert(nonBlank(row.environment?.os_version), `${row.id} OS version is required`)
assert(nonBlank(row.environment?.architecture), `${row.id} architecture is required`)
assert(nonBlank(row.browser?.name), `${row.id} browser name is required`)
assert(nonBlank(row.browser?.version), `${row.id} browser version is required`)
assert(
nonBlank(row.assistive_technology?.name),
`${row.id} assistive technology name is required`,
)
assert(
nonBlank(row.assistive_technology?.version),
`${row.id} assistive technology version is required`,
)
assert(
Array.isArray(row.scenarios) && row.scenarios.length > 0 && row.scenarios.every(nonBlank),
`${row.id} scenarios are required`,
)
assert(Array.isArray(row.artifacts), `${row.id} artifacts must be an array`)
assert(nonBlank(row.notes), `${row.id} notes are required`)
if (row.status === 'PASS') {
assert(nonBlank(row.executed_at), `${row.id} PASS requires an execution timestamp`)
assert(row.artifacts.length > 0, `${row.id} PASS requires artifacts`)
for (const artifact of row.artifacts) {
assert(nonBlank(artifact.path), `${row.id} artifact path is required`)
assert(validSHA(artifact.sha256, 64), `${row.id} artifact SHA-256 is invalid`)
}
assert(
row.mode === 'automated' ||
(!row.operator.toLowerCase().includes('automated') &&
row.operator.toLowerCase() !== 'codex'),
`${row.id} human evidence cannot be passed by automation`,
)
} else {
assert(row.executed_at === null, `${row.id} non-PASS row must not claim execution`)
}
}
return matrix
}
async function validateArtifactFiles(matrix) {
for (const row of matrix.rows) {
for (const artifact of row.artifacts) {
const path = resolve(webRoot, artifact.path)
assert(path.startsWith(`${webRoot}/`), `${row.id} artifact escaped the web root`)
const source = await readFile(path)
const digest = createHash('sha256').update(source).digest('hex')
assert(digest === artifact.sha256, `${row.id} artifact hash does not match ${artifact.path}`)
}
}
}
async function main() {
const matrixPath = resolve(webRoot, process.argv[2] ?? 'evidence/accessibility-matrix.json')
const matrix = validateAccessibilityEvidence(JSON.parse(await readFile(matrixPath, 'utf8')))
await validateArtifactFiles(matrix)
process.stdout.write(
`${JSON.stringify({
status: 'PASS',
source_sha: matrix.source_sha,
rows: matrix.rows.length,
passed: matrix.rows.filter((row) => row.status === 'PASS').map((row) => row.id),
})}\n`,
)
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await main()
}