Files
Agent-System-Frontend/src/regCitations.ts
T

236 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<string, number>()
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<string, string> = {
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<CitationStyle, 'ok' | 'warn' | 'danger'> = {
valid: 'ok',
unverified: 'warn',
invalid: 'danger',
}
const STALE_REASONS: Record<string, string> = {
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<string, string> = {
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<CitationView, 'excerpt' | 'redactionReason'>): { excerpt: string } | { redaction: string } {
if (c.excerpt) return { excerpt: c.excerpt }
return { redaction: redactionLabel(c.redactionReason) }
}
const DISCLOSURE_LABELS: Record<string, string> = {
full: '全文',
excerpt: '片段',
metadata_only: '仅元数据',
withheld: '已扣留',
}
export function disclosureLabel(d: string | undefined): string {
if (!d) return '—'
return DISCLOSURE_LABELS[d] ?? d
}
const LEG_LABELS: Record<string, string> = {
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=<batchId> 列该批的引用。cursor 沿用既有键集分页。 */
export function regsTreeParams(projectId: string, opts: { parent?: string; cursor?: string } = {}): Record<string, unknown> {
const params: Record<string, unknown> = { projectId, kind: 'regs', cursor: opts.cursor ?? '' }
if (opts.parent) params.parent = opts.parent
return params
}
export const REG_TREE_KIND_LABELS: Record<string, string> = {
'reg-batch': '证据批',
'reg-citation': '引用',
}