@@ -62,3 +62,104 @@ describe("壳层纵向基准 --ed-nav-h", () => {
6262 expect ( empty , `清单里的文件读不到或过短(路径写错了?):\n${ empty . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
6363 } ) ;
6464} ) ;
65+
66+ /* ── T17 / C10#13 追加段:兜底绑定(**纯追加**:既有 3 个 it 与既有 import 一行未动)──────────────
67+ * 病灶(批 4 交接项 #13):上面那份显式清单**不含任何 CSS**,而 `ui/primitives/Toast.css:60` 用
68+ * `top: calc(var(--ed-nav-h, 56px) + 8px)` —— **兜底 56px 没有任何判据绑到 token 真源**
69+ * (`ui/tokens.css:73` 的 `--ed-nav-h: 56px`);该文件 :56 的注释自己承认这条洞:
70+ * 「兜底 56px 与 `TopBar.css` 的 `var(--ed-nav-h, 56px)` 同值(两处兜底漂移不会有任何报错)」。
71+ *
72+ * 三条判据各自独立成 it(陷阱 #67:一个 it 里多条硬断言只有第一条拿得到"有没有牙"的读数):
73+ * ① 域与锚:域非空 · 两份已知消费者在域内 · 域内确有 `--ed-nav-h` 消费 · 域内无 FORBIDDEN 裸 56;
74+ * ② 绑定:域内每个 `var(--ed-nav-h, <N>px)` 的 N **等于**真源 M,且"带逗号的消费"全被解析器认下;
75+ * ③ 反空真:域内命中 > 0 且**无意义串命中 0**(同域双侧自证 —— 仪器纪律第 1 条)。
76+ *
77+ * 🔴 域 = `app/src` 下**递归**的全部 `*.css`(动态枚举,今日 14 个)—— **比计划逐字给的
78+ * `ui/primitives/*.css` 大**:计划引用的洞正是"**两处**兜底漂移",只扫原语层会漏掉
79+ * `shell/TopBar.css` 那一份。代价 = 邻居新增 CSS 若漂移会红(那是**真发现**,不是假红);
80+ * 不动既有 3 个 it 的语义(计划逐字要求)。
81+ * ⚠️ 本段注释里**不许**出现 `**` 紧跟 `/` 的 glob 字面量:`*` + `/` 会**提前终止块注释**
82+ * (实测 esbuild 报 `Unexpected "*"`)—— 同类"文本级仪器被字面量骗到"的 #54/#64 家族。
83+ * 残留(登记):`.tsx` 的**行内 style 兜底**不在本判据域内(口径与读数见 `task-17-report.md`)。
84+ * ──────────────────────────────────────────────────────────────────────────────────────────── */
85+ import { readdirSync , statSync } from "node:fs" ; // 独立成行:既有那行 import 保持逐字原样(计划 V2:删除行 = 0)
86+
87+ /** 域内**必须**出现的两份已知兜底消费者(动态域的锚:改名/搬走必须被看见) */
88+ const FALLBACK_ANCHORS = [ "ui/primitives/Toast.css" , "shell/TopBar.css" ] as const ;
89+ /** `var(--ed-nav-h, <N>px)`:逗号与数字两边允许空白(与上面 FORBIDDEN/REQUIRED 的宽松口径一致) */
90+ const FALLBACK_PX = / v a r \( \s * - - e d - n a v - h \s * , \s * ( \d + ) p x \s * \) / g;
91+ /** 任何"带兜底的消费"(**不看兜底形态**)——抓「兜底写成非 `<N>px` ⇒ 被 FALLBACK_PX 静默漏掉」 */
92+ const FALLBACK_ANY = / v a r \( \s * - - e d - n a v - h \s * , / g;
93+ /** token 真源:`ui/tokens.css` 的 `--ed-nav-h: <M>px` */
94+ const TOKEN_DECL = / - - e d - n a v - h \s * : \s * ( \d + ) p x / ;
95+
96+ /** `app/src` 下全部 `*.css`(相对 `app/src` 的正斜杠路径,排序稳定 ⇒ 失败信息可比对) */
97+ function cssFilesUnder ( dir : string , prefix = "" ) : string [ ] {
98+ const out : string [ ] = [ ] ;
99+ for ( const name of readdirSync ( dir ) ) {
100+ const abs = join ( dir , name ) ;
101+ // ⚠️ 本仓 `src/node-builtins.d.ts` 只声明了单参 `readdirSync(path)` ⇒ `{ withFileTypes: true }`
102+ // 在 tsc 下报 TS2554/TS2339(`columnRegistry.test.ts:43` 已记同一缺口)。零新增依赖 ⇒ 走 statSync。
103+ if ( statSync ( abs ) . isDirectory ( ) ) out . push ( ...cssFilesUnder ( abs , `${ prefix } ${ name } /` ) ) ;
104+ else if ( name . endsWith ( ".css" ) ) out . push ( `${ prefix } ${ name } ` ) ;
105+ }
106+ return out . sort ( ) ;
107+ }
108+
109+ /**
110+ * CSS 注释**就地掩码**(非换行字符替换成空格 ⇒ 行号不错位)。
111+ * Why 必须剥:`Toast.css:56` 的**注释**里逐字写着 `var(--ed-nav-h, 56px)`(它正是本判据要解释的那段
112+ * 说明)—— 不剥就是「注释里的字面量骗过扫描器」(陷阱 #54/#64 家族,本批已三犯),
113+ * 后果是**改注释措辞会假红**(T8 已裁:判据不许对措辞过敏)。既有 3 个 it 仍用未剥的 `lines()`(V2)。
114+ */
115+ const maskCssComments = ( s : string ) : string => s . replace ( / \/ \* [ \s \S ] * ?\* \/ / g, ( m ) => m . replace ( / [ ^ \n ] / g, " " ) ) ;
116+
117+ /** 域内文件逐行:`{ n: 行号, code: 去注释行(判据用), raw: 原始行(失败信息用)}` */
118+ function cssLines ( rel : string ) : { n : number ; code : string ; raw : string } [ ] {
119+ const text = readFileSync ( join ( SRC , rel ) , "utf8" ) ;
120+ const raw = text . split ( / \r ? \n / ) ;
121+ return maskCssComments ( text )
122+ . split ( / \r ? \n / )
123+ . map ( ( code , i ) => ( { n : i + 1 , code, raw : raw [ i ] ?? "" } ) ) ;
124+ }
125+
126+ const CSS_DOMAIN = cssFilesUnder ( SRC ) ;
127+ /** 域正文(去注释后的代码面;用本文件既有的读盘口径,不另起一套路径解析) */
128+ const cssDomainText = ( ) : string => CSS_DOMAIN . map ( ( rel ) => cssLines ( rel ) . map ( ( r ) => r . code ) . join ( "\n" ) ) . join ( "\n" ) ;
129+
130+ describe ( "兜底绑定(T17 / C10#13):var(--ed-nav-h, <N>px) 的 N 必须等于 token 真源" , ( ) => {
131+ it ( "① 域与锚:域非空 · 两份已知消费者在域内 · 域内确有消费 · 域内无裸 56" , ( ) => {
132+ expect ( CSS_DOMAIN . length , "域为空 ⇒ ②③ 会静默变成空真" ) . toBeGreaterThan ( 0 ) ;
133+ const missing = FALLBACK_ANCHORS . filter ( ( a ) => ! CSS_DOMAIN . includes ( a ) ) ;
134+ expect ( missing , `已知的兜底消费者不在域内(改名/搬走了?):\n${ missing . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
135+ const consuming = FALLBACK_ANCHORS . filter ( ( a ) => cssLines ( a ) . some ( ( r ) => REQUIRED . test ( r . code ) ) ) ;
136+ expect ( consuming , "锚清单里每一份都必须真的消费 --ed-nav-h(少一份 ⇒「两处兜底漂移」里的那一处就没了判据)" ) . toEqual ( [ ...FALLBACK_ANCHORS ] ) ;
137+ const bare : string [ ] = [ ] ;
138+ for ( const rel of CSS_DOMAIN ) for ( const r of cssLines ( rel ) ) if ( FORBIDDEN . some ( ( re ) => re . test ( r . code ) ) ) bare . push ( `${ rel } :${ r . n } ${ r . raw . trim ( ) } ` ) ;
139+ expect ( bare , `CSS 里仍硬编码 56(请改用 var(--ed-nav-h)):\n${ bare . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
140+ } ) ;
141+
142+ it ( "② 绑定:每个兜底 <N> == tokens.css 的 <M>,且带逗号的消费全部被解析(形态漂移也红)" , ( ) => {
143+ const decl = readFileSync ( join ( SRC , "ui/tokens.css" ) , "utf8" ) . match ( TOKEN_DECL ) ;
144+ expect ( decl , "tokens.css 里读不到 `--ed-nav-h: <M>px` ⇒ 真源不可读,本条不该静默通过" ) . not . toBeNull ( ) ;
145+ const truth = Number ( decl ?. [ 1 ] ) ;
146+ const bad : string [ ] = [ ] ;
147+ for ( const rel of CSS_DOMAIN ) {
148+ for ( const r of cssLines ( rel ) ) {
149+ for ( const hit of r . code . matchAll ( FALLBACK_PX ) ) if ( Number ( hit [ 1 ] ) !== truth ) bad . push ( `${ rel } :${ r . n } 兜底 ${ hit [ 1 ] } px ≠ 真源 ${ truth } px ${ r . raw . trim ( ) } ` ) ;
150+ }
151+ }
152+ expect ( bad , `兜底与 token 真源漂移(「两处兜底漂移不会有任何报错」—— 本条就是那个报错):\n${ bad . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
153+ const text = cssDomainText ( ) ;
154+ const any = [ ...text . matchAll ( FALLBACK_ANY ) ] . length ;
155+ const parsed = [ ...text . matchAll ( FALLBACK_PX ) ] . length ;
156+ expect ( parsed , "域内一处兜底都解析不到 ⇒ 本条是空真(真源读到了,判据却没在判)—— 确实要删兜底请连本条一起改" ) . toBeGreaterThan ( 0 ) ;
157+ expect ( parsed , `有 ${ any } 处带兜底的消费,却只解析出 ${ parsed } 处 <N>px ⇒ 兜底换了形态(判据会漏检)` ) . toBe ( any ) ;
158+ } ) ;
159+
160+ it ( "③ 反空真:域内命中 > 0 · 无意义串命中 0(同域双侧自证)" , ( ) => {
161+ const text = cssDomainText ( ) ;
162+ expect ( [ ...text . matchAll ( / - - e d - n a v - h / g) ] . length , "域内 0 命中 ⇒ 是域选错了,不是判据绿" ) . toBeGreaterThan ( 0 ) ;
163+ expect ( [ ...text . matchAll ( / z z z - n o t - a - t o k e n - z z z / g) ] . length , "无意义串也命中 ⇒ 这台仪器在乱报" ) . toBe ( 0 ) ;
164+ } ) ;
165+ } ) ;
0 commit comments