@@ -32,6 +32,20 @@ const MOTION_CSS = readPrimitiveCss("motion.css");
3232const REDUCED_BODY = reducedMotionRules ( ) [ 0 ] . body ;
3333const CSS_FILES : readonly string [ ] = readdirSync ( PRIMITIVES ) . filter ( ( f ) => f . endsWith ( ".css" ) ) ;
3434
35+ /**
36+ * 时长 / 缓动消费点的**名字模式**(T13 修:与 `motionTokens.consumption.test.ts` 的 `CONSUMER` 同类缺陷)。
37+ *
38+ * Why:`--ed-ease` 后接 `-` 处正是**词边界** ⇒ 只认 `--ed-ease`(或用 `\b` 收尾)会把
39+ * `--ed-ease-instrument` / `--ed-ease-paper` **前缀吞成** `--ed-ease`,随后因「兜底形态不符」把一处
40+ * **完全合规**的消费点报成违规 —— 波 B/C 第一次用新缓动名时就会误红(双基调的两条缓动 T8 已进真源,
41+ * 今天 0 CSS 消费点,所以这条一直是潜伏的)。四类名字都要覆盖:
42+ * `--ed-dur-*` · `--ed-ease` · `--ed-ease-instrument` · `--ed-ease-paper`。
43+ * ⚠️ 写成**转义**形态(`var\(`)⇒ `motionTokens.consumption.test.ts` 的全树扫描不会把本文件当消费点
44+ * (本文件头注边界②)。
45+ */
46+ const DURATION_VALUE = new RegExp ( String . raw `^var\((--ed-dur-[a-z-]+),\s*(\d+ms)\)$` ) ;
47+ const EASING_VALUE = new RegExp ( String . raw `^var\((--ed-ease(?:-[a-z-]+)?),\s*(cubic-bezier\([^)]*\))\)$` ) ;
48+
3549describe ( "⑤ 三档同行为 + 桶边界 + 单一真源(§8.5「节能档只留响应层」)" , ( ) => {
3650 it ( "响应层节里零 `[data-motion`,且全仓没有任何 `[data-motion]` 规则碰这些元素" , ( ) => {
3751 expect ( SECTION ) . not . toContain ( "data-motion" ) ;
@@ -78,20 +92,49 @@ describe("⑤ 三档同行为 + 桶边界 + 单一真源(§8.5「节能档只
7892 for ( const r of RULES ) {
7993 const dur = declarationValue ( r . body , "transition-duration" ) ;
8094 if ( dur !== null ) {
81- const m = / ^ v a r \( ( - - e d - d u r - [ a - z - ] + ) , \s * ( \d + m s ) \) $ / . exec ( dur ) ;
95+ const m = DURATION_VALUE . exec ( dur ) ;
8296 if ( ! m ) bad . push ( `${ r . selector } : transition-duration 不是「token + 同值兜底」形态 → ${ dur } ` ) ;
8397 else if ( truth . get ( m [ 1 ] ) !== m [ 2 ] ) bad . push ( `${ r . selector } : ${ m [ 1 ] } 兜底 ${ m [ 2 ] } ≠ 真源 ${ truth . get ( m [ 1 ] ) } ` ) ;
8498 }
8599 const ease = declarationValue ( r . body , "transition-timing-function" ) ;
86100 if ( ease !== null ) {
87- const m = / ^ v a r \( ( - - e d - e a s e ) , \s * ( c u b i c - b e z i e r \( [ ^ ) ] * \) ) \) $ / . exec ( ease ) ;
101+ const m = EASING_VALUE . exec ( ease ) ;
88102 if ( ! m ) bad . push ( `${ r . selector } : transition-timing-function 不是「token + 同值兜底」形态 → ${ ease } ` ) ;
89103 else if ( truth . get ( m [ 1 ] ) !== m [ 2 ] ) bad . push ( `${ r . selector } : ${ m [ 1 ] } 兜底 ${ m [ 2 ] } ≠ 真源 ${ truth . get ( m [ 1 ] ) } ` ) ;
90104 }
91105 }
92106 expect ( bad , `响应层的时长 / 缓动脱离了唯一真源:\n${ bad . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
93107 expect ( truth . size , "真源名册读空了 ⇒ 上面两条是空真" ) . toBe ( DURATION_TOKENS . length + EASING_TOKENS . length ) ;
94108 } ) ;
109+
110+ /* T13 追加(控制方 2026-09-13 指令):「两条正则只认旧名」是与 `motionTokens.consumption.test.ts`
111+ * 被 T6b 修掉的那条**同一类**缺陷(`--ed-ease` 前缀吞掉 `--ed-ease-<name>`)⇒ 口径本身要有自证。
112+ * 样本一律**拼接写**:本文件不得出现 `var(--ed-` 的裸串(见文件头边界②)。 */
113+ it ( "口径自证:四类 token 名都能被**整段**解析(`ease-…` 不得被前缀吞成 `--ed-ease` / 不得误判形态)" , ( ) => {
114+ const durSamples : ReadonlyArray < readonly [ string , string , string ] > = [
115+ [ "var" + "(--ed-dur-micro, 120ms)" , "--ed-dur-micro" , "120ms" ] ,
116+ [ "var" + "(--ed-dur-card, 220ms)" , "--ed-dur-card" , "220ms" ] ,
117+ ] ;
118+ for ( const [ value , name , fallback ] of durSamples ) {
119+ const m = DURATION_VALUE . exec ( value ) ;
120+ expect ( [ m ?. [ 1 ] , m ?. [ 2 ] ] , `时长消费点被漏解析 / 名字被截断:${ value } ` ) . toEqual ( [ name , fallback ] ) ;
121+ }
122+ const easeSamples : ReadonlyArray < readonly [ string , string , string ] > = [
123+ [ "var" + "(--ed-ease, cubic-bezier(0.2, 0, 0, 1))" , "--ed-ease" , "cubic-bezier(0.2, 0, 0, 1)" ] ,
124+ [ "var" + "(--ed-ease-instrument, cubic-bezier(0.4, 0, 0.2, 1))" , "--ed-ease-instrument" , "cubic-bezier(0.4, 0, 0.2, 1)" ] ,
125+ [ "var" + "(--ed-ease-paper, cubic-bezier(0.215, 0.61, 0.355, 1))" , "--ed-ease-paper" , "cubic-bezier(0.215, 0.61, 0.355, 1)" ] ,
126+ ] ;
127+ for ( const [ value , name , fallback ] of easeSamples ) {
128+ const m = EASING_VALUE . exec ( value ) ;
129+ // 名字必须**整段**取到(前缀吞掉时 m[1] 会是 `--ed-ease`、m 直接为 null 也在这里现形)
130+ expect ( [ m ?. [ 1 ] , m ?. [ 2 ] ] , `缓动消费点被漏解析 / 名字被前缀吞掉:${ value } ` ) . toEqual ( [ name , fallback ] ) ;
131+ }
132+ // 反例(防空真):形态不符的写法必须**不**被放过 —— 无兜底 / 兜底类型错位 / 值里带别的写法
133+ for ( const badValue of [ "var" + "(--ed-ease)" , "var" + "(--ed-ease, 120ms)" , "var" + "(--ed-dur-micro, cubic-bezier(0.2, 0, 0, 1))" ] ) {
134+ const ok = DURATION_VALUE . test ( badValue ) || EASING_VALUE . test ( badValue ) ;
135+ expect ( ok , `这台解析器把不合规的写法也放过了:${ badValue } ` ) . toBe ( false ) ;
136+ }
137+ } ) ;
95138} ) ;
96139
97140describe ( "⑥ 控制方 2026-09-13 追加(T6 评审 I-2 / I-3):优先级机理与两块隔离" , ( ) => {
@@ -106,6 +149,25 @@ describe("⑥ 控制方 2026-09-13 追加(T6 评审 I-2 / I-3):优先级
106149 expect ( tierBodies . join ( " " ) , "档位块带了 !important ⇒ 反转无障碍优先级" ) . not . toContain ( "!important" ) ;
107150 } ) ;
108151
152+ /* T13 追加(控制方 2026-09-13 第二次追加 B):R18.1 的 `!important` 判据只管**档位块**与 reduced 块,
153+ * 响应层节自己「零 `!important`」今天没有判据 —— 有人往里加一条 `!important` 就会反转
154+ * 「行内样式优先于类规则」这条 ADR-033 §4 的既成语义(`motion.css` 头部 ⑤ 逐字),而全仓无人察觉。 */
155+ it ( "I-4:响应层节**零** `!important`(防反转「行内优先于类」的语义)" , ( ) => {
156+ const flagged = ( text : string ) : string [ ] =>
157+ [ ...text . matchAll ( / ( [ a - z - ] + ) \s * : \s * ( [ ^ ; { } ] * ! i m p o r t a n t [ ^ ; { } ] * ) / g) ] . map ( ( m ) => `${ m [ 1 ] } : ${ m [ 2 ] . trim ( ) } ` ) ;
158+ expect ( SECTION . length , "响应层节读空了 ⇒ 本条是空真" ) . toBeGreaterThan ( 500 ) ;
159+ expect (
160+ flagged ( SECTION ) ,
161+ `响应层出现了 !important ⇒ 行内样式再也盖不过类规则(ADR-033 §4 的「行内优先于类」被反转):\n${ flagged ( SECTION ) . join ( "\n" ) } ` ,
162+ ) . toEqual ( [ ] ) ;
163+ // 反例对照(防空真):同一台扫描器对已知样本必须报满,对干净样本必须 0
164+ expect ( flagged ( ".x { opacity: .5 !important; transition-duration: 1ms !important; }" ) ) . toEqual ( [
165+ "opacity: .5 !important" ,
166+ "transition-duration: 1ms !important" ,
167+ ] ) ;
168+ expect ( flagged ( ".x { opacity: .5; transition-duration: 1ms; }" ) ) . toEqual ( [ ] ) ;
169+ } ) ;
170+
109171 it ( "I-3:档位块与 reduced 块之间零规则块(两块必须相邻)" , ( ) => {
110172 const tierAt = MOTION_CSS . lastIndexOf ( "[data-motion=" ) ;
111173 const reducedAt = MOTION_CSS . indexOf ( REDUCED_AT ) ;
0 commit comments