99 * 登记进逐文件表**(登记即计数),从而在保留全部防漂移牙齿的前提下允许合法增长。
1010 *
1111 * ★ 五颗牙(**登记制不是「把表抬高就完事」**;牙号与任务书逐条对应)
12- * 牙 1 **既有文件一格都不许涨 **:表里既有的 9 个文件逐文件 `实测 ≤ 登记值 `(主牙齿);
13- * 牙 2 **新条目只许给「原本不在表里」的文件 ∧ `实测 == 登记值` (不是 ≤)** —— 防「先把表抬高再看」;
12+ * 牙 1 **既有(legacy 档)面一格都不许涨 **:逐文件 `实测 − 该文件 new 档登记值之和 ≤ legacy 档登记值 `(主牙齿);
13+ * 牙 2 **new 档必须 `实测 − 该文件 legacy 档登记值之和 == new 档登记值之和`** (不是 ≤)—— 防「先把表抬高再看」;
1414 * 牙 3 **总数三连通**:`FROZEN_SURFACE_TAG_TOTAL == Σ 登记值 == Σ 实测`,
15- * 且 legacy 9 行的和恒等于 `SURFACE_TAG_FROZEN_LEGACY_COUNT`(堵「降 legacy 值腾地方」),
15+ * 且 ** legacy 档**(按 `tier` 筛,不按文件)的和恒等于 `SURFACE_TAG_FROZEN_LEGACY_COUNT`(堵「降 legacy 值腾地方」),
1616 * 锚(`SURFACE_TAG_ANCHOR`:条目数 + 一个具名文件的登记值与实测值)须同步;
1717 * 牙 4 **未登记文件的命中仍然 = 0**(`surfaceRatchet.test.ts` ⑤ 原样保留,本件再查一次);
1818 * 牙 5 **登记必须带理由**(`reason` ≥ 12 字,写明哪个视图 / 为什么用 `<Surface>`)**且防僵尸登记**
19- * (登记的文件必须**真实存在**于扫描面内 ∧ **此刻仍命中 == count**)。
19+ * (登记的文件必须**真实存在**于扫描面内 ∧ **此刻仍命中 > 0**)。
20+ *
21+ * ★ T19(键 `file` → `(file, tier)`):**同键(同文件 ∧ 同档)才 `REG_DUP` 红**,同一文件**可以两行**
22+ * (`legacy` + `new` 各一行)⇒ 「legacy 文件内新增一处 `<Surface>`」有了合法路径:加一行 `tier:"new"`
23+ * (值 = 新增处数)+ 抬 `FROZEN_SURFACE_TAG_TOTAL` + 同步 `SURFACE_TAG_ANCHOR.entries`;🔴 **不许**抬
24+ * legacy 面(`SURFACE_TAG_FROZEN_LEGACY_COUNT` 之和锁当场顶红)。
25+ * 🔴 **不改窄任何牙**:牙 1/2 的分档算术是「同文件两行」下**唯一**能同时表达「legacy 只许降」与
26+ * 「new 恰等于实测」的口径;legacy 和锁**按档位**求和(旧写法 `LEGACY.has(file)` 在双行文件上会把
27+ * new 档算进 legacy 面 ⇒ 那是改键后**必须**跟着改的一处口径,不是放宽)。
2028 *
2129 * ★ 仪器:与 `surfaceRatchet.test.ts` ⑦ **共用** `surfaceScan.ts` 与 `sliceScan.ts`
2230 * (域谓词 / 剥注释 / 开标签提取都只有一份实现)—— 两件判据读的是同一把尺子。
@@ -50,7 +58,7 @@ const SURFACE_TAGS = scanSurfaceTags(FILES, textOf);
5058 * 登记表的**键** = `(file, tier)` 二元组(T19)—— `REG_DUP` / 形状断言 / 字典序断言共用**这一处**实现,
5159 * 故「键退回 `file`」这类变异会同时打到三条断言上(变异体 M1 的红点即在此)。
5260 */
53- const keyOf = ( e : SurfaceTagRegistryEntry ) : string => `${ e . file } |${ e . tier } ` ;
61+ const keyOf = ( e : Pick < SurfaceTagRegistryEntry , "file" | "tier" > ) : string => `${ e . file } |${ e . tier } ` ;
5462/** 登记表的逐**键**读出(同键只许一条 —— 重复键会让「Σ 登记值」与「逐文件比对」两套口径漂移) */
5563const REG = new Map < string , number > ( ) ;
5664const REG_DUP : string [ ] = [ ] ;
@@ -59,9 +67,18 @@ for (const e of SURFACE_TAG_REGISTRY) {
5967 if ( REG . has ( k ) ) REG_DUP . push ( k ) ;
6068 REG . set ( k , ( REG . get ( k ) ?? 0 ) + e . count ) ;
6169}
62- /** 逐文件的登记值**之和**(T19 起同一文件可有两行 ⇒ 逐文件比对必须求和,不能只取一行) */
63- const REG_BY_FILE = new Map < string , number > ( ) ;
64- for ( const e of SURFACE_TAG_REGISTRY ) REG_BY_FILE . set ( e . file , ( REG_BY_FILE . get ( e . file ) ?? 0 ) + e . count ) ;
70+ /**
71+ * 逐文件**按档位**聚合(T19:同一文件可以两行 ⇒ 逐文件比对必须先分档,不能只按文件求和):
72+ * `legacy` = 该文件 legacy 档登记值之和(`≤` 上限)· `next` = new 档登记值之和(**恰等于**实测减去 legacy 面)。
73+ */
74+ interface TierAgg { readonly legacy : number ; readonly next : number }
75+ const BY_FILE = new Map < string , TierAgg > ( ) ;
76+ for ( const e of SURFACE_TAG_REGISTRY ) {
77+ const cur = BY_FILE . get ( e . file ) ?? { legacy : 0 , next : 0 } ;
78+ BY_FILE . set ( e . file , e . tier === "legacy"
79+ ? { legacy : cur . legacy + e . count , next : cur . next }
80+ : { legacy : cur . legacy , next : cur . next + e . count } ) ;
81+ }
6582/** legacy **档**的文件集(判定一律看 `e.tier`;此处只用于「legacy 集合非空」这条非真空自证) */
6683const LEGACY = new Set ( SURFACE_TAG_REGISTRY . filter ( ( e ) => e . tier === "legacy" ) . map ( ( e ) => e . file ) ) ;
6784/** 实测逐文件读出(0 命中的文件不进 `SURFACE_TAGS` ⇒ 显式补 0,好让「表里有、实测无」被抓住) */
@@ -81,6 +98,16 @@ describe("⑪ `<Surface>` 新增调用点登记制(T15a 判据修正:允许
8198 expect ( keys , "登记表的键(`file|tier`)必须按字典序(与 FROZEN_*_BY_FILE 同范式)" ) . toEqual ( [ ...keys ] . sort ( ) ) ;
8299 expect ( SURFACE_TAG_REGISTRY . length , "登记表空了 ⇒ 本组判据空真" ) . toBeGreaterThan ( 0 ) ;
83100 expect ( LEGACY . size , "legacy 集合为空 ⇒ 牙 1 与 legacy 和锁都会空真" ) . toBeGreaterThan ( 0 ) ;
101+ // 🅿️ 正控(防真空):键**确实是二元组** —— 同 `file` 不同 `tier` 必须给出**两个**不同的键。
102+ // 把 `keyOf` 变异回 `e.file` ⇒ 这两行折叠成 1 ⇒ 本行红 ⇒ 钉住「`REG_DUP` 的语义跟着键走」
103+ // (改键前 `REG_DUP` 判的是同 `file`;改键后判的是同 `(file, tier)`)。
104+ expect (
105+ new Set ( [
106+ keyOf ( { file : "views/zz-key-probe.tsx" , tier : "legacy" } ) ,
107+ keyOf ( { file : "views/zz-key-probe.tsx" , tier : "new" } ) ,
108+ ] ) . size ,
109+ "同 file 不同 tier 被折叠成同一个键 ⇒ 键退回了 `file`(改键失效)" ,
110+ ) . toBe ( 2 ) ;
84111 } ) ;
85112
86113 it ( "牙 1/4/5b:既有文件不许涨 · 未登记文件的命中 = 0 · 登记过的防僵尸(实测 > 登记 与 实测 = 0 都报错)" , ( ) => {
@@ -90,16 +117,18 @@ describe("⑪ `<Surface>` 新增调用点登记制(T15a 判据修正:允许
90117 const zombie : string [ ] = [ ] ;
91118 const overflow : string [ ] = [ ] ;
92119 for ( const [ file , n ] of measured ) {
93- const reg = REG_BY_FILE . get ( file ) ;
120+ const agg = BY_FILE . get ( file ) ;
94121 // 牙 4:未登记文件的命中仍为 0
95- if ( reg === undefined ) { zombie . push ( `${ file } : 实测 ${ n } 处,**未登记**(新调用点请登记:登记 + 抬高总数 + 同步锚)` ) ; continue ; }
96- // 牙 1:既有文件一格都不许涨(新登记行由下一用例的「恰等于实测」管)
97- if ( reg < n ) overflow . push ( `${ file } : 登记 ${ reg } 处,实测 ${ n } 处 ⇒ 登记值被超(legacy 只许降 / 新登记必须恰等于实测)` ) ;
122+ if ( agg === undefined ) { zombie . push ( `${ file } : 实测 ${ n } 处,**未登记**(新调用点请登记:登记 + 抬高总数 + 同步锚)` ) ; continue ; }
123+ // 牙 1:**两档之和**是实测的紧上界(新登记行由下一用例的「恰等于实测」管)
124+ if ( agg . legacy + agg . next < n ) {
125+ overflow . push ( `${ file } : 登记 ${ agg . legacy + agg . next } 处,实测 ${ n } 处 ⇒ 登记值被超(legacy 只许降 / 新登记必须恰等于实测)` ) ;
126+ }
98127 }
99128 // 牙 5b:登记的文件此刻必须仍命中(防僵尸登记)
100- for ( const [ file , reg ] of REG_BY_FILE ) {
129+ for ( const [ file , agg ] of BY_FILE ) {
101130 const n = hitsOf ( file ) ;
102- if ( n === 0 ) zombie . push ( `${ file } : 登记 ${ reg } 处,实测 0 处 ⇒ 僵尸登记(迁走了却还挂着)` ) ;
131+ if ( n === 0 ) zombie . push ( `${ file } : 登记 ${ agg . legacy + agg . next } 处,实测 0 处 ⇒ 僵尸登记(迁走了却还挂着)` ) ;
103132 }
104133 expect ( zombie , `未登记 / 僵尸:\n${ zombie . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
105134 expect ( overflow , `登记值被超:\n${ overflow . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
@@ -120,15 +149,17 @@ describe("⑪ `<Surface>` 新增调用点登记制(T15a 判据修正:允许
120149 SURFACE_TAG_FROZEN_LEGACY_COUNT ,
121150 ) ;
122151 const bad : string [ ] = [ ] ;
123- for ( const e of SURFACE_TAG_REGISTRY ) {
124- const n = hitsOf ( e . file ) ;
125- if ( e . tier === "legacy" ) {
126- // 牙 1:legacy 行 = ≤ 上限(只许降)
127- if ( n > e . count ) bad . push ( `${ e . file } : legacy 实测 ${ n } > 登记 ${ e . count } ⇒ 既有文件不许涨(牙 1)` ) ;
128- if ( n === 0 ) bad . push ( `${ e . file } : legacy 实测 0 ⇒ 已迁空,请从登记表收紧(legacy 行只许降,降完手工同步总和)` ) ;
129- } else if ( n !== e . count ) {
130- // 牙 2:新登记行 = **恰等于**实测(不是 ≤)
131- bad . push ( `${ e . file } : 新登记 ${ e . count } ,实测 ${ n } ⇒ 新登记必须**恰等于**实测(防「先把表抬高再看」)` ) ;
152+ for ( const [ file , agg ] of BY_FILE ) {
153+ const n = hitsOf ( file ) ;
154+ if ( agg . legacy > 0 ) {
155+ // 牙 1:legacy 面 = 实测**减去**该文件 new 档登记值之和 ⇒ `≤` 上限(只许降)
156+ const legacyPart = n - agg . next ;
157+ if ( legacyPart > agg . legacy ) bad . push ( `${ file } : legacy 实测 ${ legacyPart } > 登记 ${ agg . legacy } ⇒ 既有文件不许涨(牙 1)` ) ;
158+ if ( legacyPart === 0 ) bad . push ( `${ file } : legacy 实测 0 ⇒ 已迁空,请从登记表收紧(legacy 行只许降,降完手工同步总和)` ) ;
159+ }
160+ if ( agg . next > 0 && n - agg . legacy !== agg . next ) {
161+ // 牙 2:new 档 = **恰等于**实测减去 legacy 档登记值之和(不是 ≤)
162+ bad . push ( `${ file } : 新登记 ${ agg . next } ,实测 ${ n } ⇒ 新登记必须**恰等于**实测(防「先把表抬高再看」)` ) ;
132163 }
133164 }
134165 expect ( bad , `登记表与实测不符:\n${ bad . join ( "\n" ) } ` ) . toEqual ( [ ] ) ;
0 commit comments