/** * 任务视图与草稿层叠加 —— 纯逻辑,零 IO(编码规范 §7.2 / §5.3)。 * * 语义出处 OS 09 / AM 10.3: * 任务视图 = 正式版 + 本任务草稿层(**读己之写**:本任务可见、正式版不可见、不产生数据事件); * 环内推进读任务视图,不依赖事件;跨任务监听只认正式版事件。 */ import type { LedgerKey, LedgerRow } from '@contracts'; export type DraftEntry = { readonly key: LedgerKey; readonly patch: LedgerRow; readonly tombstone: boolean; /** 草稿建立时读到的基线值 —— 放行前的数据冲突检查用 */ readonly baseline: LedgerRow; }; /** 主键的规范化串:字段名排序后拼接,保证同一行的键恒等 */ export function keyOf(key: LedgerKey): string { return Object.keys(key) .sort() .map((k) => `${k}=${String(key[k])}`) .join('|'); } /** * 叠加:正式版行 + 本任务草稿 patch → 任务视图行。 * 草稿新增的行(正式版没有)也出现在视图里;tombstone 的行从视图中消失。 */ export function overlay( official: readonly LedgerRow[], drafts: readonly DraftEntry[], primaryKey: readonly string[], ): LedgerRow[] { const draftByKey = new Map(drafts.map((d) => [keyOf(d.key), d])); const seen = new Set(); const out: LedgerRow[] = []; for (const row of official) { const k = keyOf(pickKey(row, primaryKey)); seen.add(k); const draft = draftByKey.get(k); if (draft === undefined) { out.push(row); continue; } if (draft.tombstone) continue; out.push({ ...row, ...draft.patch }); } for (const draft of drafts) { const k = keyOf(draft.key); if (seen.has(k) || draft.tombstone) continue; out.push({ ...draft.key, ...draft.patch }); } return out; } export function pickKey(row: LedgerRow, primaryKey: readonly string[]): LedgerKey { const key: Record = {}; for (const field of primaryKey) { const value = row[field]; if (typeof value === 'string' || typeof value === 'number') key[field] = value; else if (value !== undefined && value !== null) key[field] = String(value); } return key; } /* ── 数据冲突检查(落地预检第五项,OS 7.3) ────────────────────────── */ export type ConflictFinding = { readonly key: LedgerKey; readonly field: string; readonly baseline: unknown; readonly official: unknown; readonly draft: unknown; }; /** * 草稿层所改字段与正式版的并发冲突:**他单已改同一字段即预检不过**, * 打回环内基于新正式版重拟。 * * 判据:草稿建立时读到的基线值 ≠ 当前正式版值 ⇒ 冲突。 * 只检查草稿实际修改过的字段 —— 没碰的字段被别人改了不算冲突。 */ export function detectConflicts( drafts: readonly DraftEntry[], officialByKey: ReadonlyMap, ): ConflictFinding[] { const findings: ConflictFinding[] = []; for (const draft of drafts) { const official = officialByKey.get(keyOf(draft.key)); if (official === undefined) continue; // 草稿新增的行,无从冲突 for (const field of Object.keys(draft.patch)) { const baseline = draft.baseline[field]; const current = official[field]; if (!sameValue(baseline, current)) { findings.push({ key: draft.key, field, baseline, official: current, draft: draft.patch[field], }); } } } return findings; } function sameValue(a: unknown, b: unknown): boolean { if (a === b) return true; if (a === null || a === undefined) return b === null || b === undefined; if (b === null || b === undefined) return false; if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime(); if (typeof a === 'object' || typeof b === 'object') return JSON.stringify(a) === JSON.stringify(b); return String(a) === String(b); }