import { readFile } from 'node:fs/promises'; const LEAUDIT_RULES_ROOT = `${process.cwd()}/mock-data/leaudit-rules/packs/yc`; export type RulePackScope = { documentType: string; moduleType: string; mainType: string; subtype: string; }; export type RuleSummary = { id: string; ruleId: string; name: string; group: string; risk: string; score: string; type: string; checkTypes: string[]; logic: string; subRules: Array<{ id: string; check: string; content: string; }>; subRuleIds: string[]; scope: string[]; dependencies: string[]; stageCount: number; appliesIn: string[]; prompt: string; description: string; }; export type ExtractFieldSummary = { id: string; group: string; name: string; type: string; multipleEntities: boolean; requiredFrom: string; description: string; }; export type SubDocumentSummary = { id: string; name: string; required: string; fieldCount: number; groups: string[]; description: string; fields: ExtractFieldSummary[]; }; export type RuleYamlPack = RulePackScope & { id: string; yamlPath: string | null; yamlSource: string; sourceStatus: 'ready' | 'empty' | 'missing'; metadata: { typeId: string; name: string; version: string; lastUpdated: string; parent: string; description: string; tags: string[]; keywords: string[]; inheritsFrom: string[]; }; stats: { ruleCount: number; fieldCount: number; subDocumentCount: number; visualElementCount: number; }; rules: RuleSummary[]; fields: ExtractFieldSummary[]; subDocuments: SubDocumentSummary[]; visualElements: Array<{ id: string; name: string; type: string; required: string; signerRoles?: string[]; signatureTypes?: string[]; privateSealRestricted?: boolean; }>; }; export const EMPTY_RULE_YAML = `metadata: type_id: pending.internal.document name: 内部公文规则配置 version: '0.1' last_updated: '待配置' description: '当前暂无内部公文规则 YAML。此测试页面保留规则列表与配置页流程。' extract: [] rules: [] `; const MOCK_RULE_PACKS: Array = [ { id: 'contract-purchase', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '买卖合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_purchase/rules.yaml` }, { id: 'contract-sale', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '通用买卖合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_sale/rules.yaml` }, { id: 'contract-tech', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '技术合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_tech/rules.yaml` }, { id: 'contract-lease', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '租赁合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_lease/rules.yaml` }, { id: 'contract-entrust', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '委托合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_entrust/rules.yaml` }, { id: 'contract-construction', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '建设工程合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_construction/rules.yaml` }, { id: 'contract-evaluation', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '委托评估合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_evaluation/rules.yaml` }, { id: 'contract-gift-general', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '赠与合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_gift_general/rules.yaml` }, { id: 'contract-gift-charity', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '公益捐赠合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_gift_charity/rules.yaml` }, { id: 'contract-loan', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '借款合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_loan/rules.yaml` }, { id: 'case-penalty', documentType: '案卷', moduleType: '案卷评查', mainType: '行政处罚', subtype: '通用', yamlPath: `${LEAUDIT_RULES_ROOT}/行政处罚/rules.yaml` }, { id: 'case-license-new', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '新办', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_新办/rules.yaml` }, { id: 'case-license-extend', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '延续', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_延续/rules.yaml` }, { id: 'case-license-change', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '变更', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_变更/rules.yaml` }, { id: 'case-license-cancel', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '注销', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_注销/rules.yaml` }, { id: 'case-license-suspend', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '停业', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_停业/rules.yaml` }, { id: 'case-license-close', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '歇业', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_歇业/rules.yaml` }, { id: 'case-license-reissue', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '补办', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_补办/rules.yaml` }, { id: 'case-license-retrieve', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '收回', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_收回/rules.yaml` }, { id: 'case-license-restore', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '恢复营业', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_恢复营业/rules.yaml` }, { id: 'internal-document', documentType: '内部公文', moduleType: '内部公文评查', mainType: '内部公文', subtype: '通用', yamlPath: null } ]; function getTopLevelSection(source: string, key: string): string { const lines = source.split('\n'); const start = lines.findIndex(line => line === `${key}:`); if (start === -1) { return ''; } const end = lines.findIndex((line, index) => index > start && /^[a-zA-Z_][\w-]*:/.test(line)); return lines.slice(start + 1, end === -1 ? undefined : end).join('\n'); } function stripYamlValue(value = ''): string { return value.trim().replace(/^['"]|['"]$/g, '').replace(/\u0000/g, ''); } function parseScalar(section: string, key: string): string { const match = section.match(new RegExp(`^\\s{2}${key}:\\s*(.*)$`, 'm')); return stripYamlValue(match?.[1] || ''); } function parseListAfterKey(section: string, key: string): string[] { const lines = section.split('\n'); const start = lines.findIndex(line => new RegExp(`^\\s{2}${key}:\\s*$`).test(line)); if (start === -1) { return []; } const values: string[] = []; for (let index = start + 1; index < lines.length; index += 1) { const line = lines[index]; if (/^\s{2}\w/.test(line)) { break; } const match = line.match(/^\s{2,}-\s*(.+)$/); if (match) { values.push(stripYamlValue(match[1])); } } return values; } function parseMetadata(source: string): RuleYamlPack['metadata'] { const section = getTopLevelSection(source, 'metadata'); return { typeId: parseScalar(section, 'type_id'), name: parseScalar(section, 'name'), version: parseScalar(section, 'version'), lastUpdated: parseScalar(section, 'last_updated'), parent: parseScalar(section, 'parent'), description: stripYamlValue((section.match(/^\s{2}description:\s*'?([\s\S]*?)(?:\n\s{2}\w|\n[a-zA-Z_]|\n?$)/m)?.[1] || '').replace(/\n\s+/g, ' ').trim()), tags: parseListAfterKey(section, 'tags'), keywords: parseListAfterKey(section, 'classification_keywords'), inheritsFrom: parseListAfterKey(section, 'inherits_from') }; } function splitBlocks(section: string, marker: RegExp): string[] { const lines = section.split('\n'); const starts = lines.reduce((indexes, line, index) => { if (marker.test(line)) { indexes.push(index); } return indexes; }, []); return starts.map((start, index) => lines.slice(start, starts[index + 1]).join('\n')); } function parseRules(source: string): RuleSummary[] { const section = getTopLevelSection(source, 'rules'); const groups = splitBlocks(section, /^-\s+group:\s*/); const readExplicitDependencies = (block: string): string[] => { const lines = block.split('\n'); const start = lines.findIndex(line => /^\s{4}dependencies:\s*$/.test(line)); if (start === -1) { return []; } const dependencies: string[] = []; for (let index = start + 1; index < lines.length; index += 1) { const line = lines[index]; if (/^\s{4}[a-zA-Z_][^:]*:\s*/.test(line)) { break; } const match = line.match(/^\s{4}-\s+(.+)$/); if (match) { dependencies.push(stripYamlValue(match[1])); } } return dependencies; }; const normalizeDependency = (value: string) => { const normalized = stripYamlValue(value); if (normalized === 'cross_page_seal') return '骑缝章'; if (normalized === 'seal') return '印章'; if (normalized === 'signature') return '签名'; return normalized; }; const readPrompts = (block: string): string[] => { const lines = block.split('\n'); const prompts: string[] = []; for (let index = 0; index < lines.length; index += 1) { const match = lines[index].match(/^(\s*)prompt:\s*(.*)$/); if (!match) continue; const indent = match[1].length; const parts = [match[2]]; for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) { const line = lines[nextIndex]; const nextIndent = line.match(/^\s*/)?.[0].length || 0; const trimmed = line.trim(); if (trimmed && nextIndent <= indent) break; if (trimmed && nextIndent === indent + 2 && /^[a-zA-Z_][\w-]*:\s*/.test(trimmed)) break; parts.push(line); } prompts.push(parts.join('\n') .replace(/^['"]/, '') .replace(/['"]\s*$/, '') .split('\n') .map(line => line.replace(/^\s{8}/, '')) .join('\n') .trim()); } return prompts.filter(Boolean); }; const readList = (block: string, key: string, indent = 4): string[] => { const lines = block.split('\n'); const start = lines.findIndex(line => new RegExp(`^\\s{${indent}}${key}:\\s*$`).test(line)); if (start === -1) { return []; } const values: string[] = []; for (let index = start + 1; index < lines.length; index += 1) { const line = lines[index]; if (new RegExp(`^\\s{${indent}}[a-zA-Z_][^:]*:\\s*`).test(line)) { break; } const match = line.match(new RegExp(`^\\s{${indent}}-\\s+(.+)$`)); if (match) { values.push(stripYamlValue(match[1])); } } return values; }; const readFlexibleList = (block: string, key: string): string[] => { const lines = block.split('\n'); const start = lines.findIndex(line => new RegExp(`^(\\s*)${key}:\\s*$`).test(line)); if (start === -1) return []; const indent = lines[start].match(/^\s*/)?.[0].length || 0; const values: string[] = []; for (let index = start + 1; index < lines.length; index += 1) { const line = lines[index]; const lineIndent = line.match(/^\s*/)?.[0].length || 0; const match = line.match(/^\s*-\s+(.+)$/); if (match) { values.push(stripYamlValue(match[1])); continue; } if (line.trim() && lineIndent <= indent) break; } return values; }; const readStageList = (block: string, key: string): string[] => { const lines = block.split('\n'); const start = lines.findIndex(line => new RegExp(`^\\s{6}${key}:\\s*$`).test(line)); if (start === -1) { return []; } const values: string[] = []; for (let index = start + 1; index < lines.length; index += 1) { const line = lines[index]; if (/^\s{6}[a-zA-Z_][^:]*:\s*/.test(line)) { break; } const match = line.match(/^\s{6}-\s+(.+)$/); if (match) { values.push(stripYamlValue(match[1])); } } return values; }; const readStageScalar = (block: string, key: string): string => stripYamlValue(block.match(new RegExp(`^\\s{6}${key}:\\s*(.+)$`, 'm'))?.[1] || ''); const summarizeStage = (stageBlock: string): string => { const fields = readStageList(stageBlock, 'fields'); const field = readStageScalar(stageBlock, 'field'); const left = readStageScalar(stageBlock, 'left') || readStageScalar(stageBlock, 'left_field'); const op = readStageScalar(stageBlock, 'op'); const right = readStageScalar(stageBlock, 'right') || readStageScalar(stageBlock, 'right_field'); const value = readStageScalar(stageBlock, 'value'); const prompt = readStageScalar(stageBlock, 'prompt'); const element = readStageScalar(stageBlock, 'element') || readStageScalar(stageBlock, 'seal_id') || readStageScalar(stageBlock, 'signature_id'); if (fields.length > 0) return fields.join('、'); if (left || right) return [left, op, right].filter(Boolean).join(' '); if (field && value) return `${field} = ${value}`; if (field) return field; if (element) return element; if (prompt) return prompt.slice(0, 80); return stageBlock.split('\n').map(line => line.trim()).filter(Boolean).slice(1, 4).join(';') || '未配置内容'; }; const readSubRules = (block: string) => splitBlocks(block, /^\s{4}-\s+id:\s*/).map(stageBlock => { const id = stripYamlValue(stageBlock.match(/^\s{4}-\s+id:\s*(.+)$/m)?.[1] || ''); const check = readStageScalar(stageBlock, 'check') || readStageScalar(stageBlock, 'type') || '-'; return { id, check, content: summarizeStage(stageBlock) }; }).filter(stage => stage.id); return groups.flatMap(groupBlock => { const group = stripYamlValue(groupBlock.match(/^-\s+group:\s*(.+)$/m)?.[1] || '未分组'); return splitBlocks(groupBlock, /^\s{2}-\s+rule_id:\s*/).map(ruleBlock => { const ruleId = stripYamlValue(ruleBlock.match(/^\s{2}-\s+rule_id:\s*(.+)$/m)?.[1] || ''); const name = stripYamlValue(ruleBlock.match(/^\s{4}name:\s*(.+)$/m)?.[1] || '未命名规则'); const checkTypes = Array.from(new Set(Array.from(ruleBlock.matchAll(/^\s{6,}(?:check|type):\s*(.+)$/gm)).map(match => stripYamlValue(match[1])))); const stageDependencies = Array.from(ruleBlock.matchAll(/^\s{6,}(?:field|number|chinese|left|right|left_field|right_field|target|element|seal_id|signature_id):\s*(.+)$/gm)) .map(match => normalizeDependency(match[1])); const dependencies = Array.from(new Set([...readExplicitDependencies(ruleBlock), ...stageDependencies])); const scope = Array.from(new Set(Array.from(ruleBlock.matchAll(/^\s{4,}-\s*([^:\n]+)$/gm)).map(match => stripYamlValue(match[1])).filter(value => !/^\d+$/.test(value)))); const prompts = readPrompts(ruleBlock); const subRules = readSubRules(ruleBlock); return { id: ruleId || `${group}-${name}`, ruleId, name, group, risk: stripYamlValue(ruleBlock.match(/^\s{4}risk:\s*(.+)$/m)?.[1] || 'medium'), score: stripYamlValue(ruleBlock.match(/^\s{4}score:\s*(.+)$/m)?.[1] || '-'), type: stripYamlValue(ruleBlock.match(/^\s{4}type:\s*(.+)$/m)?.[1] || 'deterministic'), checkTypes, logic: stripYamlValue(ruleBlock.match(/^\s{4}logic:\s*(.+)$/m)?.[1] || ''), subRules, subRuleIds: readList(ruleBlock, 'rules'), scope: scope.slice(0, 8), dependencies: dependencies.slice(0, 8), stageCount: subRules.length, appliesIn: readFlexibleList(ruleBlock, 'applies_in'), prompt: prompts.join('\n\n'), description: stripYamlValue(ruleBlock.match(/^\s{4}desc:\s*(.+)$/m)?.[1] || '') }; }); }); } export function parseRuleSummariesFromYaml(source: string): RuleSummary[] { return parseRules(source); } function parseTopLevelFields(source: string): ExtractFieldSummary[] { const section = getTopLevelSection(source, 'extract'); const extractedFields = splitBlocks(section, /^-\s+group:\s*/).flatMap(groupBlock => { const group = stripYamlValue(groupBlock.match(/^-\s+group:\s*(.+)$/m)?.[1] || '未分组'); return splitBlocks(groupBlock, /^\s{2}-\s+name:\s*/).flatMap(fieldBlock => { const name = stripYamlValue(fieldBlock.match(/^\s{2}-\s+name:\s*(.+)$/m)?.[1] || ''); const rawType = stripYamlValue(fieldBlock.match(/^\s{4}type:\s*(.+)$/m)?.[1] || '-'); const parentField = { id: `${group}-${name}`, group, name, type: rawType === 'multi_entity' ? 'verbatim' : rawType, multipleEntities: rawType === 'multi_entity', requiredFrom: stripYamlValue(fieldBlock.match(/^\s{4}required_from:\s*(.+)$/m)?.[1] || '-'), description: stripYamlValue(fieldBlock.match(/^\s{4}desc:\s*(.+)$/m)?.[1] || '') }; const childFields = Array.from(fieldBlock.matchAll(/^\s{4}-\s+name:\s*(.+)$/gm)).map(match => { const childName = stripYamlValue(match[1]); const start = fieldBlock.indexOf(match[0]); const next = fieldBlock.slice(start + match[0].length).search(/^\s{4}-\s+name:\s*/m); const childBlock = next === -1 ? fieldBlock.slice(start) : fieldBlock.slice(start, start + match[0].length + next); const childType = stripYamlValue(childBlock.match(/^\s{6}type:\s*(.+)$/m)?.[1] || 'verbatim'); return { id: `${group}-${name}-${childName}`, group, name: `${name}[*].${childName}`, type: childType, multipleEntities: false, requiredFrom: stripYamlValue(childBlock.match(/^\s{6}required_from:\s*(.+)$/m)?.[1] || parentField.requiredFrom), description: stripYamlValue(childBlock.match(/^\s{6}desc:\s*(.+)$/m)?.[1] || `${name}的子字段`) }; }); return [parentField, ...childFields]; }); }).filter(field => field.name); const derivedSection = getTopLevelSection(source, 'derived_fields'); const derivedFields = splitBlocks(derivedSection, /^-\s+name:\s*/).map(fieldBlock => { const name = stripYamlValue(fieldBlock.match(/^-\s+name:\s*(.+)$/m)?.[1] || ''); return { id: `derived-${name}`, group: '派生字段', name, type: stripYamlValue(fieldBlock.match(/^\s{2}type:\s*(.+)$/m)?.[1] || 'computed'), multipleEntities: false, requiredFrom: '-', description: stripYamlValue(fieldBlock.match(/^\s{2}compute:\s*(.+)$/m)?.[1] || '由其他字段计算得出') }; }).filter(field => field.name); return [...extractedFields, ...derivedFields]; } function parseDocumentFields(docBlock: string, documentId: string): ExtractFieldSummary[] { return splitBlocks(docBlock, /^\s{2}-\s+group:\s*/).flatMap(groupBlock => { const group = stripYamlValue(groupBlock.match(/^\s{2}-\s+group:\s*(.+)$/m)?.[1] || '未分组'); return splitBlocks(groupBlock, /^\s{4}-\s+name:\s*/).map(fieldBlock => { const name = stripYamlValue(fieldBlock.match(/^\s{4}-\s+name:\s*(.+)$/m)?.[1] || ''); const rawType = stripYamlValue(fieldBlock.match(/^\s{6}type:\s*(.+)$/m)?.[1] || '-'); return { id: `${documentId}-${group}-${name}`, group, name, type: rawType === 'multi_entity' ? 'verbatim' : rawType, multipleEntities: rawType === 'multi_entity', requiredFrom: '-', description: stripYamlValue(fieldBlock.match(/^\s{6}desc:\s*(.+)$/m)?.[1] || '') }; }); }).filter(field => field.name); } function parseSubDocuments(source: string): SubDocumentSummary[] { const section = getTopLevelSection(source, 'sub_documents'); return splitBlocks(section, /^-\s+id:\s*/).map(docBlock => { const id = stripYamlValue(docBlock.match(/^-\s+id:\s*(.+)$/m)?.[1] || ''); const groups = Array.from(new Set(Array.from(docBlock.matchAll(/^\s{2,}-\s+group:\s*(.+)$/gm)).map(match => stripYamlValue(match[1])))); const fields = parseDocumentFields(docBlock, id); const classifier = docBlock.match(/^\s{2}classifier:\s*$/m); let description = ''; if (classifier) { const keywordsMatch = docBlock.match(/keywords:\s*\n((?:\s{4}-\s+.+\n)+)/m); if (keywordsMatch) { const keywords = Array.from(keywordsMatch[1].matchAll(/^\s{4}-\s+(.+)$/gm)).map(match => stripYamlValue(match[1])).slice(0, 3); description = keywords.join('、'); } } return { id, name: stripYamlValue(docBlock.match(/^\s{2}name:\s*(.+)$/m)?.[1] || id), required: stripYamlValue(docBlock.match(/^\s{2}required:\s*(.+)$/m)?.[1] || '-'), fieldCount: fields.length, groups, description, fields }; }).filter(doc => doc.id); } function parseVisualElements(source: string): RuleYamlPack['visualElements'] { const section = getTopLevelSection(source, 'visual_elements'); const typedSections = [ { key: 'seals', label: '签章' }, { key: 'signatures', label: '签名' }, { key: 'cross_page_seals', label: '骑缝章' } ]; return typedSections.flatMap(({ key, label }) => { const lines = section.split('\n'); const start = lines.findIndex(line => new RegExp(`^\\s{2}${key}:`).test(line)); if (start === -1) { return []; } // 找到下一个同级分类的起始位置(2空格+字母+冒号) let end = lines.length; for (let i = start + 1; i < lines.length; i++) { if (/^\s{2}[a-zA-Z_][\w-]*:/.test(lines[i])) { end = i; break; } } const subSection = lines.slice(start + 1, end).join('\n'); return splitBlocks(subSection, /^\s{2}-\s+id:\s*/).map(block => ({ id: stripYamlValue(block.match(/^\s{2}-\s+id:\s*(.+)$/m)?.[1] || ''), name: stripYamlValue(block.match(/^\s{4}name:\s*(.+)$/m)?.[1] || ''), type: label, required: stripYamlValue(block.match(/^\s{4}required:\s*(.+)$/m)?.[1] || '-'), signerRoles: block.match(/^\s{4}signer_roles:\s*\[(.+)\]$/m)?.[1].split(',').map(s => s.trim()) || [], signatureTypes: block.match(/^\s{4}signature_types:\s*\[(.+)\]$/m)?.[1].split(',').map(s => s.trim()) || [], privateSealRestricted: block.match(/^\s{4}private_seal_restricted:\s*(.+)$/m)?.[1] === 'true' })); }).filter(item => item.id); } export function buildRuleYamlPack( config: RulePackScope & { id: string; yamlPath: string | null }, yamlSource: string, sourceStatus: RuleYamlPack['sourceStatus'] ): RuleYamlPack { const metadata = parseMetadata(yamlSource); const fields = parseTopLevelFields(yamlSource); const subDocuments = parseSubDocuments(yamlSource); const rules = parseRules(yamlSource); const visualElements = parseVisualElements(yamlSource); return { ...config, yamlSource, sourceStatus, metadata, rules, fields, subDocuments, visualElements, stats: { ruleCount: rules.length, fieldCount: fields.length + subDocuments.reduce((sum, doc) => sum + doc.fieldCount, 0), subDocumentCount: subDocuments.length, visualElementCount: visualElements.length } }; } export async function loadRuleYamlPacks(): Promise { return Promise.all(MOCK_RULE_PACKS.map(async config => { // TODO(production-data-source): // 当前测试页直接读取 leaudit 本地 YAML 作为 mock。 // 生产切换时,这里应改为调用后端接口: // 1. 后端根据文档类型/主类型/子类型查询数据库中的 OSS YAML 路径; // 2. 后端读取 OSS YAML 正文并返回元数据和内容; // 3. 前端仍消费 buildPack 之后的结构化数据,页面不直接关心 OSS 实现。 if (!config.yamlPath) { return buildRuleYamlPack(config, EMPTY_RULE_YAML, 'empty'); } try { const yamlSource = await readFile(config.yamlPath, 'utf8'); return buildRuleYamlPack(config, yamlSource, 'ready'); } catch { return buildRuleYamlPack(config, EMPTY_RULE_YAML, 'missing'); } })); } export async function loadRuleYamlPack(id: string): Promise { const packs = await loadRuleYamlPacks(); return packs.find(pack => pack.id === id); }