Files
writer-work-flow/apps/web/lib/pace/beatmap.ts
Yaojia Wang 5fb7bfb1de feat: M3 — 伏笔账本 + 节奏引擎 + 大纲(含并发记账 bugfix)
- 伏笔账本:纯函数状态机(OPEN/PARTIAL/CLOSED/OVERDUE) + ForeshadowLedger repo;验收后到期扫描(BackgroundTask 自建 session 置 OVERDUE);登记/状态变更端点
- 节奏 + 三审齐:foreshadow-analyst + pace-checker 并入 LangGraph 并行审(REVIEW_SPECS),collect 分列落 chapter_reviews(conflicts/foreshadow_sug/pace),review SSE 加 foreshadow/pace 事件
- 大纲:outliner Agent 产 OutlineResult(含 foreshadow_windows),POST /outline 逐章 upsert outline 表;GET /foreshadow?status= 看板
- 前端:伏笔四泳道看板(OVERDUE 琥珀) + 大纲编辑器(窗口徽标) + 节奏节拍图(▁▃▅) + 审稿页消费 foreshadow/pace SSE
- bugfix(T3.8):并行三审共用请求 session 记账触发 'Session is already flushing' → foreshadow/pace 静默丢失;SqlAlchemyLedgerSink.record 改 add-only(靠端点/事务 commit),加并发回归测试
- M3 E2E:真实 DB + mock 网关零 token 走通 埋设→进展→验收后扫描 OVERDUE→看板 + 大纲含窗口 + 三审齐 SSE/留痕;E2E 暴露并钉住上述 bug
- 门禁绿:mypy 111 / pytest 228(0 xfailed) / alembic 无漂移;前端 gen:api/lint/tsc/vitest 69/build
2026-06-18 14:21:17 +02:00

54 lines
1.9 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.

// 爽点节拍图:把 pace.beat_mapint 序列)映射成 ▁▃▅ 风格的 sparkline 格子UX §6.4)。
// 纯逻辑,便于 node 环境单测;渲染时配合高度类与 prefers-reduced-motion。
// 七级火花字符(低→高),与高度档位一一对应。
export const SPARK_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇"] as const;
export interface BeatCell {
// 原始强度值。
value: number;
// 归一化档位 0..6(映射到 SPARK_CHARS / 高度百分比)。
level: number;
// 对应火花字符(▁..▇)。
char: string;
// 渲染用高度百分比12%..100%,最低也可见)。
heightPct: number;
}
const MIN_HEIGHT_PCT = 12;
const MAX_LEVEL = SPARK_CHARS.length - 1;
// 把 beat_map 映射成可渲染的格子序列。
// 归一化按「本序列 min..max」线性分桶到 0..6;全相等 → 全部居中档level 3
export function toBeatCells(beatMap: readonly number[]): BeatCell[] {
const values = beatMap.filter((v) => typeof v === "number" && Number.isFinite(v));
if (values.length === 0) return [];
const min = Math.min(...values);
const max = Math.max(...values);
const span = max - min;
return values.map((value) => {
const level =
span === 0
? Math.round(MAX_LEVEL / 2)
: Math.round(((value - min) / span) * MAX_LEVEL);
const clamped = Math.min(MAX_LEVEL, Math.max(0, level));
const heightPct =
MIN_HEIGHT_PCT + (clamped / MAX_LEVEL) * (100 - MIN_HEIGHT_PCT);
return {
value,
level: clamped,
char: SPARK_CHARS[clamped] ?? SPARK_CHARS[0],
heightPct: Math.round(heightPct),
};
});
}
// 文本版 sparkline无障碍/紧凑场景UX §6.4 示例 ▁▃▅▂▁▇)。
export function toSparkline(beatMap: readonly number[]): string {
return toBeatCells(beatMap)
.map((c) => c.char)
.join("");
}