Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 30x 2x 28x 28x 159x 26x 1x 25x 1x 24x 1x 23x 1x 22x 1x 21x 1x 20x 1x 19x 1x 18x 28x 28x 1x 27x 1x 26x 1x 25x 1x 24x 2x 22x 11x 1x 11x 1x 10x | /**
* @module roll-forward-pirs/validator
* @description Strict structural + enum validation for `pir-status.json`
* documents. Dependency-free (no ajv).
*
* @author Hack23 AB
* @license Apache-2.0
*/
import {
PIR_ID_PATTERN,
VALID_CONFIDENCES,
VALID_CYCLES,
VALID_STATUSES,
} from './constants.js';
import type { Confidence, CycleType, PirStatus, PirStatusFile } from './types.js';
/**
* Strict structural + enum validation. Validates top-level required fields,
* `schema_version`, `cycle`, `date`, `subfolder`, `generated_at`, optional
* `inherited_from`, `pirs` array shape, and each PIR's `pir_id` pattern,
* `status`, and `confidence` enum membership. Does not call ajv to keep the
* script dependency-free.
*
* @throws Error with descriptive message on any validation failure.
*/
export function validateSource(raw: unknown, filePath: string): PirStatusFile {
if (typeof raw !== 'object' || raw === null) {
throw new Error(`${filePath}: not a JSON object`);
}
const obj = raw as Record<string, unknown>;
for (const key of ['schema_version', 'cycle', 'date', 'subfolder', 'generated_at', 'pirs'] as const) {
if (!(key in obj)) throw new Error(`${filePath}: missing required field '${key}'`);
}
if (obj['schema_version'] !== '1.0') {
throw new Error(
`${filePath}: unsupported schema_version '${String(obj['schema_version'])}'`,
);
}
if (typeof obj['cycle'] !== 'string' || !VALID_CYCLES.has(obj['cycle'] as CycleType)) {
throw new Error(`${filePath}: cycle '${String(obj['cycle'])}' is not a valid cycle`);
}
if (typeof obj['date'] !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(obj['date'])) {
throw new Error(`${filePath}: date '${String(obj['date'])}' must match YYYY-MM-DD`);
}
if (typeof obj['subfolder'] !== 'string' || obj['subfolder'].length === 0) {
throw new Error(`${filePath}: subfolder must be a non-empty string`);
}
if (obj['subfolder'] !== obj['cycle']) {
throw new Error(
`${filePath}: subfolder '${String(obj['subfolder'])}' must equal cycle '${String(obj['cycle'])}'`,
);
}
if (typeof obj['generated_at'] !== 'string' || Number.isNaN(Date.parse(obj['generated_at']))) {
throw new Error(`${filePath}: generated_at '${String(obj['generated_at'])}' must be a valid date-time string`);
}
if (
obj['inherited_from'] !== undefined &&
obj['inherited_from'] !== null &&
typeof obj['inherited_from'] !== 'string'
) {
throw new Error(`${filePath}: inherited_from must be a string or null when present`);
}
if (!Array.isArray(obj['pirs'])) {
throw new Error(`${filePath}: 'pirs' must be an array`);
}
for (let i = 0; i < (obj['pirs'] as unknown[]).length; i++) {
const p = (obj['pirs'] as unknown[])[i] as Record<string, unknown>;
if (typeof p !== 'object' || p === null) {
throw new Error(`${filePath}: pirs[${i}] is not an object`);
}
if (typeof p['pir_id'] !== 'string' || !PIR_ID_PATTERN.test(p['pir_id'])) {
throw new Error(
`${filePath}: pirs[${i}].pir_id '${String(p['pir_id'])}' does not match ${PIR_ID_PATTERN}`,
);
}
if (typeof p['statement'] !== 'string' || p['statement'].length < 10) {
throw new Error(
`${filePath}: pirs[${i}] (${String(p['pir_id'])}).statement missing or shorter than 10 chars`,
);
}
if (!VALID_STATUSES.has(p['status'] as PirStatus)) {
throw new Error(
`${filePath}: pirs[${i}] (${String(p['pir_id'])}).status '${String(p['status'])}' is not a valid PIR status`,
);
}
if (!VALID_CONFIDENCES.has(p['confidence'] as Confidence)) {
throw new Error(
`${filePath}: pirs[${i}] (${String(p['pir_id'])}).confidence '${String(p['confidence'])}' is not a valid confidence value`,
);
}
if (p['status'] === 'answered') {
if (typeof p['answer_summary'] !== 'string' || p['answer_summary'].length === 0) {
throw new Error(
`${filePath}: pirs[${i}] (${String(p['pir_id'])}) status='answered' requires non-empty answer_summary`,
);
}
} else if (p['answer_summary'] !== undefined) {
throw new Error(
`${filePath}: pirs[${i}] (${String(p['pir_id'])}) status='${String(p['status'])}' must not carry answer_summary`,
);
}
}
return obj as unknown as PirStatusFile;
}
|