状态:已接受(2026-09-13,批 6 波 A 落地;决策本体 = 规格 §8 + 控制方裁决 R0–R12 + 波 A 的代码与守卫) 🔻 批 6 收口就地加注(T35,2026-09-13,上面状态行原文一字未改):批 6 的 T1–T36 已全部落地(
ac504b06^..975b3285,77 个提交)⇒ 状态行的「批 6 波 A 落地」按「批 6 波 A–D 全落」读;本 ADR 的判据终态见下方「合规性验证」的收口加注与「后端契约例外」的定稿节。docs/adr/README.md索引的同步措辞见该索引的批 6 收口加注(R14.7 #4 要求把「波 A 落地」改述为进行中时的口径 —— 收口后以索引加注为准)。 关联:前端重设计规格(§3 红线 6(:127) · §8 全节(:489–:560)· §10 批 6 行(:658)· §11-4(:740)/ §11-5(:741)/ §11-9(:760)/ §11-10(:766)· §13(:811)· §14(:836))· 动效规范(本 ADR 的规范载体;判据纪律的机械输入)· ADR-034 · ADR-033 · ADR-032 · ADR-013 · 批 6 实施计划
规格 §14 的 ADR 表逐字登记「ADR-034/035 顺延至批 3 / 批 6」;批 3 / 批 5 已把 L2 壳层(ADR-034)与 L1 原语 / L3 视图(ADR-033)落成 durable 载体,而 L4 动效层在批 6 之前只有接缝、没有纲领:批 4 收口逐字声明「只有接缝、没有纲领(三档强度 / 双基调 / GSAP 全缺)」,app/src 里只有 [data-phase] 退场接缝与 app/src/ui/primitives/motion.css 的 10 个临时 token(批 0-D 的临时接缝,文件头逐字承诺「批 6 的动效 token 真源完成后整块删除」)。
⇒ 本 ADR 把架构级决策固化成 durable 载体(引擎选型与接入面 · 唯一入口与插件注册 · token 单一真源 · 三档 / 双基调 / 位移上限的实现层 · 判据纪律 · 一处红线例外)。三条事实驱动它:
- 规范先于代码(AGENTS.md §0.4):动效规范(批 6 T1,
bffed928)先落地,本 ADR 记录其上的决策,波 A 的 T3–T14 按它装配。 - 本批首次改动「零新增依赖」(R2.1 授权装
gsap@3.15.0+@gsap/react@2.1.2),也首次为规格 §3 红线 6 新增第三处例外(R0.1 第 1 条:用户裁决「B · 连承载面一起建」)。 - 手感的数值(三档倍数、编排时长、错开量)不属本 ADR 管辖 —— 它们是可 headless 抽检、不得升格为判据的手感参数;本 ADR 只管「在哪一层、由谁强制」。
- 版本逐字冻结:
gsap@3.15.0+@gsap/react@2.1.2。事实(R2.1 实测):零传递依赖 · peer =gsap ^3.12.5+react >=17· 许可Standard "no charge" license;2025 起 Webflow 令 GSAP 全插件免费(含Flip/ScrollToPlugin/CustomEase)。落点 =app/package.json的dependencies恰 +2(归 T3)。 - 分界规则(规格 §8.2 逐字表;执行口径,全文见动效规范):单属性 · 无时序 · <200ms ⇒ CSS transition;多元素 · 有时序 · 需中断/反向/seek ⇒ GSAP timeline;「从 A 布局滑到 B 布局」⇒ GSAP Flip;循环环境动效 ⇒ CSS keyframes(
@keyframes桶边界只留Loading/Skeleton/Probe)。 - 四层纲领(规格 §8.1 逐字:响应 80–180ms / 环境 2–6s 循环 / 编排 400–900ms / 生长 秒→天)是时间尺度与配重的唯一真源,本 ADR 不复制,只固化两条不可协商:① 第一优先 = 响应层 + 用户动作触发的编排层;② 环境层不得被降为纯背景(用户二次确认原话「交互时最重要,但闲置时也要有生命感(环境层别退太多)」,规格 §8.6.1(:552))。
- 本批是**「零新增依赖」的唯一一次破例**(批 2/4/5 的硬约束由 R2.1 授权破例一次);除这 2 个 npm 包外零新增依赖(Rust 侧
Cargo.lock692 包零变化)。
2. 全仓唯一 GSAP 入口 = app/src/motion/engine.ts,顶层必须 gsap.registerPlugin(useGSAP, Flip, ScrollToPlugin, CustomEase)(依据:R2.2 · 尖刺 S3.2 实测)
接口冻结快照(T3 逐字照此建;本 ADR 是插件集合的真源):
// 唯一允许静态 import GSAP 家族的文件
import { useGSAP } from "@gsap/react";
import { gsap } from "gsap";
import { Flip } from "gsap/Flip";
import { ScrollToPlugin } from "gsap/ScrollToPlugin";
import { CustomEase } from "gsap/CustomEase";
gsap.registerPlugin(useGSAP, Flip, ScrollToPlugin, CustomEase); // ← 顶层,不可省、不可条件化
export { gsap, useGSAP };🔴 为什么必须注册(测试底座独有的事实;尖刺 S3.2 的读数逐字抄在此处,因为 spike-gsap-runtime.md 在 .superpowers/** 里 gitignored、不入库):
- 不
registerPlugin时useGSAP()防不住 React 19 StrictMode 的双 tween —— 实测:裸useEffect= 2 个 tween,useGSAP()也是 2 个,卸载后transform残留。 - 根因 = gsap 双实例:
gsap的exports["."]把import → index.js(ESM)与require → dist/gsap.js(CJS)分成两个物理文件,而@gsap/react@2.1.2无exports、main指 UMD/CJS(内部require('gsap'))⇒ Vitest external 后@gsap/react拿到 CJS 运行时、应用拿到 ESM 运行时,两个_context⇒context.revert()静默空转、无任何警告。 registerPlugin(useGSAP)之后实测:StrictMode 下 1 个 tween、卸载后transform === ""且 tween 数 0。- 浏览器构建不受影响(Vite 认
module字段)—— 这是测试底座独有的坑,验收判据落在 T3 的engine.test.ts(StrictMode 单 tween + 卸载清净)与 T4 的守卫。
其余边界:registerPlugin 是全局副作用(进程级可见),且本文件只能经 await import() 到达(决策 3);插件集合恰 4 个、顺序逐字,engine.guard.test.ts 与本节做集合对拍。
3. 懒加载边界 = 「app/src/** 中静态 import gsap / @gsap/react 的文件集合 ∩ 首屏静态可达闭包 = ∅」(依据:R2.1 判据 ③ · R11.1 追认并加强)
- 槽位早已预留:
app/src/build/manualChunks.ts的EXACT已含gsap/@gsap/react两行(批 2 交付);G8 =app/src/build/manualChunks.test.ts:272由「此刻没有 gsap」改判为「恰是这两个」(按它自带的指引修,不是改断言)。 - 判据不是白名单(白名单易腐化:新文件一加就漏)而是闭包交集(结构性、有工具可算:复用
scripts/bundle-eager-graph.mjs的源文件集合)。三件套:① 产物出现vendor-gsap-*.js;②check-bundle-budget.mjs --no-build --json的firstScreen.chunks不含它(首屏 chunk 数仍 3,首屏 gzip 基线 100.49 kB);③ 上式的交集为空。 - 三条连带硬约束:①
engine.ts仍是唯一引擎装配文件(决策 2 不变);② 任何需要使用useGSAP的组件,其自身必须位于动态 import 链上 —— 首屏静态页(课堂页pages/ClassroomPage.tsx是静态的,批 2 交付形态「8 页懒 + 课堂页静态」)里的 GSAP 动画必须抽成React.lazy子件;③ 派生禁令:@gsap/react不得被任何首屏静态模块静态 import(它是vendor-gsap成员,一旦进闭包就等于 GSAP 进首屏)。 - 落点 = T4 的
app/src/motion/engine.guard.test.ts:闭包交集是无条件可跑的硬判据;①② 是runIf(existsSync(dist))的产物用例 + 真实构建强制命令行 —— 🔴dist不入库 ⇒ 在导出树里 ①② 不运行:未运行 ≠ 通过,必须计入报告的「未验证」单列(R11.5)。
🔻 批 6 收口就地加注 · 决策 2 快照注释与决策 3 判据的两种口径**(T1 评审 I-1;2026-09-13,上面原文一字未改):决策 2 的冻结快照首行
// 唯一允许静态 import GSAP 家族的文件是诊断读数口径**(易腐化:新文件一加就漏),判据本体是决策 3 的闭包交集 = ∅;engine.ts的文件头已按 T3 逐字写成「这是闭包判据(ADR-035 决策 3)的派生结论,不是判据本身」⇒ 两处口径并存是有意的,读到快照注释时不得把它当成判据。 🔻 批 6 收口就地加注 ·controls.ts的适用面(R41.3 经 R61 重述;2026-09-13):规则不是「controls.ts只许经await import()到达」(那是一句过宽的绝对规则 —— 该措辞今天仍在app/src/motion/controls.ts的文件头,属波 D 标签清扫的待改项,本批只登记不改代码),而是「不许从「首屏静态可达」的模块静态 importcontrols.ts」(它内部静态import { gsap } from "./engine")⇒ 惰性视图(React.lazy/ 注册表驱动)静态 import 它是安全的(波 C 的 T27/T29/T31 正是这条路)。
- 10 个变量逐字迁入(值一字不改):
DURATION_TOKENS× 9(micro120 ·overlay-in200 ·overlay-out160 ·toast-in180 ·toast-out140 ·skeleton1200 ·card220 ·reveal500 ·page150)+EASING_TOKENS× 1(--ed-ease: cubic-bezier(0.2, 0, 0, 1),出场比进场快)⇒ 产物app/src/ui/tokens.css(:root)+tokens.gen.ts(导出DURATION_TOKENS/EASING_TOKENS/MOTION_TOKENS)。 motion.css不得再定义任何--ed-*:临时:root{}块整块删除(T5 已落:motion.css74 → 54 行,--ed-*定义实测 0)。生成物永不手改:改真源必须重生成并同提交带两个产物。- 判据落点(已落):
app/src/ui/primitives/style-seams.test.ts(G4:motion.css的--ed-*定义恒 0 的回归封条 + 真源侧计数 9/1 + 逐名逐值改读renderAll().css)·app/src/ui/tokens.drift.test.ts(产物 ==renderAll())。 pill档:radiusScale第 5 档{ name: "pill", px: 999 };999 取自既有原语层兜底值(全仓唯一命中app/src/ui/primitives/Surface.css:58的var(--ed-radius-pill, 999px)),不许自创 ⇒ G9(tokens.drift.test.ts:55→[3, 5, 8, 10, 999])与 G14(app/scripts/gen-tokens.test.mjs:122+:132-136的真源 ↔ 消费者同值对拍)两处独立真源断言必须同任务同提交处理。- 为什么不在
motion.css定义--ed-*(也不引--ed-私有档位变量):与 G4 的新判据直接冲突;T6 的档位块因此直接覆写属性,不新增任何--ed-*。
5. 强度三档 = <html data-motion="eco|standard|rich">;系统 prefers-reduced-motion 优先于档位(源序)(依据:规格 §8.5(:526)· R3.1–R3.3)
- 载体 / 取值逐字:
data-motion写在<html>上,取值"eco"|"standard"|"rich",默认"standard";持久化键逐字motion:intensity(照app/src/views/useViewMemory.ts范式:三纯函数 + 注入Storage+globalThis.localStorage默认值 + 惰性读取 + 静默降级);初值 = 跟随系统(prefers-reduced-motion: reduce⇒"eco",否则"standard");matchMedia必须自带守卫(typeof window.matchMedia !== "function")。 - 🔴 源序是优先级实现:CSS 里后写覆盖 ⇒ 档位规则块必须写在 reduced-motion 块之前;顺序写反 = 用户选的档盖掉系统无障碍设置,违反规格 §8.6.1(:552)第 4 条「『活』不得以无障碍为代价」。新增守卫断言行号关系(
app/src/ui/primitives/motion-coverage.test.ts,归 T6,带变异体)。 - 必须有可切换的 UI 入口(否则「三档正确」不可验收):
SettingsPage的「动效强度」三段控件(新组件app/src/components/MotionIntensityControl.tsx,归 T7;复用ViewSwitcher原语以满足「ed-btn段控件类名空间 +aria-pressed+ 零行内 style」且不触nativeButton.ratchet的域)。 - 不在本 ADR 管辖:三档的具体倍数(手感参数);跨窗口同步(
?float=1/?overlay=1各自独立,登记)。
🔻 批 6 收口就地加注 · 「源序是优先级实现」的机理更正(R18.1 的因果更正;2026-09-13,上面原文一字未改):上段的「CSS 里后写覆盖 ⇒ 档位规则块必须写在 reduced-motion 块之前」把源序当成了承重机理,因果写反。实测(T12 的 I-2 / I-3):真正让系统赢的是 reduced-motion 块的每条声明都带
!important(档位块不带)—— 按 CSS Cascade,即使把两块顺序对调,reduced 仍然赢。⇒ 本决策的结论不变(prefers-reduced-motion优先于档位),但性质改为「防御性钦定」:源序判据守的是「reduced 块带!important、档位块不带」这两条前提被无意改动。🔴 不得给档位块加!important(那会反转无障碍优先级,违反规格 §8.6.1 第 4 条)。两条机器判据:app/src/ui/primitives/motion-coverage.test.ts的源序 describe(行号比对)+app/src/motion/responseSeams.test.ts的 I-2(逐条声明带!important)/ I-3(两块之间零规则块)。 🔻 批 6 收口就地加注 ·motion.css的块序已成硬事实(R49.1①;2026-09-13):计划 V3 的「相位块可写在档位块之后」被实测改判 —— 相位块必须在档位块之前(实测行号 191 < 210 < 260):①responseSeams.test.ts的 I-3 要求档位块与 reduced 块之间零规则块;②motion-coverage.test.ts的名单抽取口径是「reduced 块 → EOF」⇒ 写在 reduced 之后会被判死条目。⇒ 终态块序 = 相位块 → 档位块 → reduced-motion 块(变异体 m3 双捕获,且 m1b 首轮注入落进 CSS 注释体被扫描器正确忽略 ⇒ 注入点自证)。
- 载体 =
data-tone,取值逐字"instrument"|"paper";每个动效落点显式声明基调(instrument= 有「读数」的界面:采集 / 复习 / 时间轴 / 到期刻度;paper= 有「文字」的界面:笔记 / 会话 / 体系)。禁止在 CSS 里用类名区分基调(会与既有ed-*类名空间冲突)。 - 缓动落点 =
tokens.gen.ts的两个缓动 token(精密仪器power3.inOut族,匀速段更长、沿轴线带刻度感 / 活的纸power2.out+ 自定义「洇开」曲线,带惯性沉降、不是回弹);GSAP 侧经CustomEase.create()注册具名 ease,默认 ease 不承担基调语义。归 T8(token + 具名 ease + 单调性判据)。 - 「惯性沉降」= 减速到停(无过冲、无二次反方向运动);「回弹」属规格 §8.7 明确不做的三类之一。
- 判据域 = 全仓
app/src/**的translate*()数值实参 +motion/层导出的位移常量;> 8px 即红。实测支撑:全仓translate*()18 处(数值实参 10 处),> 8px = 0 处(最大Modal.css:52/Toast.css:64的translateY(8px))⇒ 扩域零存量白名单可行。 - 🔴 不判
margin/left/top的 px 字面量:实测 > 8px 的 4 处全是布局量(app/src/components/NotePreviewView.tsx:64的margin:10px·app/src/shell/TopBar.test.tsx:215与app/src/ui/primitives/Toast.placement.test.tsx:103-104的top: 64px),拉进来只造假阳性;而「动画不得动 layout 属性」已由决策 8 的属性集合审计单独覆盖 ⇒ 无漏洞。 - 位移不做成 CSS 变量:没有消费者就是死变量(还会给人「已被强制」的错觉)。强制手段是机器判据。归 T9(唯一出口 + 扩域守卫)。
正文(含被守卫逐字复用的字符串)在动效规范的「判据纪律」节,本 ADR 不复制全文,只固化四条不可协商的边界:
- 确定性推进的唯一正解 =
gsap.timeline({ paused: true })+tl.time(t);🔴 四个禁用项 =gsap.updateRoot·gsap.ticker.tick·gsap.ticker.sleep·await sleep(真实定时器 / fake timers)。测试里的推进调用只能是正解。 - 可中断 / 可反向必须双断言:同时断 tween 计数(
gsap.globalTimeline.getChildren().length,或旧 tween 的totalTime()冻结)与目标元素style.transform—— 只看style.transform会假绿(GSAP 3 默认overwrite:false⇒ 覆盖同属性时旧 tween 仍在跑)。 - 「60fps」在本环境不可判 ⇒ 代理判据 = 被动画属性集合 ⊆
{transform, translate, rotate, scale, opacity, filter}(即未动 layout 属性)。需要几何变化的动效走 transform 路径(Flip 用scale: true)—— 让实现去满足判据,不放宽判据去迁就实现(R12.3)。🔴 任何收口不得出现「60fps 已达成」类表述。 - 只能登记、不许编造弱判据的清单(durable 落点):真实 60fps · Flip 几何位移(
Flip.getState的bounds全 0、位移增量恒translate3d(0px,0px,0px))· 真实媒体播放 ·window级滚动(ScrollToPlugin的window目标读数恒 0)· 真机 / WebView2 观感 · 视图密度观感 · 切视图卡顿 · 惰性挂载的运行时内存效果 · 暗档(data-theme)实际生效。
结构锚点四个(凡能落成 DOM 结构 + data-* + 类名 + 属性值的判据优先于时间判据,R8.5):data-motion · data-tone · data-shell-phase · data-phase。两条 API 陷阱:tl.to() 返回 Timeline 本身不是 Tween(取句柄用 tl.to(...).getChildren() 或 gsap.to);绝不可把 jsdom 的 performance 挂到 globalThis(自调用栈溢出)。
依据:R0.1 第 1 条逐字 —— 「规格 §3 红线 6『后端数据模型零改动』新增第三处例外,必须:① 规格 §3 就地加注(带原因/影响面/回滚);②
ADR-035单列『后端契约例外』节;③cargo test --test app_lib_tests本批必须真跑」。规格 §3 的既有两处例外(E1MemoryEntry.tier/ E2tag_colors种子)是先例不是授权(规格 §12 逐字「例外须各自登记原因/影响面/回滚;新增例外需再次裁决」)。 🔻 本节状态(批 6 收口定稿 · T35,2026-09-13 —— 本节自本提交起是「已完成」状态):授权已下、三条例外的形态由三份探针回收裁决定稿(PB1probe-audio-availability.md· PB2probe-fsrs-interval.md·probe-audio-runtime.md);波 B(T20 / T22 / T23)落地后的五项读数已全部回填(本节写作时挂账的那五项,逐条如下):
- 命令注册计数实测值 = 313 / 313 / 0(
node scripts/check-command-registry.mjs,exit 0:定义 313 / 注册 313 / 重复 0)——session_audio_path是本批唯一新增 IPC(312 → 313)。- sidecar 字段终名 =
{id}.wav.meta.json,键逐字冻结为version/aligned/firstTsMs/samplesWritten(T23 落audio_store.rs的finalize)。本批不改名(改名会让读侧静默回落false);若将来确需改名 ⇒ 必须同提交改读侧。aligned初值语义终稿 = 「不能保证对齐」(不是「一定没对齐」):时间戳缺失 / 为负 / 回退 / 超大空档之一 ⇒ 永久置false并退回纯追加(不补静音、样本一个不丢、不阻断会话主链路)。本修复之前录的 WAV 没有 sidecar ⇒aligned = false⇒ UI 对无基准的录音不得假装精确;🔴 禁止用恒为true的字段充当该标记。MAX_GAP_MS的授权值与理由 = 10 分钟(600000 ms):一段 1 小时静默若按空档补静音会写 ~115 MB 静音 ⇒ 超大空档宁可不对齐,也不写巨量静音(超限 ⇒None+aligned = false)。cargo test逐字读数 =cd app/src-tauri; cargo test --test app_lib_tests→running 2335 tests→test result: ok. 2329 passed; 0 failed; 6 ignored; 0 measured; 0 filtered out; finished in 8.90s,exit 0(R0.1 第 1 条③「本批必须真跑」由此兑现;读数出处task-34-report.md§3)。- E3 的「①域(精确值)不落库」(R17.5 逐字回填):DB 无 interval 列、写库只有
db_flashcards.rs的update_card_schedule(UPDATE flashcards SET state_json = ?1, due_at = ?2)⇒ 精确值只在review_card的返回体里(当场用outcome.interval_days显式覆写 —— 该函数用..card结构体更新语法,不覆写会漏出旧值),之后读库只能得整天值(row_to_card由dueAt − stateJson.lastReviewMs反推max(round(…),1))。 共同约束(三条例外逐条适用):只读 / 不新增表 / 不改既有字段与既有 SQL 文本的语义 / 零迁移(flashcards建表后零ALTER TABLE;CARD_COLUMNS不动);都不触碰 AGENTS.md §10 的「SQLite schema / 迁移」面。
| # | 例外 | 原因 | 影响面 | 回滚 | 依据 |
|---|---|---|---|---|---|
| E3 | Flashcard += 只读字段 intervalDays(camelCase;Rust 4 文件 / 5 处) |
真实间隔前端今天拿不到,但后端已经算出来了:scheduler.rs:69 的 ScheduleOutcome{…, interval_days: f32, …} 与 :99 的 max(1.0) 都算过,而 review_card 的返回构造(PB2 读数 commands_flashcards.rs:206-210;Ok(Flashcard { state_json, due_at, ..card }))只返回 state_json / due_at,写库只有 db_flashcards.rs 的 update_card_schedule(UPDATE flashcards SET state_json = ?1, due_at = ?2)⇒ DB 无 interval 列、stateJson 键逐字只有 stability/difficulty/reps/lapses/lastReviewMs。而 stateJson 的契约逐字是「后端调度契约,前端透传不解析」⇒ 前端不得用 dueAt 差分或字符数自造「真实间隔」(AGENTS.md「不发明数字」) |
Flashcard 是 6 条命令的共享返回类型 ⇒ 字段会同时出现在 list_due_cards / review_card / quiz_group_cards / create_model_card / list_group_cards / model_card_from_note 的返回里;本批只消费前两处,其余四处如实登记为「顺带变化」。前端 app/src/types/notes.ts 加可选 intervalDays?: number(零夹具改动)。🔴 两个精度域必须在类型注释里显式声明:① review_card 路径 = 精确值(当场用 outcome.interval_days 显式覆写 —— 该函数用 ..card 结构体更新语法,不覆写会漏出旧值,这是必须点名的陷阱);② 行派生路径 = 整天粒度(row_to_card 由 dueAt − stateJson.lastReviewMs 反推 max(round(…),1),是唯一能给 5 个查询方法一次供值处)。UI 上「答『忘了』则回缩」必须用 ① 的精确值 |
删字段 + 还原 5 处(types_note.rs · db_flashcards.rs×2 · commands_flashcards.rs · types_contract_tests.rs 的追加行)+ 删前端可选字段。零迁移、零 SQL 变更 ⇒ 回滚不影响任何已落库数据 |
R5.4 的 PB2 回收裁决(含 G11 = types_contract_tests.rs:34 的 assert_wire! 只许追加、不许删除任何既有字段/键) |
| E4 | 新增只读 IPC session_audio_path(命令注册 312 → 313) |
音频真的落盘且已接线,但前端拿不到路径:audio_store.rs(212 行)写 {data_dir}/session-audio/{session_id}.wav(16 kHz 单声道 PCM16,44 字节 RIFF 头,~115 MB/小时);创建 live_session.rs:258-262 · 每 200 ms 写块 live_session_loop.rs:162-164 · finalize :391-397。播放三件套已现成:app/src-tauri/tauri.conf.json:24 的 CSP 已含 media-src 'self' asset: http://asset.localhost blob:,:25-28 的 assetProtocol.enable=true + scope:["$APPDATA/**"] 覆盖 app_data_dir ⇒ 🔴 本批零 tauri.conf.json 改动(AGENTS.md §10 的该审查面不触发) |
本批唯一的新增 IPC:入参 session_id: i64;出参 Option<SessionAudioRef>(字段只许 path / aligned / durationMs —— aligned 自证量必须到 UI,Option<String> 装不下);注册点 app_commands.rs:335 邻近;计数 312 → 313(定义 + 注册各 +1);SessionAudioRef 必须进 types_contract_tests.rs 的 assert_wire!(新增一行断言,不是改判)。🔴 安全三条(AGENTS.md §4):① 必须校验入参;② 必须把路径限定在应用数据目录内(不许把任意路径透传给 asset protocol ⇒ 目录穿越);③ 不许前端 fetch() 整取音频(不带 Range 的 200 分支会把 115 MB 读进内存)⇒ 只许 <audio src>(硬判据)。%TEMP% 且导入结束即 remove_dir_all、sessions 表无源视频路径列 ⇒ 对导入会话本命令恒 Ok(None),UI 必须如实区分 |
删命令 + 删注册行 ⇒ 计数回 312;前端删调用点即回到「只读时间轨、无音频播放」。零依赖变更(Cargo.lock 692 包零变化)、零 tauri.conf.json 改动 |
R5.5 的 PB1 回收裁决 + R12.2(出参形状追认) |
| E5 | WAV 轴 ≡ 会话轴:写块路径按 chunk.timestamp_ms 的空档等长补静音 + 失效安全 + aligned 自证量 |
今天 segments[].start_ms / 1000 不能当 audio.currentTime:start_ms 是会话纪元墙钟 − total_paused_ms,而 WAV 轴 = 已写样本数 ÷ 16000、从第一个被捕获样本起纯追加、无任何静音填充。Stop ↔ 时间戳冻结严格抵消;暂停期 write_chunk 完全不调用,不写 0 字节、不写静音);间隙来自静默窗根本不产包 ⇒ 偏差清单里 D2 最危险且无上界(对大量静默的课程录音,漂移可达分钟级)。⇒ 采根治方案:「只暴露 first_sample_ms + samples_written」的最小方案只修 D1、会交付一个「看起来对、实际会漂」的播放头,违反「标签不许说谎」 |
live_session*.rs(AGENTS.md §10 名单内 ⇒ 该任务必须显式执行 §10 审查记录)· audio_store.rs(write_chunk 加 timestamp_ms: i64 入参 + 对齐簿记 + finalize 写 sidecar {id}.wav.meta.json)· live_session_loop.rs:163 一个调用点 · 新纯函数 audio_align.rs + audio_align_tests.rs · audio_store_tests.rs 的既有 write_chunk 调用点改签名(必要连带:只改调用形态、不改任何期望值)· audio_store.rs:3 的 @ai-context 就地更正(旧文「实时链路当前不落盘」自相矛盾)· ADR-013 就地加注(新增「WAV 轴与会话轴的对齐」一节)。失效安全语义:时间戳缺失 / 非单调 / 超大空档(> MAX_GAP_MS)⇒ 退回今天的纯追加并置 aligned = false(绝不静默分歧),不丢样本、不阻断会话主链路。历史录音必须如实区分:本修复之前的 WAV 轴不保证对齐 ⇒ UI 对无基准的录音不得假装精确;🔴 禁止用恒为 true 的字段充当该标记 |
还原 1 个签名 + 1 个调用点 + 删 2 个新文件 + 停写 sidecar(补静音只影响新录音,既有 WAV 一字不动);aligned 字段随命令一同删除。零新增依赖、零迁移、零新数据面 |
R5.5-b 的三条裁决(根治方案 / 纯函数 + 失效安全 / aligned 自证量)+ 可行性复核(chunk.timestamp_ms 今天就在写块路径手边) |
- 正面:L4 动效层首次有了 durable 契约 —— 引擎与接入面、唯一真源、三档与双基调的载体、位移上限的强制手段、判据纪律与「只能登记」清单,都有单一出处可引;批 7/8 及后续动效工作不必再回到批次台账(
.superpowers/**,不入库)。规格 §3 红线 6 的第三处例外有了 durable 记录(此前只有 E1/E2 两个先例)。 - 负面 / 代价:
① 本 ADR 的多数判据落点在写它时尚未存在(T4/T6/T8/T9/T11/T13/T14 才建)⇒ 合规性验证表逐行标状态,「已落」与「归 T#」必须分开读,否则会误读成「判据齐备」。
② 测试底座独有的双实例坑(决策 2)是代码形态约束(顶层
registerPlugin),不是可选项;把它当「风格问题」会在 StrictMode 下静默产生双 tween。 ③ 三档 / 双基调的观感不可机器判据 ⇒ 「三档正确」的机器面只有「档位映射 + DOM 属性 + 源序」三件,观感面进「只能登记」清单。 - 风险:
① 新落点的类名形状:必须是
<既有基类>--<修饰>(motion-coverage.test.ts:113-124的选择器域过滤)—— 新起一个.ed-*基类名会当场红;T13 的环境层与 T12 的响应层必须照此。 ②@keyframes桶边界(只留Loading/Skeleton/Probe):新循环动效只能写进motion.css(Loading.css/Button.css/StatusLine.css三处均被既有守卫禁止新增),且每一处animation声明的选择器原文(含伪元素)必须逐字进 reduced-motion 名单(G7 不动)。 ③aligned的语义是「不能保证对齐」而非「一定没对齐」(历史录音无法判定 ⇒ 取保守值)—— UI 文案不得说「这条录音没有对齐」。
- 自研 timeline:否决。规格 §8.2 已定分界规则(Flip / 中断 / 反向 / seek 是签名动效的硬需求),自研等于重写一套调度器 + 清理语义,且无 StrictMode 清理的既有解。
motion/framer-motion(含其它 React 动画绑定):否决。绑定在 React 树上、对 Flip(跨布局形态变化的原子能力)无对等物;本批的分界表要求「从 A 布局滑到 B 布局」走 Flip。- 把
engine.ts静态 import 进首屏 / 只维护一份「静态 import 白名单」:否决(R11.1)。白名单易腐化(新文件一加就漏),且会把 GSAP 拉进首屏闭包 —— 与 R2.1 判据①②(vendor-gsap独立懒 chunk、firstScreen.chunks不含它)直接冲突。取闭包交集判据:结构性、已有工具可算。 - 把 8px 做成 CSS 变量 / token:否决。没有消费者就是死变量,且会给人「已被强制」的错觉;强制手段是机器判据(决策 7)。
- 把
margin/left/top的 px 字面量拉进位移判据:否决(R11.4)。实测 4 处 > 8px 全是布局量 ⇒ 假阳性;动画不得动 layout 属性由属性集合审计单独覆盖。 - 用类名区分双基调:否决(R3.4)。会与既有
ed-*类名空间冲突;载体用data-tone属性 + 两个缓动 token。 - 把时长 / 缓动留在
motion.css双写(或在motion.css定义--ed-*私有档位变量):否决(R1.1)。临时接缝的文件头逐字承诺「真源完成后整块删除」;单一真源、绝不双写;T6 的档位块改为直接覆写属性。 - 把
ADR-035并入批 8 治理收口 / 并入ADR-034:否决(同 ADR-034 的处置理由)。批 6 正是 L4 动效批,AGENTS.md §11 要求「任何技术栈/架构级变更先写 ADR」;ADR-034的管辖面是 L2 壳层,合写会让引擎与判据纪律被壳层细节稀释。 - E3 走「前端用
dueAt差分自造真实间隔」:否决(AGENTS.md「不发明数字」+stateJson的「前端透传不解析」契约)。 - E4 出参用裸
Option<String>:否决(R12.2)。装不下aligned自证量;把aligned塞进第二条命令会破坏「本批唯一新增 IPC」与「312 → 313」两条硬约束。 - E5 走「最小方案:只暴露
first_sample_ms+samples_written」:否决(R5.5-b)。只修 D1,而 D2 无上界 ⇒ 交付一个会漂的播放头;逐块(timestamp_ms, sample_offset)索引登记为日后可能的精度升级,本批不做。 - E5 用「继续收但丢弃」代替端点
Stop实现暂停:否决(既有语义)。ADR-013逐字「端点 Stop、不喂 ASR、不写 WAV、时间戳补偿」;改语义不在本批范围。
口径同 ADR-034:每一条决策都要能在已提交的树上被判据钉住;「落点」= 断言所在文件,不是「注释里提过」。
⚠️ 本 ADR 写于波 A 进行中 ⇒ 状态列必读:✅ 已落= 该判据已在树上可跑;⏳ T#= 由该任务建立(此刻读作「已定形态、未见判据」)。
| 决策 | 断言 | 落点 | 状态 |
|---|---|---|---|
| §1 引擎版本冻结 + 分界 + 四层配重 | 规范正文(四层表 / 分界表 / 配重)逐字在位 | docs/standards/motion.md(「引擎与分界规则」「四层纲领」节) |
✅ 已落(bffed928) |
§2 唯一入口 + 顶层 registerPlugin(恰 4 个) |
StrictMode 单 tween · 卸载后 transform === "" 且 tween 数 0 · 四插件真的注册 |
新建 app/src/motion/engine.ts + app/src/motion/engine.test.ts |
⏳ T3 |
| §3 懒加载边界(闭包交集 + 三件套) | 静态 import 文件集合 ∩ 首屏静态可达闭包 = ∅(含两个阳性对照)· 产物 vendor-gsap-*.js · firstScreen.chunks 不含它 |
新建 app/src/motion/engine.guard.test.ts + G8 = app/src/build/manualChunks.test.ts:272 改判 |
⏳ T4 / T3 |
§4 token 单一真源(9 + 1 + pill) |
motion.css 的 --ed-* 定义恒 0(回归封条)+ 真源侧计数 9/1 + 逐名逐值 == motion.css 原值 |
app/src/ui/primitives/style-seams.test.ts:166-188(G4) |
✅ 已落(T5) |
§4 pill = 999(真源 ↔ 唯一消费者同值) |
[3, 5, 8, 10, 999] + == Surface.css:58 的兜底字面量 |
app/src/ui/tokens.drift.test.ts:55(G9)· app/scripts/gen-tokens.test.mjs:122/:132-136(G14) |
✅ 已落(T5) |
| §4 生成物不手改 | 产物 == renderAll()(真源 → 产物漂移守卫) |
app/src/ui/tokens.drift.test.ts:25-28 区段 |
✅ 已落(批 0-D,T5 后仍绿) |
| §5 三档载体 / 初值 / 静默降级 | data-motion 恰 ["eco","standard","rich"] · motion:intensity 键逐字 · 无 matchMedia 不抛 · Storage 抛静默降级 |
新建 app/src/motion/intensity.ts + intensity.test.ts |
⏳ T6 |
| §5 源序:系统 reduced-motion 优先于档位 | 档位块行号 < reduced-motion 块行号(两块都在) | app/src/ui/primitives/motion-coverage.test.ts(新增 describe) |
⏳ T6 |
| §5 可切换 UI 入口 | 三段控件 + aria-pressed + 零行内 style + 无裸 <button> |
新建 app/src/components/MotionIntensityControl.tsx + SettingsPage.tsx |
⏳ T7 |
| §6 双基调载体与具名 ease | 两个缓动 token 落 tokens.gen.ts;CustomEase.create 后 parseEase(name) 是 function |
app/scripts/gen-tokens.mjs + 生成物 2 件 + app/src/motion/tone.ts + engine.ts |
⏳ T8 |
| §7 位移 ≤ 8px(唯一出口 + 扩域) | translate*() 数值实参 > 8px = 0;motion/ 层常量 ≤ 8 |
app/src/ui/primitives/style-seams.test.ts(扩域)或新建位移守卫 |
⏳ T9 |
| §8 确定性推进 / 双断言 / 属性集合 | 推进唯一正解 + 四禁用(底座)· 双断言模板 · 被动画属性集合 ⊆ transform 族 | 新建 app/src/test/motionHarness.ts · app/src/motion/controls.ts |
⏳ T10 / T11 |
| §8 判据纪律的机械输入 | 8 个纪律串逐字在位(守卫逐字复用) | docs/standards/motion.md 的「判据纪律」节 + T10 的守卫 |
✅ 规范已落 / ⏳ T10 守卫 |
| 例外 E3 | 线格式含 intervalDays 且既有 10 键逐字仍在(只许追加)· 精确值覆写 · 派生整天粒度 |
app/src-tauri/src/types_contract_tests.rs:34(G11)· flashcards 相关 Rust 单测 |
⏳ T20 |
| 例外 E4 | 无音频 ⇒ None · 非法 id ⇒ Err · 路径越界 ⇒ Err · aligned 不得恒 true · 312 → 313 |
app/src-tauri/src/commands_audio.rs + 单测 · scripts/check-command-registry.mjs |
⏳ T22 |
| 例外 E5 | 空档补静音字节数逐字算式 · 时间戳缺失/非单调 ⇒ 纯追加 + aligned = false 且样本一个不丢 · 批量 ≡ 增量对拍 |
新建 app/src-tauri/src/audio_align_tests.rs + audio_store_tests.rs |
⏳ T23 |
| 例外三件套 | ① 规格 §3 就地加注 ② 本节已定稿(五项读数回填) ③ cargo test --test app_lib_tests 真跑 |
规格 §3(:127-134)· 本节 · cargo test 读数 |
✅ 已落(① ② = T35 收口提交;③ = T20/T22/T23 落地 + T34 真跑 2329 / 0 / 6,exit 0) |
🔻 批 6 收口就地加注 · 上表状态列的终态(T35,2026-09-13,上表原文(除末行的「已定稿」措辞外)一字未改):上表写于波 A 进行中(
:136的口径逐字「⏳ T#= 由该任务建立,此刻读作「已定形态、未见判据」」)⇒ 收口时全部判据均已落树,逐行终态: §1 规范正文 ✅(bffed928,T1)· §2engine.ts+engine.test.ts✅(T3:StrictMode 单 tween / 卸载后transform === ""且 tween 数 0 / 四插件行为级注册)· §3engine.guard.test.ts+ G8 ✅(T4 + T3;真实构建后复跑 11/11 passed、exit 0,产物有vendor-gsap-BQLVn3Z6.js105,584 B 且firstScreen.chunks不含它)· §4 token 真源三行 ✅(T5)· §5 载体 / 初值 / 源序 ✅(T6)· §5 UI 入口 ✅(T7,MotionIntensityControl复用ViewSwitcher)· §6 双基调 ✅(T8)· §7 位移 ✅(T9)· §8 底座 / 出口 ✅(T10test/motionHarness.ts+ T11motion/controls.ts)· §8 判据纪律的机械输入 ✅(8 个纪律串逐字在docs/standards/motion.md的「判据纪律」节并被守卫复用)· E3 / E4 / E5 ✅(T20 / T22 / T23,读数见本 ADR 的「后端契约例外」节)· 例外三件套 ✅(见末行)。
- 🔴 末行的语义翻转(诚实登记,R63 / 批 6 计划 T35 的 V3):上表末行原写「本节 挂账 → 定稿」,描述的是尚未回填的中间态 ⇒ 收口后该措辞必然失效,本加注点名这条改判并把该行的正向判据改为:「后端契约例外」节含
intervalDays/session_audio_path/aligned三个串 + 五列表头齐备 + 五项读数在场(313/313/0 · sidecar 键名 ·aligned语义 ·MAX_GAP_MS= 10min ·cargo test2329/0/6)。
ADR-019缺号:docs/adr/实盘最高 ADR-034(本 ADR 落库后最高 = ADR-035),编号序列里 ADR-019 缺号 ⇒ 🔴 不补、不复用,只登记(编号规则逐字「编号一经分配不再复用」,docs/adr/README.md的「编号规则」节)。- 暗档
data-theme接线 → 批 7/8(R2.5):app/src/ui/tokens.css:92定义了[data-theme="dark"](⚠️ R2.5 / 批 6 计划表 2 写的:79是 T5 迁移前的读数;T5 把 12 行时长 / 缓动 token 插进:root后该块下移到:92,本 ADR 取迁移后的行号),全仓无写入方(setAttribute("data-theme"/dataset.theme =0 命中)⇒ 暗档今天永不生效;主题靠 8 处matchMedia("(prefers-color-scheme: dark)")一次性读取、无订阅。本批零动作;批 6 新增的动效 CSS 一律var(--ed-*)、不得假设亮档、不得用[data-theme]做前提。 ink-4与规格 §4.3 条件③(审校模式)→ 批 7/8(R6.2):ink-4保持 0 生产调用点;条件③逐字「进入『审校模式』时全部升到 ≥4.5:1」,而今天没有这个模式。Text字号越界 551 处 / 120 文件 → 批 8 治理收口(R6.3):本批只保证「新代码不新增越界」。pinnable的消费方 → 本批波 C 的列折叠 Flip(规格 §6.2 该行的「接线拖拽+记忆或删钩子」):代码权威 =app/src/shell/columnRegistry.ts:50的notes-outline=true、其余 12 行false(ADR-034 §2 加注① 已就地更正规格)。- 五类棘轮余量 ·
FROZEN_NUMERIC_ZINDEX·bundle-eager-graph的import type口径 → 均非本批(R6.10,去向照docs/versions/v0.22.md原表:批 5/7、批 8)。 docs/tech-debt/(191,980 B / 1555 行,未入库未忽略)→ 挂起、待用户裁决(R6.9):本批零动作。🔻 前向指针(2026-09-13 · 批 8 收口;上面那条原文一字未改,按当时时态读):R6.9 的挂起已由用户裁决结清 —— 2026-09-13 用户裁决 U3 = a ⇒ 归档到
docs/archive/,落入docs/archive/2026-09-13/(README.md/tech-debt.md/review-2026-09-11.md);「挂起、待用户裁决」按当时时态读,本指针不改任何既有结论。- 只能登记(仪器不可达):真实 60fps · Flip 几何位移 · 真实媒体播放 ·
window级滚动 · 真机 / WebView2 观感 · 视图密度观感 · 切视图卡顿 · 惰性挂载的运行时内存效果 · 暗档实际生效 —— 清单本体在决策 8,收口报告与docs/versions/v0.22.md的批 6 节各带一份。 - 本 ADR 不管辖的邻近项:相变两态(规格 §6.3)与列折叠 Flip 的结构改造(「先改结构、再上 Flip」,8 处
ColumnBar落点)· 6 个签名动效的逐条形态(R5.1–R5.7,含 #2 的字符率近似「规格未定义语速函数」)·usePresence本批零改动(R2.4)· 多窗口间的档位同步 ·%TEMP%导入会话无音频承载面 · sidecar 孤儿文件(cleanup只扫.wav)· 「删除会话不删音频」—— 均归批 6 的波 B/C/D 或后续批次,见批 6 实施计划。
🔻 批 6 收口就地加注 · 登记节的本批补充(T35,2026-09-13,上面各条原文一字未改):
- 列折叠 Flip 的最终形态 = 控制方授权的「路径 B」(
data-flip-id跨元素匹配、零既有断言改动):落地 2 / 8 处(components/ChatSidebar.tsx:80·pages/GoalsPage.tsx:42),其余 6 处未接线;🔴Flip会写width/height(layout 属性)⇒ 与决策 8 第 3 条的属性集合判据天然冲突 —— 如实登记为「属性集合判据在 Flip 上不成立」,不得为了让它变绿而放宽 R8.4(批 6 计划「待裁决清单」#11 的裁决落地)。- 两条硬边(后续任务必读):①
app/src/App.tsx已到 599 / 600(余 1) ⇒ 任何后续任务不许动App.tsx(必须动 ⇒ 先拆件);②views/session/SessionTriTrackView.tsx295 / 300 ·shell/TopBar.test.tsx298 / 300 ·ui/primitives/style-seams.test.ts299 / 300 ⇒ 追加前必须先拆(T34 §9 的「余 ≤5」清单共 22 个文件)。- 本批新增登记(逐条带去向):
aligned残余 D3–D6(量级未实测)→ 批 7/8 · 导入会话无音频承载面 → 产品裁决 · 跨窗口档位/相位同步 → 批 7/8 · 孤儿 sidecar + 删会话不删音频 → 与导入会话一并裁决 ·[[ts:ms]]的会话页那一跳(T26 只修了NoteMarkdown那一半;NotePreviewView+utils/html.ts:32的芯片同样无点击,未修)→ 批 7 首要候选 ·lowConfidenceClass从structuredBlocks.ts析出轻模块 + 懒侧字节纳入门禁(katex@0.18.4第二份副本 = +227,858 B 原始 / +64,497 gzip,懒侧今天零预算)→ 批 7/8 ·ink-4/ 审校模式 → 批 7/8 · 暗档data-theme接线 → 批 7/8 · 「对齐」的错位语义(R5.1 定义,用户可否决)→ 产品裁决。- 波 D 的标签清扫(comment-only,「标签不许说谎」):R40.2 点名的清单在本批只登记、未执行(它涉及
app/**源码注释 ⇒ 属生产代码面,T35 零生产代码 ⇒ 移交后续单元)。收口时的在场清单(探针读数,2026-09-13)逐条:① 已过期(事件已发生)——ui/primitives/Modal.css:15·ui/primitives/Toast.css:16-17·ui/primitives/StatusLine.test.tsx:239(三处逐字「批 6 的动效 token 真源落地后删掉motion.css的变量块即生效」)·ui/primitives/ViewSwitcher.css:24-25(「批 6 的三档强度/双基调/GSAP 不在本文件」)·shell/kbCommands.ts:15(「check-command-registry.mjs的 312/312」—— 实测已是 313/313/0) ·ui/tokens.gen.ts:83-85(「今日 0 生产消费者,波 C 首次消费」—— 三个时长 token 今天均有消费点) ·motion/tone.test.ts:23(「今日app/src生产代码里data-tone仍 0 命中」—— 实测已有多处生产命中);② 前向引用(届时自证伪)——views/session/SessionProofView.tsx:56·views/session/SessionTriTrackView.tsx:59·components/session-detail/SessionRawView.tsx:42·ui/primitives/textBaseline.ts:9;③ 假 warn(最要紧的一条) ——motion/engine.guard.test.ts:19与:222/:226-227的用例名与console.warn仍写「今天源图未到达 GSAP ⇒ 存在性分支未运行」,而 T34 的真实构建证明源图已到达(产物有vendor-gsap-BQLVn3Z6.js、该件复跑 11/11)⇒ 它现在说的是假话;④ 过宽措辞 ——motion/controls.ts:22-27的「本模块只许经await import()到达」(正确表述见「决策 3」的收口加注);⑤ 不算说谎 ——ui/primitives/EmptyState.tsx:9-16(登记面,非承诺)。本批不做(零生产代码),去向 = 下一个动效/治理单元。- 🔻 T36 回写(2026-09-13,上一条原文一字未改):🔴 该「标签清扫」已在本批内执行 —— 上面「只登记、未执行」按过去时读。三个 comment-only 微单元(授权见
rulings.md§六十四 R64.1 / §六十五 / §六十六 R66.3 / R66.4):T35b9c1f615c(6 文件 / +36 −22)关闭「已过期 / 假 warn / 过宽措辞」三类共 5 处(kbCommands.ts:15→ 313/313/0 ·tokens.gen.ts:83-85→card6 处 /reveal7 处 ·tone.test.ts:23→ 4 处data-tone·engine.guard.test.ts4 处含用例名与console.warn⇒ 存在性断言真跑 ·controls.ts两处 → R60.1 精确口径)· T35c04dc9de2(8 文件 / +38 −18)关闭 A 组 7 行过宽措辞 + B 组 3 处前向引用(ViewSwitcher.css:25·SessionProofView.tsx:55-56·SessionRawView.tsx:42)· T35d7d6d677b(2 文件 / +10 −3)关闭SessionTriTrackView.tsx:59的悬空承诺与engine.ts:30-31的过宽绝对句。三件的门禁读数(line-limits --full0 ·docs-check0 · registry 313/313/0 ·tsc0 错 · vitest 221 文件 / 2132 用例 / 0 失败 · 单跑engine.guard.test.ts11/11)逐条见各报告;T36 独立复跑line-limits/docs-check/ registry /engine.guard单跑 /cargo test均同读数(见task-36-report.md§3.2)。- ⇒ 🔴 唯一未改项(判为「不算说谎」而只登记):两处测试名仍留旧措辞 ——
components/session-detail/useRevealChoreography.test.tsx:283·views/session/useTriTrackAlign.test.tsx:279(R66.3 #3:名字描述的就是断言本身、且断言仍为真)。上面「在场清单」的逐条读数是清扫前(T35 当时)的快照,保留作对照。
- ADR-034:L2 壳层契约(本 ADR 的 §5 相变与 §7 列折叠 Flip 落在它的列契约上;它的「登记」节逐字把
ADR-035 = L4 动效纲领与引擎归批 6) - ADR-033:L1 原语层与视图层契约(
[data-phase]动效接缝 · 退场指针门控 —— 本批在其上接纲领,不改其契约) - ADR-032:前端设计系统与 token 层(
--ed-前缀单源生成;本 ADR 的决策 4 是它的动效子集延伸) - ADR-013:实时会话预热与播放暂停驱动(例外 E5 的暂停语义出处,并由 T23 就地加注「WAV 轴与会话轴的对齐」)
- 动效规范(L4):本 ADR 的规范载体(四层 / 分界 / 三档 / 双基调 / 判据纪律的正文)