{{breadcrumbs}}
HUB / lib_bejson_validator.js
lib_bejson_validator.js
- Runtime
- JavaScript
- Category
- JavaScript
- Path
- /storage/emulated/0/Projects/Management/Libraries/js/lib_bejson_validator.js
FILE // lib_bejson_validator.js
/*
Library: lib_bejson_validator.js
MFDB Version: 1.3.1
Format_Creator: Elton Boehnen
Status: OFFICIAL - v1.3.1
Date: 2026-05-06
*/
/**
* Library: lib_bejson_validator.js
* Jurisdiction: ["JAVASCRIPT", "CORE_COMMAND"]
* Status: OFFICIAL — Core-Command/Lib (v1.1)
* Author: Elton Boehnen
* Version: 1.3 OFFICIAL
* Date: 2026-05-01
* Description: BEJSON validator — schema validation for 104, 104a, 104db.
MFDB validation functions are in lib_mfdb_validator.js (decoupled).
Author: Elton Boehnen
Version: 1.3 OFFICIAL
Date: 2026-05-01
* Changelog v3.1.0:
[FIX] MFDB decoupled: validation engine moved to lib_mfdb_validator.js.
Unidirectional dependency: MFDB → validator (not vice versa).
* Error code ranges:
1–15 → lib_bejson_validator (BEJSONValidationError)
*/
'use strict';
// ---------------------------------------------------------------------------
// Error codes (mirror bash readonly values / Python constants)
// ---------------------------------------------------------------------------
const E_INVALID_JSON = 1;
const E_MISSING_MANDATORY_KEY = 2;
const E_INVALID_FORMAT = 3;
const E_INVALID_VERSION = 4;
const E_INVALID_RECORDS_TYPE = 5;
const E_INVALID_FIELDS = 6;
const E_INVALID_VALUES = 7;
const E_TYPE_MISMATCH = 8;
const E_RECORD_LENGTH_MISMATCH = 9;
const E_RESERVED_KEY_COLLISION = 10;
const E_INVALID_RECORD_TYPE_PARENT = 11;
const E_NULL_VIOLATION = 12;
const E_FILE_NOT_FOUND = 13;
const E_PERMISSION_DENIED = 14;
const E_ATOMIC_WRITE_FAILED = 15;
const VALID_VERSIONS = new Set(['104', '104a', '104db']);
const MANDATORY_KEYS = ['Format', 'Format_Version', 'Format_Creator', 'Records_Type', 'Fields', 'Values'];
const VALID_FIELD_TYPES = new Set(['string', 'integer', 'number', 'boolean', 'array', 'object']);
// ---------------------------------------------------------------------------
// Validation exception
// ---------------------------------------------------------------------------
class BEJSONValidationError extends Error {
constructor(message, code) {
super(message);
this.name = 'BEJSONValidationError';
this.code = code;
}
}
// ---------------------------------------------------------------------------
// Validation state — mirrors bash globals / Python ValidationState
// ---------------------------------------------------------------------------
class ValidationState {
constructor() {
this.errors = [];
this.warnings = [];
this.currentFile = '';
}
reset() {
this.errors = [];
this.warnings = [];
this.currentFile = '';
}
addError(message, location = '', context = '') {
let entry = 'ERROR';
if (location) entry += ` | Location: ${location}`;
entry += ` | Message: ${message}`;
if (context) entry += ` | Context: ${context}`;
this.errors.push(entry);
}
addWarning(message, location = '') {
let entry = 'WARNING';
if (location) entry += ` | Location: ${location}`;
entry += ` | Message: ${message}`;
this.warnings.push(entry);
}
getErrors() { return [...this.errors]; }
getWarnings() { return [...this.warnings]; }
hasErrors() { return this.errors.length > 0; }
hasWarnings() { return this.warnings.length > 0; }
errorCount() { return this.errors.length; }
warningCount() { return this.warnings.length; }
}
// Module-level default state (mirrors bash global arrays / Python _state)
const _state = new ValidationState();
// ---------------------------------------------------------------------------
// Convenience accessors that operate on the module-level state
// (mirror the exported bash / Python functions)
// ---------------------------------------------------------------------------
function bejson_validator_reset_state() {
_state.reset();
}
function bejson_validator_get_errors() {
return _state.getErrors();
}
function bejson_validator_get_warnings() {
return _state.getWarnings();
}
function bejson_validator_has_errors() {
return _state.hasErrors();
}
function bejson_validator_has_warnings() {
return _state.hasWarnings();
}
function bejson_validator_error_count() {
return _state.errorCount();
}
function bejson_validator_warning_count() {
return _state.warningCount();
}
// ---------------------------------------------------------------------------
// Dependency check (no-op in JS — JSON is built-in)
// mirrors bejson_validator_check_dependencies
// ---------------------------------------------------------------------------
function bejson_validator_check_dependencies() {
return true;
}
// ---------------------------------------------------------------------------
// JSON syntax validation
// mirrors bejson_validator_check_json_syntax
// ---------------------------------------------------------------------------
function bejson_validator_check_json_syntax(input, isFile = false) {
let text;
if (isFile) {
// Node.js file-system path
let fs;
try { fs = require('fs'); } catch (_) {
throw new BEJSONValidationError('File I/O not available in this environment', E_FILE_NOT_FOUND);
}
if (!fs.existsSync(input)) {
_state.addError(`File not found: ${input}`, 'File System');
throw new BEJSONValidationError(`File not found: ${input}`, E_FILE_NOT_FOUND);
}
try {
text = fs.readFileSync(input, 'utf8');
_state.currentFile = input;
} catch (err) {
if (err.code === 'EACCES') {
_state.addError(`Permission denied: ${input}`, 'File System');
throw new BEJSONValidationError(`Permission denied: ${input}`, E_PERMISSION_DENIED);
}
throw new BEJSONValidationError(`Cannot read file: ${err.message}`, E_FILE_NOT_FOUND);
}
} else {
text = input;
}
if (typeof text === 'object' && text !== null) {
return text; // already parsed
}
try {
return JSON.parse(text);
} catch (err) {
_state.addError(`Invalid JSON syntax: ${err.message}`, 'JSON Parse');
throw new BEJSONValidationError(`Invalid JSON syntax: ${err.message}`, E_INVALID_JSON);
}
}
// ---------------------------------------------------------------------------
// Mandatory key validation
// mirrors bejson_validator_check_mandatory_keys
// ---------------------------------------------------------------------------
function bejson_validator_check_mandatory_keys(doc) {
for (const key of MANDATORY_KEYS) {
if (!(key in doc)) {
_state.addError(`Missing mandatory top-level key: '${key}'`, 'Top-Level Keys');
throw new BEJSONValidationError(`Missing mandatory key: ${key}`, E_MISSING_MANDATORY_KEY);
}
}
if (doc['Format'] !== 'BEJSON') {
_state.addError(
`Invalid 'Format' value: Expected 'BEJSON', got '${doc['Format']}'`,
'Top-Level Keys/Format',
);
throw new BEJSONValidationError('Invalid Format', E_INVALID_FORMAT);
}
const version = doc['Format_Version'] ?? '';
if (!VALID_VERSIONS.has(version)) {
_state.addError(
`Invalid 'Format_Version': Expected '104', '104a', or '104db', got '${version}'`,
'Top-Level Keys/Format_Version',
);
throw new BEJSONValidationError(`Invalid version: ${version}`, E_INVALID_VERSION);
}
if (typeof doc['Format_Creator'] !== 'string') {
_state.addError("Invalid 'Format_Creator': Must be a string", 'Top-Level Keys/Format_Creator');
throw new BEJSONValidationError('Invalid Format_Creator', E_INVALID_FORMAT);
}
const checks = [
['Records_Type', E_INVALID_RECORDS_TYPE, 'Top-Level Keys/Records_Type'],
['Fields', E_INVALID_FIELDS, 'Top-Level Keys/Fields'],
['Values', E_INVALID_VALUES, 'Top-Level Keys/Values'],
];
for (const [key, code, section] of checks) {
if (!Array.isArray(doc[key])) {
_state.addError(`Invalid '${key}': Must be an array`, section);
throw new BEJSONValidationError(`Invalid ${key}`, code);
}
}
return version;
}
// ---------------------------------------------------------------------------
// Records_Type validation
// mirrors bejson_validator_check_records_type
// ---------------------------------------------------------------------------
function bejson_validator_check_records_type(doc, version) {
const rt = doc['Records_Type'];
const count = rt.length;
if (version === '104' || version === '104a') {
if (count !== 1 || typeof rt[0] !== 'string') {
_state.addError(
`For BEJSON ${version}, 'Records_Type' must contain exactly one string. Found ${count} entries.`,
'Records_Type',
);
throw new BEJSONValidationError('Bad Records_Type', E_INVALID_RECORDS_TYPE);
}
} else if (version === '104db') {
if (count < 2) {
_state.addError(
`For BEJSON 104db, 'Records_Type' must contain two or more unique strings. Found ${count} entries.`,
'Records_Type',
);
throw new BEJSONValidationError('Bad Records_Type', E_INVALID_RECORDS_TYPE);
}
const seen = new Set();
for (let i = 0; i < rt.length; i++) {
if (typeof rt[i] !== 'string') {
_state.addError(`Records_Type[${i}] must be a string`, `Records_Type[${i}]`);
throw new BEJSONValidationError('Bad Records_Type entry', E_INVALID_RECORDS_TYPE);
}
if (seen.has(rt[i])) {
_state.addError(`Duplicate type '${rt[i]}' found in 'Records_Type'`, 'Records_Type');
throw new BEJSONValidationError(`Duplicate Records_Type: ${rt[i]}`, E_INVALID_RECORDS_TYPE);
}
seen.add(rt[i]);
}
}
}
// ---------------------------------------------------------------------------
// Fields structure validation
// mirrors bejson_validator_check_fields_structure
// ---------------------------------------------------------------------------
function bejson_validator_check_fields_structure(doc, version) {
const fields = doc['Fields'];
if (!fields || fields.length === 0) {
_state.addError("'Fields' array cannot be empty", 'Fields Array');
throw new BEJSONValidationError('Empty Fields', E_INVALID_FIELDS);
}
const seenNames = new Set();
for (let i = 0; i < fields.length; i++) {
const fieldDef = fields[i];
if (typeof fieldDef !== 'object' || fieldDef === null || Array.isArray(fieldDef)) {
_state.addError(`Field at index ${i} must be an object`, `Fields[${i}]`);
throw new BEJSONValidationError(`Field ${i} not an object`, E_INVALID_FIELDS);
}
const name = fieldDef['name'];
if (typeof name !== 'string') {
_state.addError(
`Field at index ${i}: Missing or invalid 'name' (must be string)`,
`Fields[${i}]`,
);
throw new BEJSONValidationError(`Field ${i} bad name`, E_INVALID_FIELDS);
}
if (seenNames.has(name)) {
_state.addError(`Duplicate field name '${name}' found in 'Fields' array`, `Fields[${i}]`);
throw new BEJSONValidationError(`Duplicate field: ${name}`, E_INVALID_FIELDS);
}
seenNames.add(name);
const ftype = fieldDef['type'];
if (typeof ftype !== 'string' || !VALID_FIELD_TYPES.has(ftype)) {
_state.addError(
`Field '${name}' (index ${i}): Invalid type '${ftype}'. Valid: ${[...VALID_FIELD_TYPES].join(', ')}`,
`Fields[${i}]`,
);
throw new BEJSONValidationError(`Field ${name} invalid type`, E_INVALID_FIELDS);
}
if (version === '104a' && (ftype === 'array' || ftype === 'object')) {
_state.addError(
`Field '${name}' (index ${i}): Type '${ftype}' not allowed in 104a.`,
`Fields[${i}]`,
);
throw new BEJSONValidationError(`Field ${name} disallowed type for 104a`, E_INVALID_FIELDS);
}
}
return fields.length;
}
// ---------------------------------------------------------------------------
// 104db Record_Type_Parent validation
// mirrors bejson_validator_check_record_type_parent
// ---------------------------------------------------------------------------
function bejson_validator_check_record_type_parent(doc) {
const fields = doc['Fields'];
const first = fields[0] ?? {};
if (!fields.length || first['name'] !== 'Record_Type_Parent' || first['type'] !== 'string') {
_state.addError(
`For BEJSON 104db, the first field must be {"name": "Record_Type_Parent", "type": "string"}. ` +
`Found: ${JSON.stringify(first)}`,
'Fields[0]',
);
throw new BEJSONValidationError('Bad Record_Type_Parent field', E_INVALID_RECORD_TYPE_PARENT);
}
const validTypes = new Set(doc['Records_Type']);
for (let i = 0; i < doc['Values'].length; i++) {
const record = doc['Values'][i];
if (!Array.isArray(record)) {
_state.addError(`Values[${i}] must be an array (record)`, `Values[${i}]`);
throw new BEJSONValidationError(`Bad record at ${i}`, E_INVALID_VALUES);
}
const rtp = record[0] ?? null;
if (!rtp) {
_state.addError(
`Record at 'Values' index ${i}: 'Record_Type_Parent' is missing or null`,
`Values[${i}][0]`,
);
throw new BEJSONValidationError(`Missing RTP at ${i}`, E_INVALID_RECORD_TYPE_PARENT);
}
if (!validTypes.has(rtp)) {
_state.addError(
`Record at 'Values' index ${i}: 'Record_Type_Parent' value '${rtp}' ` +
`does not match any declared type in 'Records_Type'`,
`Values[${i}][0]`,
);
throw new BEJSONValidationError(`Invalid RTP '${rtp}' at ${i}`, E_INVALID_RECORD_TYPE_PARENT);
}
}
}
// ---------------------------------------------------------------------------
// Values validation
// mirrors bejson_validator_check_values
// ---------------------------------------------------------------------------
function _jsonType(value) {
if (value === null) return 'null';
if (typeof value === 'boolean') return 'boolean';
if (Number.isInteger(value)) return 'integer';
if (typeof value === 'number') return 'number';
if (typeof value === 'string') return 'string';
if (Array.isArray(value)) return 'array';
if (typeof value === 'object') return 'object';
return 'unknown';
}
function bejson_validator_check_values(doc, version, fieldsCount) {
const values = doc['Values'];
const fields = doc['Fields'];
for (let i = 0; i < values.length; i++) {
const record = values[i];
if (!Array.isArray(record)) {
_state.addError(`Values[${i}] must be an array (record)`, `Values[${i}]`);
throw new BEJSONValidationError(`Bad record at ${i}`, E_INVALID_VALUES);
}
if (record.length !== fieldsCount) {
_state.addError(
`Record at 'Values' index ${i} has ${record.length} elements, ` +
`but 'Fields' defines ${fieldsCount} fields.`,
`Values[${i}]`,
);
throw new BEJSONValidationError(`Length mismatch at ${i}`, E_RECORD_LENGTH_MISMATCH);
}
const recordType = (version === '104db' && record.length > 0) ? record[0] : null;
for (let j = 0; j < record.length; j++) {
const value = record[j];
const fieldDef = fields[j];
const fieldName = fieldDef['name'];
const fieldType = fieldDef['type'];
const fieldParent = fieldDef['Record_Type_Parent'] ?? '';
// 104db applicability: field not for this record type → must be null
if (version === '104db' && fieldParent && j > 0) {
if (fieldParent !== recordType) {
if (value !== null) {
_state.addError(
`Record at 'Values' index ${i} (type '${recordType}'), ` +
`field '${fieldName}' (index ${j}): not applicable to this type; must be null.`,
`Values[${i}][${j}]`,
);
throw new BEJSONValidationError('Null violation', E_NULL_VIOLATION);
}
continue;
}
}
if (value === null) continue;
const vtype = _jsonType(value);
let typeValid = false;
switch (fieldType) {
case 'string':
typeValid = typeof value === 'string';
break;
case 'integer':
typeValid = Number.isInteger(value) && typeof value !== 'boolean';
break;
case 'number':
typeValid = typeof value === 'number' && typeof value !== 'boolean';
break;
case 'boolean':
typeValid = typeof value === 'boolean';
break;
case 'array':
typeValid = Array.isArray(value);
break;
case 'object':
typeValid = typeof value === 'object' && !Array.isArray(value);
break;
}
if (!typeValid) {
_state.addError(
`Record at 'Values' index ${i}, field '${fieldName}' (index ${j}): ` +
`Value '${value}' is of type '${vtype}', but 'Fields' defines type '${fieldType}'.`,
`Values[${i}][${j}]`,
);
throw new BEJSONValidationError(`Type mismatch at [${i}][${j}]`, E_TYPE_MISMATCH);
}
}
}
}
// ---------------------------------------------------------------------------
// Custom headers validation (104a)
// mirrors bejson_validator_check_custom_headers
// ---------------------------------------------------------------------------
const _PASCAL_CASE = /^[A-Z][a-zA-Z0-9_]*$/;
function bejson_validator_check_custom_headers(doc, version) {
const mandatorySet = new Set(MANDATORY_KEYS);
for (const key of Object.keys(doc)) {
if (mandatorySet.has(key) || key === 'Parent_Hierarchy') continue;
if (version === '104' || version === '104db') {
_state.addError(
`For BEJSON ${version}, custom top-level key '${key}' is not permitted.`,
`Top-Level Keys/${key}`,
);
throw new BEJSONValidationError(`Unexpected key: ${key}`, E_RESERVED_KEY_COLLISION);
}
// 104a: warn on non-PascalCase
if (!_PASCAL_CASE.test(key)) {
_state.addWarning(
`Custom top-level key '${key}' does not follow recommended PascalCase naming convention.`,
`Top-Level Keys/${key}`,
);
}
}
}
// ---------------------------------------------------------------------------
// Main validation entry points
// mirrors bejson_validator_validate_string / bejson_validator_validate_file
// ---------------------------------------------------------------------------
function bejson_validator_validate_string(jsonString) {
bejson_validator_reset_state();
const doc = bejson_validator_check_json_syntax(jsonString, false);
const version = bejson_validator_check_mandatory_keys(doc);
bejson_validator_check_custom_headers(doc, version);
bejson_validator_check_records_type(doc, version);
const fieldsCount = bejson_validator_check_fields_structure(doc, version);
if (version === '104db') bejson_validator_check_record_type_parent(doc);
bejson_validator_check_values(doc, version, fieldsCount);
return true;
}
function bejson_validator_validate_file(filePath) {
bejson_validator_reset_state();
let fs;
try { fs = require('fs'); } catch (_) {
throw new BEJSONValidationError('File I/O not available in this environment', E_FILE_NOT_FOUND);
}
const text = fs.readFileSync(filePath, 'utf8');
return bejson_validator_validate_string(text);
}
// ---------------------------------------------------------------------------
// Validation report
// mirrors bejson_validator_get_report
// ---------------------------------------------------------------------------
function bejson_validator_get_report(input, isFile = false) {
let valid = false;
try {
if (isFile) {
valid = bejson_validator_validate_file(input);
} else {
valid = bejson_validator_validate_string(input);
}
} catch (_) {
// errors captured in _state
}
const lines = [
'=== BEJSON Validation Report ===',
`Status: ${valid ? 'VALID' : 'INVALID'}`,
'',
`Errors: ${bejson_validator_error_count()}`,
];
if (bejson_validator_has_errors()) {
lines.push('---');
lines.push(...bejson_validator_get_errors());
}
lines.push('', `Warnings: ${bejson_validator_warning_count()}`);
if (bejson_validator_has_warnings()) {
lines.push('---');
lines.push(...bejson_validator_get_warnings());
}
return lines.join('\n');
}
// ===========================================================================
// MFDB VALIDATOR — appended from lib_mfdb_validator.js v1.0.0
// ===========================================================================
// ---------------------------------------------------------------------------
// Error codes (30–49)
// ---------------------------------------------------------------------------
const E_MFDB_NOT_MANIFEST = 30;
const E_MFDB_NOT_ENTITY_FILE = 31;
const E_MFDB_MANIFEST_RECORDS_TYPE = 32;
const E_MFDB_ENTITY_NOT_FOUND = 33;
const E_MFDB_ENTITY_NAME_MISMATCH = 34;
const E_MFDB_DUPLICATE_ENTRY = 35;
const E_MFDB_NO_PARENT_HIERARCHY = 36;
const E_MFDB_MANIFEST_NOT_FOUND = 37;
const E_MFDB_BIDIRECTIONAL_FAIL = 38;
const E_MFDB_FK_UNRESOLVED = 39;
const E_MFDB_MISSING_REQUIRED_FIELD = 40;
const E_MFDB_NULL_REQUIRED = 41;
class MFDBValidationError extends Error {
constructor(message, code) {
super(message);
this.name = 'MFDBValidationError';
this.code = code;
}
}
// ---------------------------------------------------------------------------
// Validation state
// ---------------------------------------------------------------------------
let _mErrors = [];
let _mWarnings = [];
function _mReset() { _mErrors = []; _mWarnings = []; }
function _mAddError(msg, loc) { _mErrors.push( 'ERROR' + (loc ? ` | Location: ${loc}` : '') + ` | Message: ${msg}`); }
function _mAddWarning(msg, loc) { _mWarnings.push( 'WARNING' + (loc ? ` | Location: ${loc}` : '') + ` | Message: ${msg}`); }
function _mHasErrors() { return _mErrors.length > 0; }
function _mHasWarnings() { return _mWarnings.length > 0; }
// ---------------------------------------------------------------------------
// Internal helpers (also exported for lib_mfdb_core.js)
// ---------------------------------------------------------------------------
function _loadJson(filePath) {
const fs = require('fs');
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function _rowsAsDicts(doc) {
const names = doc.Fields.map(f => f.name);
return doc.Values.map(row => Object.fromEntries(names.map((n, i) => [n, row[i]])));
}
function _resolveEntityPath(manifestPath, filePathRel) {
const path = require('path');
return path.resolve(path.dirname(manifestPath), filePathRel);
}
function _fileExists(filePath) {
const fs = require('fs');
return fs.existsSync(filePath);
}
// ---------------------------------------------------------------------------
// Manifest Validation (Spec §8.1)
// ---------------------------------------------------------------------------
function mfdb_validator_validate_manifest(manifestPath) {
_mReset();
if (!_fileExists(manifestPath)) {
_mAddError(`Manifest file not found: ${manifestPath}`, 'File System');
throw new MFDBValidationError(`File not found: ${manifestPath}`, E_MFDB_MANIFEST_NOT_FOUND);
}
try {
bejson_validator_validate_file(manifestPath);
} catch (exc) {
_mAddError(`BEJSON 104a validation failed: ${exc.message}`, 'BEJSON Validation');
throw new MFDBValidationError(exc.message, E_MFDB_NOT_MANIFEST);
}
const doc = _loadJson(manifestPath);
if (doc.Format_Version !== '104a') {
_mAddError("Manifest must be Format_Version '104a'", 'Format_Version');
throw new MFDBValidationError('Manifest must be 104a', E_MFDB_NOT_MANIFEST);
}
const rt = doc.Records_Type || [];
if (!(rt.length === 1 && rt[0] === 'mfdb')) {
_mAddError(`Records_Type must be ["mfdb"]. Found: ${JSON.stringify(rt)}`, 'Records_Type');
throw new MFDBValidationError('Bad manifest Records_Type', E_MFDB_MANIFEST_RECORDS_TYPE);
}
const fieldNames = (doc.Fields || []).map(f => f.name);
for (const required of ['entity_name', 'file_path']) {
if (!fieldNames.includes(required)) {
_mAddError(`Manifest Fields must include '${required}'`, 'Fields');
throw new MFDBValidationError(`Missing required field '${required}'`, E_MFDB_MISSING_REQUIRED_FIELD);
}
}
const entries = _rowsAsDicts(doc);
const seenNames = new Set();
const seenPaths = new Set();
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const entityName = entry.entity_name;
const filePath = entry.file_path;
if (!entityName) {
_mAddError(`Record ${i}: entity_name is null or missing`, `Values[${i}]`);
throw new MFDBValidationError('Null entity_name', E_MFDB_NULL_REQUIRED);
}
if (!filePath) {
_mAddError(`Record ${i}: file_path is null or missing`, `Values[${i}]`);
throw new MFDBValidationError('Null file_path', E_MFDB_NULL_REQUIRED);
}
if (seenNames.has(entityName)) {
_mAddError(`Duplicate entity_name: '${entityName}'`, `Values[${i}]`);
throw new MFDBValidationError(`Duplicate entity_name: ${entityName}`, E_MFDB_DUPLICATE_ENTRY);
}
seenNames.add(entityName);
if (seenPaths.has(filePath)) {
_mAddError(`Duplicate file_path: '${filePath}'`, `Values[${i}]`);
throw new MFDBValidationError(`Duplicate file_path: ${filePath}`, E_MFDB_DUPLICATE_ENTRY);
}
seenPaths.add(filePath);
const resolved = _resolveEntityPath(manifestPath, filePath);
if (!_fileExists(resolved)) {
_mAddError(`Entity file '${filePath}' not found (resolved: ${resolved})`, `Values[${i}]/file_path`);
throw new MFDBValidationError(`Entity file not found: ${resolved}`, E_MFDB_ENTITY_NOT_FOUND);
}
}
return true;
}
// ---------------------------------------------------------------------------
// Entity File Validation (Spec §8.2)
// ---------------------------------------------------------------------------
function mfdb_validator_validate_entity_file(entityPath, checkBidirectional = true) {
const path = require('path');
_mReset();
if (!_fileExists(entityPath)) {
_mAddError(`Entity file not found: ${entityPath}`, 'File System');
throw new MFDBValidationError(`File not found: ${entityPath}`, E_MFDB_ENTITY_NOT_FOUND);
}
try {
bejson_validator_validate_file(entityPath);
} catch (exc) {
_mAddError(`BEJSON 104 validation failed: ${exc.message}`, 'BEJSON Validation');
throw new MFDBValidationError(exc.message, E_MFDB_NOT_ENTITY_FILE);
}
const doc = _loadJson(entityPath);
if (doc.Format_Version !== '104') {
_mAddError("Entity file must be Format_Version '104'", 'Format_Version');
throw new MFDBValidationError('Entity file must be 104', E_MFDB_NOT_ENTITY_FILE);
}
const parentHierarchy = doc.Parent_Hierarchy;
if (!parentHierarchy) {
_mAddError('Entity file must contain Parent_Hierarchy pointing to the manifest', 'Parent_Hierarchy');
throw new MFDBValidationError('Missing Parent_Hierarchy', E_MFDB_NO_PARENT_HIERARCHY);
}
const entityDir = path.dirname(path.resolve(entityPath));
const manifestPath = path.resolve(entityDir, parentHierarchy);
if (!_fileExists(manifestPath)) {
_mAddError(
`Parent_Hierarchy '${parentHierarchy}' resolves to '${manifestPath}' which does not exist`,
'Parent_Hierarchy',
);
throw new MFDBValidationError(`Manifest not found: ${manifestPath}`, E_MFDB_MANIFEST_NOT_FOUND);
}
if (!path.basename(manifestPath).endsWith('.mfdb.bejson')) {
_mAddWarning(
`Parent_Hierarchy target '${manifestPath}' does not end in '.mfdb.bejson'. Expected: 104a.mfdb.bejson`,
'Parent_Hierarchy',
);
}
const rt = doc.Records_Type || [];
if (rt.length !== 1) {
_mAddError(`Entity file Records_Type must have exactly one entry. Found: ${JSON.stringify(rt)}`, 'Records_Type');
throw new MFDBValidationError('Entity Records_Type must be single-entry', E_MFDB_NOT_ENTITY_FILE);
}
const entityName = rt[0];
let manifestDoc, entries, manifestEntityNames;
try {
manifestDoc = _loadJson(manifestPath);
entries = _rowsAsDicts(manifestDoc);
manifestEntityNames = entries.map(e => e.entity_name);
} catch (exc) {
_mAddError(`Could not read manifest: ${exc.message}`, 'Manifest');
throw new MFDBValidationError(`Cannot read manifest: ${exc.message}`, E_MFDB_MANIFEST_NOT_FOUND);
}
if (!manifestEntityNames.includes(entityName)) {
_mAddError(
`Records_Type '${entityName}' does not appear as entity_name in the manifest`,
'Records_Type vs Manifest',
);
throw new MFDBValidationError(
`Entity '${entityName}' not registered in manifest`, E_MFDB_ENTITY_NAME_MISMATCH,
);
}
if (checkBidirectional) {
const match = entries.find(e => e.entity_name === entityName);
if (match) {
const manifestDir = path.dirname(path.resolve(manifestPath));
const fromManifest = path.resolve(manifestDir, match.file_path || '');
const thisFile = path.resolve(entityPath);
if (fromManifest !== thisFile) {
_mAddError(
`Bidirectional check failed for entity '${entityName}': ` +
`manifest points to '${fromManifest}', but this file is '${thisFile}'`,
'Bidirectional Path Check',
);
throw new MFDBValidationError('Bidirectional path check failed', E_MFDB_BIDIRECTIONAL_FAIL);
}
}
}
return true;
}
// ---------------------------------------------------------------------------
// Database-Level Validation (Spec §8.3)
// ---------------------------------------------------------------------------
function mfdb_validator_validate_database(manifestPath, strictFk = false) {
_mReset();
try {
mfdb_validator_validate_manifest(manifestPath);
} catch (exc) {
throw exc;
}
const manifestDoc = _loadJson(manifestPath);
const entries = _rowsAsDicts(manifestDoc);
const pkMap = {};
for (const e of entries) {
if (e.primary_key) pkMap[e.entity_name] = e.primary_key;
}
for (const entry of entries) {
const entityName = entry.entity_name;
const filePathRel = entry.file_path;
const declaredCount = entry.record_count;
const resolved = _resolveEntityPath(manifestPath, filePathRel);
try {
mfdb_validator_validate_entity_file(resolved, true);
} catch (exc) {
_mAddError(`Entity '${entityName}' failed validation: ${exc.message}`, `Entity/${entityName}`);
throw exc;
}
if (declaredCount !== null && declaredCount !== undefined) {
const edoc = _loadJson(resolved);
const actualCount = (edoc.Values || []).length;
if (actualCount !== declaredCount) {
_mAddWarning(
`Entity '${entityName}': manifest declares record_count=${declaredCount}, ` +
`actual=${actualCount}. Call mfdb_core_sync_all_counts() to correct.`,
`Entity/${entityName}/record_count`,
);
}
}
if (strictFk) {
const edoc = _loadJson(resolved);
const fkFields = (edoc.Fields || []).filter(f => f.name.endsWith('_fk')).map(f => f.name);
for (const fkField of fkFields) {
const targetFound = Object.entries(pkMap).some(
([en, pk]) => pk && (fkField.includes(pk) || fkField.toLowerCase().includes(en.toLowerCase()))
);
if (!targetFound) {
_mAddWarning(
`Entity '${entityName}': FK field '${fkField}' has no matching primary_key ` +
`declaration in the manifest. Consider adding a Relationships header (MFDB v1.1).`,
`Entity/${entityName}/${fkField}`,
);
}
}
}
}
return true;
}
// ---------------------------------------------------------------------------
// Validation report
// ---------------------------------------------------------------------------
function mfdb_validator_get_report(manifestPath, strictFk = false) {
let valid = false;
try {
valid = mfdb_validator_validate_database(manifestPath, strictFk);
} catch (_) {}
const lines = [
'=== MFDB Validation Report ===',
`Manifest : ${manifestPath}`,
`Status : ${valid ? 'VALID' : 'INVALID'}`,
'',
`Errors : ${_mErrors.length}`,
];
if (_mHasErrors()) {
lines.push('---');
lines.push(..._mErrors);
}
lines.push('', `Warnings : ${_mWarnings.length}`);
if (_mHasWarnings()) {
lines.push('---');
lines.push(..._mWarnings);
}
return lines.join('\n');
}
// ---------------------------------------------------------------------------
// State accessors
// ---------------------------------------------------------------------------
function mfdb_validator_reset_state() { _mReset(); }
function mfdb_validator_has_errors() { return _mHasErrors(); }
function mfdb_validator_has_warnings() { return _mHasWarnings(); }
function mfdb_validator_get_errors() { return [..._mErrors]; }
function mfdb_validator_get_warnings() { return [..._mWarnings]; }
function mfdb_validator_error_count() { return _mErrors.length; }
function mfdb_validator_warning_count() { return _mWarnings.length; }
// ---------------------------------------------------------------------------
// Exports (CommonJS + browser global)
// ---------------------------------------------------------------------------
const exports_ = {
// ── BEJSON VALIDATOR ──
// Error codes
E_INVALID_JSON,
E_MISSING_MANDATORY_KEY,
E_INVALID_FORMAT,
E_INVALID_VERSION,
E_INVALID_RECORDS_TYPE,
E_INVALID_FIELDS,
E_INVALID_VALUES,
E_TYPE_MISMATCH,
E_RECORD_LENGTH_MISMATCH,
E_RESERVED_KEY_COLLISION,
E_INVALID_RECORD_TYPE_PARENT,
E_NULL_VIOLATION,
E_FILE_NOT_FOUND,
E_PERMISSION_DENIED,
E_ATOMIC_WRITE_FAILED,
// Constants
VALID_VERSIONS,
MANDATORY_KEYS,
VALID_FIELD_TYPES,
// Classes
BEJSONValidationError,
ValidationState,
// State accessors
bejson_validator_reset_state,
bejson_validator_get_errors,
bejson_validator_get_warnings,
bejson_validator_has_errors,
bejson_validator_has_warnings,
bejson_validator_error_count,
bejson_validator_warning_count,
// Validation steps
bejson_validator_check_dependencies,
bejson_validator_check_json_syntax,
bejson_validator_check_mandatory_keys,
bejson_validator_check_records_type,
bejson_validator_check_fields_structure,
bejson_validator_check_record_type_parent,
bejson_validator_check_values,
bejson_validator_check_custom_headers,
// Entry points
bejson_validator_validate_string,
bejson_validator_validate_file,
bejson_validator_get_report,
// ── MFDB VALIDATOR ──
// Error codes
E_MFDB_NOT_MANIFEST,
E_MFDB_NOT_ENTITY_FILE,
E_MFDB_MANIFEST_RECORDS_TYPE,
E_MFDB_ENTITY_NOT_FOUND,
E_MFDB_ENTITY_NAME_MISMATCH,
E_MFDB_DUPLICATE_ENTRY,
E_MFDB_NO_PARENT_HIERARCHY,
E_MFDB_MANIFEST_NOT_FOUND,
E_MFDB_BIDIRECTIONAL_FAIL,
E_MFDB_FK_UNRESOLVED,
E_MFDB_MISSING_REQUIRED_FIELD,
E_MFDB_NULL_REQUIRED,
// Class
MFDBValidationError,
// Internal helpers (needed by lib_bejson_core.js MFDB section)
_loadJson,
_rowsAsDicts,
_resolveEntityPath,
_fileExists,
// Validation functions
mfdb_validator_validate_manifest,
mfdb_validator_validate_entity_file,
mfdb_validator_validate_database,
mfdb_validator_get_report,
// State accessors
mfdb_validator_reset_state,
mfdb_validator_has_errors,
mfdb_validator_has_warnings,
mfdb_validator_get_errors,
mfdb_validator_get_warnings,
mfdb_validator_error_count,
mfdb_validator_warning_count,
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = exports_;
}
if (typeof window !== 'undefined') {
window.BEJSON_VALIDATOR = exports_;
}