267 lines
8.5 KiB
JavaScript
267 lines
8.5 KiB
JavaScript
/**
|
||
* ━━━ HLDP 格式校验器 ━━━
|
||
* TCS 通感语言核系统编程语言 · 第一个落地协议层
|
||
* HLDP = TCS 在 Notion ↔ GitHub 通道上的落地实现
|
||
* 版权:国作登字-2026-A-00037559 · 冰朔(ICE-GL∞)
|
||
* 指令来源:SY-CMD-BRG-005
|
||
*
|
||
* 校验项:
|
||
* 1. JSON 结构是否符合 schema
|
||
* 2. 所有必填字段是否存在
|
||
* 3. relations 引用的 target_id 是否在 data/ 中存在
|
||
* 4. 编号格式是否合规
|
||
* 5. 日期格式是否为 ISO-8601
|
||
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const ROOT = path.resolve(__dirname, '../..');
|
||
const VALID_DATA_TYPES = ['persona', 'registry', 'instruction', 'broadcast', 'id_system', 'snapshot'];
|
||
const VALID_RELATION_TYPES = ['parent', 'child', 'sibling', 'reference', 'owner'];
|
||
|
||
// ID format patterns
|
||
const ID_PATTERNS = {
|
||
dev: /^DEV-\d{3}$/,
|
||
persona: /^PER-[A-Z0-9]+$/,
|
||
instruction: /^SY-CMD-[A-Z]+-\d+$/,
|
||
agent: /^AG-[A-Z]+-\d+$/,
|
||
system: /^(TCS|ICE|SYS)-/,
|
||
hldp: /^(HLDP|REG|PER-BABY)-/
|
||
};
|
||
|
||
// ISO-8601 date pattern
|
||
const ISO_DATE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/;
|
||
|
||
/**
|
||
* Validate a single HLDP entry
|
||
* @returns {Object} { valid: boolean, errors: string[], warnings: string[] }
|
||
*/
|
||
function validateEntry(entry, filePath) {
|
||
const errors = [];
|
||
const warnings = [];
|
||
const prefix = filePath ? `[${path.basename(filePath)}] ` : '';
|
||
|
||
// 1. Check required top-level fields
|
||
if (!entry.hldp_version) errors.push(`${prefix}缺少 hldp_version`);
|
||
if (!entry.data_type) errors.push(`${prefix}缺少 data_type`);
|
||
if (!entry.source) errors.push(`${prefix}缺少 source`);
|
||
if (!entry.metadata) errors.push(`${prefix}缺少 metadata`);
|
||
if (!entry.payload) errors.push(`${prefix}缺少 payload`);
|
||
|
||
// 2. Check hldp_version
|
||
if (entry.hldp_version && entry.hldp_version !== '1.0' && entry.hldp_version !== '2.0') {
|
||
warnings.push(`${prefix}hldp_version 为 "${entry.hldp_version}",预期 "1.0" 或 "2.0"`);
|
||
}
|
||
|
||
// 3. Check data_type
|
||
if (entry.data_type && !VALID_DATA_TYPES.includes(entry.data_type)) {
|
||
errors.push(`${prefix}无效的 data_type: "${entry.data_type}"`);
|
||
}
|
||
|
||
// 4. Check source
|
||
if (entry.source) {
|
||
if (!entry.source.platform) {
|
||
errors.push(`${prefix}source.platform 为空`);
|
||
}
|
||
if (entry.source.last_edited && !ISO_DATE.test(entry.source.last_edited)) {
|
||
warnings.push(`${prefix}source.last_edited 不是有效的 ISO-8601 日期`);
|
||
}
|
||
}
|
||
|
||
// 5. Check metadata
|
||
if (entry.metadata) {
|
||
if (!entry.metadata.id) errors.push(`${prefix}metadata.id 为空`);
|
||
if (!entry.metadata.name) errors.push(`${prefix}metadata.name 为空`);
|
||
|
||
if (entry.metadata.created && !ISO_DATE.test(entry.metadata.created)) {
|
||
warnings.push(`${prefix}metadata.created 不是有效的 ISO-8601 日期`);
|
||
}
|
||
|
||
if (entry.metadata.tags && !Array.isArray(entry.metadata.tags)) {
|
||
errors.push(`${prefix}metadata.tags 应为数组`);
|
||
}
|
||
}
|
||
|
||
// 6. Check payload based on data_type
|
||
if (entry.data_type === 'persona' && entry.payload) {
|
||
if (!entry.payload.persona_id && !entry.payload.display_name) {
|
||
warnings.push(`${prefix}persona payload 缺少 persona_id 和 display_name`);
|
||
}
|
||
if (entry.payload.status && !['active', 'dormant', 'frozen', 'born', 'incubating'].includes(entry.payload.status)) {
|
||
warnings.push(`${prefix}无效的 persona status: "${entry.payload.status}"`);
|
||
}
|
||
}
|
||
|
||
if (entry.data_type === 'registry' && entry.payload) {
|
||
if (!entry.payload.registry_type) {
|
||
warnings.push(`${prefix}registry payload 缺少 registry_type`);
|
||
}
|
||
if (!entry.payload.entries || !Array.isArray(entry.payload.entries)) {
|
||
warnings.push(`${prefix}registry payload 缺少 entries 数组`);
|
||
}
|
||
}
|
||
|
||
if (entry.data_type === 'instruction' && entry.payload) {
|
||
if (!entry.payload.instruction_id) {
|
||
warnings.push(`${prefix}instruction payload 缺少 instruction_id`);
|
||
}
|
||
if (!entry.payload.title) {
|
||
warnings.push(`${prefix}instruction payload 缺少 title`);
|
||
}
|
||
}
|
||
|
||
if (entry.data_type === 'snapshot' && entry.payload) {
|
||
if (!entry.payload.consciousness_status) {
|
||
warnings.push(`${prefix}snapshot payload 缺少 consciousness_status`);
|
||
}
|
||
if (!entry.payload.system_counts) {
|
||
warnings.push(`${prefix}snapshot payload 缺少 system_counts`);
|
||
}
|
||
if (!entry.payload.health) {
|
||
warnings.push(`${prefix}snapshot payload 缺少 health`);
|
||
}
|
||
}
|
||
|
||
// 7. Check relations
|
||
if (entry.relations && Array.isArray(entry.relations)) {
|
||
for (const rel of entry.relations) {
|
||
if (!rel.target_id) {
|
||
errors.push(`${prefix}relation 缺少 target_id`);
|
||
}
|
||
if (rel.relation_type && !VALID_RELATION_TYPES.includes(rel.relation_type)) {
|
||
warnings.push(`${prefix}无效的 relation_type: "${rel.relation_type}"`);
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
valid: errors.length === 0,
|
||
errors,
|
||
warnings
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Validate all HLDP files in a directory
|
||
*/
|
||
function validateDirectory(dataDir, schemaDir) {
|
||
const results = {
|
||
total: 0,
|
||
valid: 0,
|
||
invalid: 0,
|
||
errors: [],
|
||
warnings: [],
|
||
files: []
|
||
};
|
||
|
||
if (!fs.existsSync(dataDir)) {
|
||
console.log(`⚠️ 数据目录不存在: ${dataDir}`);
|
||
return results;
|
||
}
|
||
|
||
// Recursively find all JSON files
|
||
function walkDir(dir) {
|
||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||
for (const entry of entries) {
|
||
const fullPath = path.join(dir, entry.name);
|
||
if (entry.isDirectory()) {
|
||
walkDir(fullPath);
|
||
} else if (entry.name.endsWith('.json')) {
|
||
results.total++;
|
||
try {
|
||
const data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||
|
||
// Skip non-HLDP files
|
||
if (!data.hldp_version) {
|
||
results.total--;
|
||
results.files.push({ file: path.relative(ROOT, fullPath), status: 'skipped', reason: 'not HLDP format' });
|
||
continue;
|
||
}
|
||
|
||
const validation = validateEntry(data, fullPath);
|
||
results.files.push({
|
||
file: path.relative(ROOT, fullPath),
|
||
status: validation.valid ? 'valid' : 'invalid',
|
||
errors: validation.errors,
|
||
warnings: validation.warnings
|
||
});
|
||
|
||
if (validation.valid) {
|
||
results.valid++;
|
||
} else {
|
||
results.invalid++;
|
||
}
|
||
|
||
results.errors.push(...validation.errors);
|
||
results.warnings.push(...validation.warnings);
|
||
} catch (e) {
|
||
results.invalid++;
|
||
results.errors.push(`[${entry.name}] JSON 解析失败: ${e.message}`);
|
||
results.files.push({ file: fullPath, status: 'parse_error', errors: [e.message] });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
walkDir(dataDir);
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* CLI entry point
|
||
*/
|
||
function runCLI() {
|
||
const args = process.argv.slice(2);
|
||
let dataDir = path.join(ROOT, 'hldp', 'data');
|
||
let schemaDir = path.join(ROOT, 'hldp', 'schema');
|
||
|
||
for (let i = 0; i < args.length; i++) {
|
||
if (args[i] === '--data' && args[i + 1]) { dataDir = path.resolve(ROOT, args[i + 1]); i++; }
|
||
if (args[i] === '--schema' && args[i + 1]) { schemaDir = path.resolve(ROOT, args[i + 1]); i++; }
|
||
}
|
||
|
||
console.log('✅ HLDP 格式校验器启动');
|
||
console.log(` 📂 数据目录: ${dataDir}`);
|
||
console.log(` 📋 Schema 目录: ${schemaDir}`);
|
||
|
||
const results = validateDirectory(dataDir, schemaDir);
|
||
|
||
console.log(`\n📊 校验结果:`);
|
||
console.log(` 总文件数: ${results.total}`);
|
||
console.log(` ✅ 有效: ${results.valid}`);
|
||
console.log(` ❌ 无效: ${results.invalid}`);
|
||
console.log(` ⚠️ 警告: ${results.warnings.length}`);
|
||
|
||
if (results.errors.length > 0) {
|
||
console.log('\n❌ 错误:');
|
||
results.errors.forEach(e => console.log(` - ${e}`));
|
||
}
|
||
|
||
if (results.warnings.length > 0) {
|
||
console.log('\n⚠️ 警告:');
|
||
results.warnings.forEach(w => console.log(` - ${w}`));
|
||
}
|
||
|
||
// Write validation report
|
||
const reportPath = path.join(ROOT, 'signal-log', 'hldp-validation-report.json');
|
||
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
||
fs.writeFileSync(reportPath, JSON.stringify({
|
||
timestamp: new Date().toISOString(),
|
||
results
|
||
}, null, 2), 'utf8');
|
||
console.log(`\n📝 校验报告已保存: ${reportPath}`);
|
||
|
||
// Exit with error code if there are errors
|
||
if (results.invalid > 0) {
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
module.exports = { validateEntry, validateDirectory };
|
||
|
||
if (require.main === module) {
|
||
runCLI();
|
||
}
|