77 * **不该**染上语义色)。没有棘轮,「还没迁完」就退化成「永远在迁」。
88 *
99 * 四条判据:
10- * ① 迁移面**冻结**(49 文件,只许增不许减)—— 迁走了文件要**主动**把它登记进来;
11- * ② 三红字面量**逐文件 ≤ 冻结值** + **总数 ≤ 116** + 新增文件里**不得出现**三红字面量;
10+ * ① 迁移面**冻结**(49 文件,只许增不许减)+ **逐文件 `<StatusLine` 处数下界**(② 的牙齿,
11+ * T13–T15 评审 M-7 的结清 —— 只判「出现过」时,把 5 处收敛成 1 处也照样绿);
12+ * ② 三红字面量**逐文件 ≤ 冻结值** + **总数 ≤ 114** + 新增文件里**不得出现**三红字面量;
13+ * ⚠️ 计数**先剥注释**(`sliceScan.stripComments`,与三个兄弟棘轮同口径 —— 评审 M-2 的结清:
14+ * 原实现读原文,于是注释里列举色值的两处(`ClassroomBanners.tsx:13` / `utils/refineDiff.ts:13`)
15+ * 被算成命中,纯注释改动会假红)。
1216 * ③ `error` 档必须走 `--ed-stamp` **文字色**(不是别的档、不是底色)—— `StatusLine.tsx` 的
1317 * 用色契约与控制方 2026-09-11 裁决③ 同源;`StatusLine.css` 里**连底色属性名都不出现**
1418 * (「错误色绝不进按钮底色」因此是结构保证,不是纪律);
@@ -30,6 +34,8 @@ import { dirname, join } from "node:path";
3034import { fileURLToPath } from "node:url" ;
3135import { describe , expect , it } from "vitest" ;
3236import { FROZEN_RED_BY_FILE , FROZEN_RED_TOTAL } from "./statusLineBaseline" ;
37+ // 剥注释仪器:三个兄弟棘轮(loading / emptyState / confirm)都用它 ⇒ 口径必须同源,不许本地重写
38+ import { stripComments } from "./sliceScan" ;
3339
3440const HERE = dirname ( fileURLToPath ( import . meta. url ) ) ;
3541const SRC = join ( HERE , ".." , ".." ) ;
@@ -83,13 +89,26 @@ function walk(prefix = "", out: string[] = []): string[] {
8389const PROD = walk ( ) . filter ( ( r ) => ! r . startsWith ( "ui/primitives/" ) ) ;
8490const read = ( rel : string ) : string => readFileSync ( join ( SRC , ...rel . split ( "/" ) ) , "utf8" ) ;
8591
86- /** 逐文件三红计数(按**出现次数**,同一行两次算 2 —— 与基线生成器同口径) */
92+ /**
93+ * 逐文件 `<StatusLine` 处数**下界**(① 的牙齿,评审 M-7 的结清)。只写 > 1 的 8 个,其余一律 1;
94+ * 下界是「迁移面真的把错误行交给了原语」的最小证据 —— 加处数不红(那不是回潮),掉到下界以下必红。
95+ */
96+ const MIN_CALLS : Readonly < Record < string , number > > = {
97+ "components/GroupDeleteConfirm.tsx" : 5 ,
98+ "components/EnrichPanel.tsx" : 4 ,
99+ "components/AiRefineCard.tsx" : 2 , "components/AiServicePanel.tsx" : 2 , "components/GoalDetail.tsx" : 2 ,
100+ "components/ModelManagementPanel.tsx" : 2 , "components/RefineLaunchDialog.tsx" : 2 ,
101+ "components/RefineWorkbench.tsx" : 2 ,
102+ } ;
103+
104+ /** 一段文本里三红十六进制字面量的**出现次数**(同一行两次算 2 —— 与基线生成器同口径) */
105+ const countIn = ( text : string ) : number => RED_HEX . reduce ( ( n , h ) => n + ( text . split ( h ) . length - 1 ) , 0 ) ;
106+
107+ /** 逐文件三红计数:**先剥注释**(M-2 —— 注释里列举色值不算命中,与三个兄弟棘轮同口径) */
87108function redCounts ( ) : Map < string , number > {
88109 const map = new Map < string , number > ( ) ;
89110 for ( const rel of PROD ) {
90- const text = read ( rel ) ;
91- let n = 0 ;
92- for ( const h of RED_HEX ) n += text . split ( h ) . length - 1 ;
111+ const n = countIn ( stripComments ( read ( rel ) ) ) ;
93112 if ( n > 0 ) map . set ( rel , n ) ;
94113 }
95114 return map ;
@@ -99,19 +118,25 @@ const CSS = read("ui/primitives/StatusLine.css");
99118const TSX = read ( "ui/primitives/StatusLine.tsx" ) ;
100119
101120describe ( "状态行棘轮(B11 · T15)" , ( ) => {
102- it ( "① 迁移面 49 文件:逐条在盘上、都真的用了 StatusLine、且与回退账不重叠" , ( ) => {
121+ it ( "① 迁移面 49 文件:逐条在盘上、`< StatusLine` 处数 ≥ 下界 、且与回退账不重叠" , ( ) => {
103122 expect ( MIGRATED ) . toHaveLength ( 49 ) ;
104123 expect ( new Set ( MIGRATED ) . size , "迁移面有重复项" ) . toBe ( 49 ) ;
105124 expect ( [ ...MIGRATED ] . sort ( ) , "迁移面未按字典序(对拍会假红)" ) . toEqual ( [ ...MIGRATED ] ) ;
106125 const missing = MIGRATED . filter ( ( rel ) => ! PROD . includes ( rel ) ) ;
107126 expect ( missing , `迁移面里这些文件在盘上不存在(改名 / 迁移后本行先红):\n${ missing . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
108- const noUse = MIGRATED . filter ( ( rel ) => ! / < S t a t u s L i n e [ \s / > ] / . test ( read ( rel ) ) ) ;
109- expect ( noUse , `登记为「已迁移」但没有 <StatusLine>(迁移面是虚的):\n${ noUse . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
127+ const thin = MIGRATED . map ( ( rel ) => [ rel , ( stripComments ( read ( rel ) ) . match ( / < S t a t u s L i n e [ \s / > ] / g) ?? [ ] ) . length ] as const )
128+ . filter ( ( [ rel , n ] ) => n < ( MIN_CALLS [ rel ] ?? 1 ) )
129+ . map ( ( [ rel , n ] ) => `${ rel } : 实测 ${ n } < 下界 ${ MIN_CALLS [ rel ] ?? 1 } ` ) ;
130+ expect ( thin , `登记为「已迁移」但 \`<StatusLine\` 处数掉到下界以下(迁移面是虚的):\n${ thin . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
131+ // 下界表不许有僵尸键 / 漏键(49 个迁移面文件里 > 1 的恰 8 个)
132+ const keys = Object . keys ( MIN_CALLS ) ;
133+ expect ( keys . filter ( ( k ) => ! MIGRATED . includes ( k ) ) , "下界表里有不在迁移面的键" ) . toEqual ( [ ] ) ;
134+ expect ( keys . filter ( ( k ) => ( MIN_CALLS [ k ] ?? 0 ) < 2 ) , "下界表里的值必须 > 1(=1 是默认值)" ) . toEqual ( [ ] ) ;
110135 const overlap = MIGRATED . filter ( ( rel ) => REVERTED_BY_B1_B2 . includes ( rel ) ) ;
111136 expect ( overlap , `回退账里的文件不许同时算进迁移面(两个账本必须互斥):\n${ overlap . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
112137 } ) ;
113138
114- it ( "② 三红只许减:逐文件 ≤ 冻结值 · 总数 ≤ 116 · 新增文件里不得出现三红" , ( ) => {
139+ it ( "② 三红只许减:逐文件 ≤ 冻结值 · 总数 ≤ 114 · 新增文件里不得出现三红" , ( ) => {
115140 const grown = [ ...COUNTS . entries ( ) ] . filter ( ( [ f , n ] ) => n > ( FROZEN_RED_BY_FILE [ f ] ?? 0 ) )
116141 . map ( ( [ f , n ] ) => `${ f } : ${ FROZEN_RED_BY_FILE [ f ] ?? 0 } → ${ n } ` ) ;
117142 expect ( grown , `这些文件的三红字面量**涨了**(棘轮只许降;真迁移了请手工收紧 statusLineBaseline):\n${ grown . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
@@ -147,9 +172,12 @@ describe("状态行棘轮(B11 · T15)", () => {
147172 expect ( / \b a r i a - l i v e \s * = / . test ( TSX ) , "StatusLine 重复声明了 aria-live(role 已隐含,会制造第二个真源)" ) . toBe ( false ) ;
148173 } ) ;
149174
150- it ( "④ 仪器自证:七值可读、正/负样本、域边界(原语层在域外)" , ( ) => {
175+ it ( "④ 仪器自证:七值可读、剥注释口径、 正/负样本、域边界(原语层在域外)" , ( ) => {
151176 expect ( RED_HEX ) . toHaveLength ( 7 ) ;
152177 expect ( RED_HEX . every ( ( h ) => / ^ # [ 0 - 9 a - f ] { 6 } $ / . test ( h ) ) , "词表里有非法十六进制" ) . toBe ( true ) ;
178+ // 剥注释口径自证(M-2):字符串里的三红算命中、行/块注释里的**不算** —— 两条各一,防口径单侧失效
179+ expect ( countIn ( stripComments ( 'const a = "#b91c1c"; // #dc2626\n/* #ef4444 */' ) ) , "注释里的三红被计入了" ) . toBe ( 1 ) ;
180+ expect ( countIn ( stripComments ( "// #b91c1c\n/* #dc2626 #ef4444 */" ) ) , "全是注释却仍有命中" ) . toBe ( 0 ) ;
153181 // 正样本:域内确实还有命中(基线非空;否则下面的棘轮是空真)
154182 expect ( COUNTS . size , "域内一处三红都没有 ⇒ 基线表是空真" ) . toBeGreaterThan ( 0 ) ;
155183 expect ( [ ...COUNTS . values ( ) ] . reduce ( ( a , b ) => a + b , 0 ) , "域内三红总数与基线不符" ) . toBe ( FROZEN_RED_TOTAL ) ;
0 commit comments