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; }; 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 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")); } export function parseRuleSummariesFromYaml(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] || ""), } satisfies RuleSummary; }); }); }