1515 * ⑤未登记文件 = 0 ⑥防真空对照)—— `describe.each` 让三组用例**各自具名**,失败可单点定位。
1616 *
1717 * ★ 第 ⑦ 条(计划 M3/V4 点名必须补):**调用点不得用行内 `style` 覆盖 `Surface` 的底/圆角/边框**
18- * (ADR-033 §4:`style` 是纯透传、**视觉权威在类**)。今日域内 `<Surface` 开标签 **0 个** ⇒ 该判据
19- * 对真实代码**空真**;故它自带正反 fixture(正例必命中 / 布局反例必不命中 / 域外样本不在域内)
20- * 并冻结 `FROZEN_SURFACE_TAG_TOTAL`(T17-B 迁移后必须手工抬高 ⇒ 空真结束)。
18+ * (ADR-033 §4:`style` 是纯透传、**视觉权威在类**)。迁移前域内 `<Surface` 开标签 **0 个** ⇒ 该判据
19+ * 对真实代码**空真**;故它自带正反 fixture(正例必命中 / 布局反例必不命中 / 域外样本不在域内)。
20+ * ⚠️ T15a 后本件**不再**断言「标签数 == `FROZEN_SURFACE_TAG_TOTAL`」(那会让任何新视图用 `<Surface>`
21+ * 必然红)—— 改由 `surfaceTagRegistry.test.ts` ⑪ 的**新增调用点登记制**判:登记即计数、总数 ==
22+ * Σ登记值 == Σ实测。本件只保留「标签数 == Σ 登记值」这一条**交叉**自证(两件判据对同一把尺子)。
2123 *
2224 * ★ 变异体(`tmp/t17a/run-mutants.mjs`,**每个变异新解一棵导出树** · CONTROL 在冻结提交的新解树上取):
2325 * M1 恢复一处 `border: "1px solid #e5e7eb"` ⇒ ①红 · M2 调用点新增 `boxShadow:` 字面量 ⇒ 阴影组红
2426 * (**阴影红线**)· M3 行内 `style` 覆盖 `Surface` 底色 ⇒ ⑦红 · M4 新增 `borderRadius: 6` ⇒ 圆角组红 ·
2527 * M5 删基线一行 ⇒ ④/③红 · M6(反例守卫,**必须绿**)新增合法档 `borderRadius: 8` ⇒ 全绿。
28+ * (T15a 的 M1–M6 是**另一组**:`<Surface>` 调用点登记制,见 `surfaceTagRegistry.test.ts`。)
2629 *
2730 * 副作用:只读磁盘(递归遍历 `app/src`)。边界:**文本级**判据、不做 AST;切片外余量(边框 28 · 圆角 28 · 阴影 3 文件)只冻结不迁移,去向见 `surfaceBaseline.ts` 与 `tmp/t17a/slice.md`。
2831 */
2932import { existsSync , readFileSync } from "node:fs" ;
3033import { dirname , join } from "node:path" ;
3134import { fileURLToPath } from "node:url" ;
3235import { describe , expect , it } from "vitest" ;
33- import { relOf , readLines , stripComments , walkSources } from "./sliceScan" ;
3436import {
3537 BORDER_ANCHOR , FROZEN_BORDER_BY_FILE , FROZEN_BORDER_TOTAL ,
3638 FROZEN_RADIUS_OUTLIER_BY_FILE , FROZEN_RADIUS_OUTLIER_TOTAL ,
37- FROZEN_SHADOW_BY_FILE , FROZEN_SHADOW_TOTAL , FROZEN_SURFACE_TAG_TOTAL ,
38- RADIUS_OUTLIER_ANCHOR , SHADOW_ANCHOR , SHADOW_RESIDUAL , type ShadowResidualKind ,
39+ FROZEN_SHADOW_BY_FILE , FROZEN_SHADOW_TOTAL ,
40+ RADIUS_OUTLIER_ANCHOR , SHADOW_ANCHOR ,
3941} from "./surfaceBaseline" ;
40- import { BORDER_RESIDUAL , RADIUS_RESIDUAL , SURFACE_RESIDUAL_WHY } from "./surfaceResidual" ;
42+ import { relOf , stripComments , walkSources } from "./sliceScan" ;
43+ import {
44+ absOf as absOfSrc , inDomain , overridesSurface , scanSurfaceTags , surfaceTagsIn , textOf as textOfSrc ,
45+ } from "./surfaceScan" ;
46+ import {
47+ BORDER_RESIDUAL , RADIUS_RESIDUAL , SHADOW_RESIDUAL , SURFACE_RESIDUAL_WHY ,
48+ SURFACE_TAG_REGISTRY , type ShadowResidualKind ,
49+ } from "./surfaceResidual" ;
4150
4251const HERE = dirname ( fileURLToPath ( import . meta. url ) ) ;
4352/** `app/src` —— 基线的键就是相对这个目录的正斜杠路径。 */
4453const SRC = join ( HERE , ".." , ".." ) ;
4554
46- /** 域口径(判据与 fixture 共用**同一个**谓词,防"两套口径") */
47- const inDomain = ( rel : string ) : boolean => ! / \. t e s t \. t s x ? $ / . test ( rel ) && ! rel . startsWith ( "ui/primitives/" ) ;
55+ /** 域口径与读法**全部来自共享件** `surfaceScan.ts`(域谓词只有一份实现) */
56+ const absOf = ( rel : string ) : string => absOfSrc ( SRC , rel ) ;
57+ const textOf = ( rel : string ) : string => textOfSrc ( SRC , rel ) ;
4858const FILES : readonly string [ ] = walkSources ( SRC ) . map ( ( abs ) => relOf ( SRC , abs ) ) . filter ( inDomain ) ;
49- const absOf = ( rel : string ) : string => join ( SRC , ...rel . split ( "/" ) ) ;
50- /** 剥注释后的整段文本(仪器只此一处取用,判据与自证同源) */
51- const textOf = ( rel : string ) : string => readLines ( absOf ( rel ) ) . stripped . join ( "\n" ) ;
5259/** 「处」口径:整段文本上的匹配次数(`g` 只用于统计) */
5360const countOcc = ( text : string , re : RegExp ) : number =>
5461 ( text . match ( new RegExp ( re . source , re . flags . includes ( "g" ) ? re . flags : `${ re . flags } g` ) ) ?? [ ] ) . length ;
@@ -144,42 +151,8 @@ describe.each(FAMILIES)("$label(T17-A 冻结基线,只许降)", (f) => {
144151} ) ;
145152
146153/* ───────── ⑦ ADR-033 §4 的机器判据:调用点不得用行内 `style` 覆盖 `Surface`(计划 M3/V4) ───────── */
147- /** 被覆盖即违规的属性键(底/圆角/边框三族;**不含** `padding/margin/width` 等布局口 —— 那是 `style` 的合法用途) */
148- const OVERRIDE_KEYS =
149- / (?: ^ | [ , { ; \s ] ) ( b a c k g r o u n d | b a c k g r o u n d C o l o r | b a c k g r o u n d I m a g e | b o r d e r | b o r d e r C o l o r | b o r d e r W i d t h | b o r d e r S t y l e | b o r d e r T o p | b o r d e r B o t t o m | b o r d e r L e f t | b o r d e r R i g h t | b o r d e r R a d i u s ) \s * : / ;
150-
151- /**
152- * 单个 `<Surface …>` 开标签是否用行内 `style` 覆盖了底/圆角/边框。
153- * 边界(**文本级启发式,不是 AST**):假阳 = JSX 文本节点里出现的字面量 `<Surface`(实测本仓 0 例);
154- * 假阴 = 样式对象经变量间接给出(`style={S}` / `style={pick()}`)⇒ 判据只认 `style={{` 的字面对象。
155- * `style={{ padding: 8 }}` 这类**纯布局**写法**不**算违规(ADR-033 §4:`style` 是透传,供布局用)。
156- */
157- function overridesSurface ( tag : string ) : boolean {
158- const at = tag . search ( / \b s t y l e \s * = \s * \{ / ) ;
159- return at >= 0 && OVERRIDE_KEYS . test ( tag . slice ( at ) ) ;
160- }
161-
162- /** 取 `<Surface …>` 的整个开标签:从 `<Surface` 起按 `{}` 深度找深度 0 的 `>`(跨行标签也能取全) */
163- function surfaceTagsIn ( text : string ) : { tag : string ; line : number } [ ] {
164- const out : { tag : string ; line : number } [ ] = [ ] ;
165- const re = / < S u r f a c e (? = [ \s / > ] ) / g;
166- let m : RegExpExecArray | null ;
167- while ( ( m = re . exec ( text ) ) !== null ) {
168- let i = m . index + 8 ;
169- let depth = 0 ;
170- while ( i < text . length ) {
171- const c = text [ i ] ;
172- if ( c === "{" ) depth ++ ;
173- else if ( c === "}" ) depth -- ;
174- else if ( c === ">" && depth === 0 ) break ;
175- i ++ ;
176- }
177- out . push ( { tag : text . slice ( m . index , i + 1 ) , line : text . slice ( 0 , m . index ) . split ( "\n" ) . length } ) ;
178- }
179- return out ;
180- }
181-
182- const SURFACE_TAGS = FILES . flatMap ( ( rel ) => surfaceTagsIn ( textOf ( rel ) ) . map ( ( t ) => ( { file : rel , ...t } ) ) ) ;
154+ // 仪器在 `surfaceScan.ts`(T15a 析出:⑦ 与 ⑪ 的登记制共用同一把尺子,防两套口径漂移)
155+ const SURFACE_TAGS = scanSurfaceTags ( FILES , textOf ) ;
183156const OVERRIDES = SURFACE_TAGS . filter ( ( t ) => overridesSurface ( t . tag ) ) ;
184157
185158describe ( "⑦ 调用点不得用行内 `style` 覆盖 `Surface` 的底/圆角/边框(ADR-033 §4)" , ( ) => {
@@ -188,7 +161,7 @@ describe("⑦ 调用点不得用行内 `style` 覆盖 `Surface` 的底/圆角/
188161 expect ( hits , `行内 style 覆盖了 Surface 的视觉语义(视觉权威必须在类上):\n${ hits . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
189162 } ) ;
190163
191- it ( "启发式双侧自证 + 非真空登记 :正例必命中 · 布局反例必不命中 · 域外样本不在域内 · 标签数 == 冻结值 " , ( ) => {
164+ it ( "启发式双侧自证 + 登记面自证 :正例必命中 · 布局反例必不命中 · 域外样本不在域内 · 标签数 == Σ 登记值 " , ( ) => {
192165 expect ( overridesSurface ( '<Surface style={{ background: "#fff" }}>' ) , "底色覆盖没被命中" ) . toBe ( true ) ;
193166 expect ( overridesSurface ( '<Surface\n className="card"\n style={{ borderRadius: 8 }}\n>' ) , "跨行 + 圆角覆盖没被命中" ) . toBe ( true ) ;
194167 expect ( overridesSurface ( '<Surface style={{ border: "1px solid #e5e7eb" }}>' ) , "边框覆盖没被命中" ) . toBe ( true ) ;
@@ -198,9 +171,10 @@ describe("⑦ 调用点不得用行内 `style` 覆盖 `Surface` 的底/圆角/
198171 expect ( existsSync ( absOf ( "ui/primitives/Surface.test.tsx" ) ) , "域外样本文件不存在 ⇒ 排除自证无效" ) . toBe ( true ) ;
199172 expect ( inDomain ( "ui/primitives/Surface.test.tsx" ) , "原语层/测试文件掉进了域内" ) . toBe ( false ) ;
200173 expect ( inDomain ( "components/GoalCard.tsx" ) , "域内样本被误排除" ) . toBe ( true ) ;
201- // 空真登记:今日 0 个 `<Surface>` ⇒ 上面那条 0 命中是**空真**;T17-B 迁移后必须手工抬高
202- expect ( SURFACE_TAGS . length , "调用点里出现了 <Surface> ⇒ 请抬高 FROZEN_SURFACE_TAG_TOTAL 并复核 ⑦ 已变为有真实输入" ) . toBe (
203- FROZEN_SURFACE_TAG_TOTAL ,
174+ // 空真登记:迁移前 0 个 `<Surface>` ⇒ 那条 0 命中是**空真**;T17-B 迁进 14 个后 ⑦ 第一次有真实输入。
175+ // T15a 修正:标签数不再钉死常数,而须**恰等于登记值之和**(登记即计数;增长走 ⑪ 的登记制通道)
176+ expect ( SURFACE_TAGS . length , "调用点里的 `<Surface>` 数 ≠ 登记表之和 ⇒ 请走 ⑪ 的登记制(登记 + 抬高总数 + 同步锚)" ) . toBe (
177+ SURFACE_TAG_REGISTRY . reduce ( ( a , e ) => a + e . count , 0 ) ,
204178 ) ;
205179 } ) ;
206180} ) ;
0 commit comments