// U7 法规结构化引用纯逻辑层(契约:Dev 仓 docs/superpowers/specs // 2026-10-18-regulations-citation-contract.md §1/§3/§5/§6)。只放可测判定: // 正文 [cit-…] 标记解析与编号、核验状态文案与样式、原文省略原因、审计树请求参数。 // 引用是 RDM 证据批的派生视图;除 verified 外一律不得呈现为有效法律依据。 // ---- 契约类型 ---- interface CitationWeb { url: string finalUrl?: string captureId?: string contentSha256?: string official?: boolean fetchedAt?: string excerpt?: string excerptStart?: number excerptEnd?: number } export interface CitationView { id: string batchId?: string claim?: string state: string // verified|single_source|degraded|edition_unresolved|conflict|unverified|not_applicable|invalid stale?: boolean staleReason?: string regulationId?: string editionId?: string nodeId?: string docNo?: string title?: string editionLabel?: string legalStatus?: string clauseNo?: string headingPath?: string[] corpusRevision?: string excerpt?: string excerptStart?: number excerptEnd?: number nodeSha256?: string disclosure?: string // full|excerpt|metadata_only|withheld redactionReason?: string web?: CitationWeb manifestSha256?: string } interface RegLegStates { kb: string; web: string; kbReason?: string; webReason?: string } interface RegSealView { state: string; keyId?: string; manifestSha256?: string } interface RegResolution { actor: string; decision: string; note?: string; at: string; batchManifestSha256?: string } /** rdm.inspect.object kind=reg-batch(契约 §5)。 */ export interface RegBatchDetail { title: string state: string stateReasons?: string[] | null legStates?: RegLegStates seal?: RegSealView citations?: CitationView[] | null resolutions?: RegResolution[] | null } /** rdm.inspect.object kind=reg-citation(契约 §5)。 */ export interface RegCitationDetail { title: string; citation: CitationView } // ---- 正文标记:[cit-<12 位小写十六进制>] ---- const MARKER = /\[(cit-[0-9a-f]{12})\]/g type CitedSegment = | { kind: 'text'; text: string } | { kind: 'cite'; id: string; n: number } /** 解析多段正文:按首次出现编号(跨段连续),重复标记复用编号;不合规的近似标记原样保留为文本。 */ export function parseCitedBlocks(texts: string[]): { blocks: CitedSegment[][]; order: string[] } { const order: string[] = [] const numbers = new Map() const blocks = texts.map(text => { const out: CitedSegment[] = [] let last = 0 for (const m of text.matchAll(MARKER)) { const at = m.index ?? 0 if (at > last) out.push({ kind: 'text', text: text.slice(last, at) }) const id = m[1] let n = numbers.get(id) if (n === undefined) { order.push(id) n = order.length numbers.set(id, n) } out.push({ kind: 'cite', id, n }) last = at + m[0].length } if (last < text.length) out.push({ kind: 'text', text: text.slice(last) }) return out }) return { blocks, order } } /** 引用清单顺序:正文编号在前,正文未出现的引用按返回顺序续编。 */ export function citationListOrder(order: string[], citations: CitationView[] | null | undefined): string[] { const out = [...order] const seen = new Set(order) for (const c of citations || []) { if (!seen.has(c.id)) { seen.add(c.id); out.push(c.id) } } return out } // ---- 核验状态 ---- const STATE_LABELS: Record = { verified: '已核验', single_source: '单一来源', degraded: '降级', edition_unresolved: '版本未定', conflict: '冲突', unverified: '未核验', not_applicable: '不适用', invalid: '无效引用', } /** 状态文案;未知状态原样显示,不冒充已知状态。 */ export function citationStateLabel(state: string | undefined): string { if (!state) return STATE_LABELS.unverified return STATE_LABELS[state] ?? state } type CitationStyle = 'valid' | 'unverified' | 'invalid' /** 样式档:仅 verified 且未过期才是有效依据;ID 缺失或 invalid 为无效;其余一律未核验档。 */ export function citationStyle(c: { state?: string; stale?: boolean } | undefined): CitationStyle { if (!c || c.state === 'invalid') return 'invalid' if (c.state === 'verified' && !c.stale) return 'valid' return 'unverified' } export const CITATION_CHIP_TONE: Record = { valid: 'ok', unverified: 'warn', invalid: 'danger', } const STALE_REASONS: Record = { node_text_changed: '条款文本已变更', legal_status_changed: '效力状态已变更', edition_superseded: '版本已被替代', } /** 过期提示:stale 显示「待复核」并附原因;未过期返回 null。 */ export function staleLabel(c: { stale?: boolean; staleReason?: string }): string | null { if (!c.stale) return null if (!c.staleReason) return '待复核' return `待复核(${STALE_REASONS[c.staleReason] ?? c.staleReason})` } /** 标记文字:有效依据只显示编号;其余附状态文案,不靠颜色单独区分。 */ export function citationMarkText(n: number, c: CitationView | undefined): string { const style = citationStyle(c) if (style === 'valid') return `${n}` if (style === 'invalid') return `${n}·${STATE_LABELS.invalid}` return `${n}·${c?.stale && c.state === 'verified' ? '待复核' : citationStateLabel(c?.state)}` } // ---- 原文披露 ---- const REDACTION_LABELS: Record = { withheld_by_rights: '受版权限制', metadata_only: '仅元数据', no_source_text_permission: '无原文查看权限', source_withdrawn: '来源已撤回', } /** 原文省略原因;未知原因原样显示。 */ export function redactionLabel(reason: string | undefined): string { if (!reason) return '未提供原文' return REDACTION_LABELS[reason] ?? reason } /** 原文片段呈现:有片段给片段;省略时给中文原因,不留空白。 */ export function excerptView(c: Pick): { excerpt: string } | { redaction: string } { if (c.excerpt) return { excerpt: c.excerpt } return { redaction: redactionLabel(c.redactionReason) } } const DISCLOSURE_LABELS: Record = { full: '全文', excerpt: '片段', metadata_only: '仅元数据', withheld: '已扣留', } export function disclosureLabel(d: string | undefined): string { if (!d) return '—' return DISCLOSURE_LABELS[d] ?? d } const LEG_LABELS: Record = { ok: '正常', empty: '无结果', unavailable: '不可用', degraded: '降级', error: '出错', rejected: '已拒绝', } export function legStateLabel(state: string | undefined, reason?: string): string { const label = state ? (LEG_LABELS[state] ?? state) : '—' return reason ? `${label}(${reason})` : label } export function sealStateLabel(state: string | undefined): string { if (state === 'sealed') return '已封存' if (state === 'unsealed') return '未封存' return state || '—' } /** 只有 http(s) 地址才可作为外链;其余地址按文本显示。 */ export function safeWebHref(url: string | undefined): string | null { if (!url) return null return /^https?:\/\//i.test(url) ? url : null } // ---- 审计读模型请求(rdm.inspect.tree kind=regs;契约 §5) ---- /** 根层列证据批;parent= 列该批的引用。cursor 沿用既有键集分页。 */ export function regsTreeParams(projectId: string, opts: { parent?: string; cursor?: string } = {}): Record { const params: Record = { projectId, kind: 'regs', cursor: opts.cursor ?? '' } if (opts.parent) params.parent = opts.parent return params } export const REG_TREE_KIND_LABELS: Record = { 'reg-batch': '证据批', 'reg-citation': '引用', }