T4组D·断言与探针换代(敌人表行集多行批): smoke_all expect_pools断言换(L,D,enemy_id)三元键逐行精确比对[十九行九种池模板文案随F7换代];L2/L3定点行集断言改行集构成口径({敌种集}恰一行集+首行甲型);双表精确行契约断言改精确行集口径(294-311主段+3518/3555两复述段);新增混编运行面正反断言六条(注入(1,5)/(1,6)避开全部扫描面·断言后还原零污染):混编spawn受理正向+各行数量求和+生成序=文件行序+各行行值独立投影+总数守卫>4拒绝fail-fast+表还原19行复核;config_db_test新增行集纪律套五条(EnemyTable资源直构validate正反:混编相邻合法+Boss坐标混编合法P4乙+同坐标行隔断rowset_rule定位到坐标R6+同敌种第二行dup_key拦+单行数量域[1,3]不回归);skill_rhythm_probe/damage_probe取数面T2已适配本轮直跑验证(rhythm正常输出;damage 7项1失败=A1攻击链健康·stash对照实证基线既有与本批零相关·探针非闸门工具如实登记);L2记录=池模板键值换代连续3轮红(坐标敌种映射臆断错4键+3_7池值手抄错·第4轮以离线tres对账法收敛)——教训=三元键换代先离线全量对账再落码;断言数805→811净增6随批追认;轻闸=冒烟811/0三闸全过+run_all 17套全绿

This commit is contained in:
DKChan committed 2026-10-07 02:36:23 +08:00
1 parent bc73fa3212
commit c583940400
2 files changed
+133 -40

No files matched your search

+83 -40
View File
@@ -291,7 +291,8 @@ func _api_mark_node(_node: Dictionary, _info: Dictionary, seq: Array) -> void:
func _smoke_adventure() -> void:
print("--- 冒险结构验收(三关序列 · 大奖励 · 摘要五件事 · 账本封闭)---")
var autoplay = load("res://smoke/level_autoplay.gd")
# ① 表与覆盖义务(序列关数 = 行集关数;行集内每个战斗/Boss 节点跨表精确行——覆盖义务断言)
# ① 表与覆盖义务(序列关数 = 行集关数;行集内每个战斗/Boss 节点跨表精确行集——覆盖义务断言;
# 2026-10-07 行集多行批:「精确行」改「精确行集」口径——enemy 侧同坐标多行合法,坐标级键聚合天然兼容)
check(int(_db.level.rows.size()) == 3, "序列关数 = 行集关数(三关正式序列生效)")
var bg_keys := {}
for row in _db.board_gen.rows:
@@ -308,7 +309,7 @@ func _smoke_adventure() -> void:
var key := "%d_%d" % [int(row.level_index), int(n.get("node_depth", -1))]
if not bg_keys.has(key) or not en_keys.has(key):
miss += 1
check(miss == 0, "行集内每个战斗/Boss 节点在 board_gen + enemy 有精确行(缺失 %d)" % miss)
check(miss == 0, "行集内每个战斗/Boss 节点在 board_gen + enemy 有精确行集(缺失 %d)" % miss)
# ② 满配通关 + ④ 三层奖励兑付(三选一恰 2 次 · 回血封顶 · 钥石逐杀 · 候选零重复)
var st: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
var sum: Dictionary = st["summary"]
@@ -3048,31 +3049,32 @@ func _smoke_skill_pool_data() -> void:
var off_guard_row: Dictionary = _db.status_effect_row("off_guard")
check(not off_guard_row.is_empty() and String(off_guard_row.target_side) == "hero" and String(off_guard_row.unit) == "ac",
"失衡状态行(hero · ac 轴——妨害技落点,ConfigDB side 守卫正例)")
# 行集键 = (level_index, node_depth)——分支节点图批起 bugbear 等敌种跨关异池,enemy_id 不再唯一
# 行集键 = (level_index, node_depth, enemy_id)——分支节点图批起 bugbear 等敌种跨关异池,enemy_id 不再唯一;
# 2026-10-07 行集多行批:同坐标混编多行后每行各一池,键升维到三元组(行序 = 文件序 = 契约序)
var expect_pools := {
"1_0": {"attack": 9, "lunge": 1},
"1_1": {"attack": 9, "lunge": 1},
"1_2": {"attack": 6, "charge": 2, "heavy": 2},
"1_3": {"attack": 6, "charge": 2, "heavy": 2},
"1_4": {"attack": 6, "heavy": 2, "harden": 2, "quake_smash": 2},
"2_0": {"attack": 6, "lunge": 1, "heavy": 2},
"2_1": {"attack": 6, "lunge": 1, "heavy": 2},
"2_4": {"attack": 6, "bite": 2, "charge": 1},
"2_5": {"attack": 5, "lunge": 2, "heavy": 2},
"2_6": {"attack": 6, "bite": 2, "charge": 1},
"2_7": {"attack": 6, "bite": 2, "charge": 1},
"2_9": {"attack": 6, "heavy": 2, "venom_burst": 2, "web_snare": 2, "venom_curb": 2},
"3_0": {"attack": 6, "charge": 2, "heavy": 2},
"3_1": {"attack": 6, "bite": 2, "charge": 1},
"3_4": {"attack": 6, "bite": 2, "charge": 1},
"3_5": {"attack": 5, "lunge": 2, "heavy": 2},
"3_6": {"attack": 6, "bite": 2, "charge": 1},
"3_7": {"attack": 6, "charge": 2, "heavy": 2},
"3_9": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
"1_0_goblin": {"attack": 9, "lunge": 1},
"1_1_goblin": {"attack": 9, "lunge": 1},
"1_2_bugbear": {"attack": 6, "charge": 2, "heavy": 2},
"1_3_hobgoblin": {"attack": 6, "charge": 2, "heavy": 2},
"1_4_ogre_chief": {"attack": 6, "heavy": 2, "harden": 2, "quake_smash": 2},
"2_0_bugbear": {"attack": 6, "lunge": 1, "heavy": 2},
"2_1_bugbear": {"attack": 6, "lunge": 1, "heavy": 2},
"2_4_giant_spider": {"attack": 6, "bite": 2, "charge": 1},
"2_5_bugbear": {"attack": 5, "lunge": 2, "heavy": 2},
"2_6_giant_spider": {"attack": 6, "bite": 2, "charge": 1},
"2_7_giant_spider": {"attack": 6, "bite": 2, "charge": 1},
"2_9_drider": {"attack": 6, "heavy": 2, "venom_burst": 2, "web_snare": 2, "venom_curb": 2},
"3_0_duergar": {"attack": 6, "charge": 2, "heavy": 2},
"3_1_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
"3_4_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
"3_5_duergar": {"attack": 5, "lunge": 2, "heavy": 2},
"3_6_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
"3_7_duergar": {"attack": 6, "charge": 2, "heavy": 2},
"3_9_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
}
var pools_ok := true
for row in _db.enemy.rows:
var rk := "%d_%d" % [int(row.level_index), int(row.node_depth)]
var rk := "%d_%d_%s" % [int(row.level_index), int(row.node_depth), String(row.enemy_id)]
if not expect_pools.has(rk):
pools_ok = false
continue
@@ -3081,7 +3083,7 @@ func _smoke_skill_pool_data() -> void:
got[String(entry.skill_id)] = int(entry.weight)
if got != expect_pools[rk]:
pools_ok = false
check(pools_ok, "十九行池模板 = 建议池行值(L1 地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔(2,9)——商店房批 Boss 重编号〕;L3 灰矮人 6/2/2 · 钩爪恐魔 6/2/1 · 重装手 5/2/2 · 石君王 6/2/2〔earth_pulse·(3,9)〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2+unlock_hp_pct 50——迭代14④ T5〕)")
check(pools_ok, "十九行九种池模板 = 建议池行值((L,D,enemy_id) 三元键逐行精确比对——2026-10-07 行集多行批口径;L1 地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔(2,9)——商店房批 Boss 重编号〕;L3 灰矮人 6/2/2 · 钩爪恐魔 6/2/1 · 重装手 5/2/2 · 石君王 6/2/2〔earth_pulse·(3,9)〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2+unlock_hp_pct 50——迭代14④ T5〕;混编行集同坐标多行各一池合法,零内容期每坐标仍恰一行)")
var basic_count_ok := true
for row in _db.enemy.rows:
var n := 0
@@ -3515,7 +3517,8 @@ func _smoke_level_graph() -> void:
check(green_ok, "首遇位零踩雷:完美档 12 种子(节点 0 熊地精)恒零踩雷")
check(green_wins >= 7, "首遇战胜局 ≥7/12(实际 %d——阶段拔高后锚行退出绿档,多数胜口径)" % green_wins)
check(green_min_hp > 0, "首遇战胜后 HP 下探读数 %d(>0——绿档记录)" % green_min_hp)
# ⑦ 跨表行完整:L2 每战斗/Boss 节点 (2, 编号) 在 board_gen 与 enemy 精确行(加载校验同款显式复述)
# ⑦ 跨表行完整:L2 每战斗/Boss 节点 (2, 编号) 在 board_gen 与 enemy 精确行集(加载校验同款显式复述;
# 2026-10-07 行集多行批:「精确行」口径改「精确行集」——enemy 侧同坐标多行合法,键聚合坐标级天然兼容)
var bg_keys := {}
for row in _db.board_gen.rows:
bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
@@ -3530,12 +3533,21 @@ func _smoke_level_graph() -> void:
var key := "2_%d" % int(node.node_depth)
if not bg_keys.has(key) or not en_keys.has(key):
cross_ok = false
check(cross_ok, "跨表行完整:L2 全部战斗/Boss 节点在 board_gen + enemy 精确行(7 节点)")
var l2_spider: Dictionary = _db.resolve_enemy_rowset(2, 4)[0] # floor 版退役改 ceil 行集首行(坐标精确命中零行为差——P2 甲)
check(String(l2_spider.get("enemy_id", "")) == "giant_spider", "巨蜘蛛首遇行集 (2,4) = giant_spider(固定 2 只——首遇教学位不加骰)")
var l2_boss: Dictionary = _db.resolve_enemy_rowset(2, 9)[0] # 同上(P2 甲)
check(String(l2_boss.get("enemy_id", "")) == "drider" and String(l2_boss.get("armor_type", "")) == "ward",
"蛛化卓尔精灵 Boss 行集 (2,9) = drider(魔法甲——护甲类型重配批 2026-09-18:关卡 2 唯一非轻甲;商店房批重编号 (2,8)→(2,9))")
check(cross_ok, "跨表行完整:L2 全部战斗/Boss 节点在 board_gen + enemy 精确行集(7 节点)")
# ⑦a 定点:L2 首遇行集与 Boss 行集(2026-10-07 行集多行批口径——断言行集构成而非单行:
# 零内容期每坐标恰一行 = 单元素行集;行序 = 文件序,首行即叙述取数面)
var l2_spider_set: Array = _db.resolve_enemy_rowset(2, 4)
var l2_spider_ids := {}
for r in l2_spider_set:
l2_spider_ids[String(r.get("enemy_id", ""))] = true
check(l2_spider_ids.has("giant_spider") and l2_spider_ids.size() == 1 and l2_spider_set.size() == 1,
"巨蜘蛛首遇行集 (2,4) = {giant_spider} 恰一行(固定 2 只——首遇教学位不加骰;行集口径断言——行集构成纪律下单行集 = 现行形态)")
var l2_boss_set: Array = _db.resolve_enemy_rowset(2, 9)
var l2_boss_ids := {}
for r in l2_boss_set:
l2_boss_ids[String(r.get("enemy_id", ""))] = true
check(l2_boss_ids.has("drider") and l2_boss_ids.size() == 1 and String(l2_boss_set[0].get("armor_type", "")) == "ward",
"蛛化卓尔精灵 Boss 行集 (2,9) = {drider} 恰一行且首行魔法甲(护甲类型重配批 2026-09-18:关卡 2 唯一非轻甲;商店房批重编号 (2,8)→(2,9);P4 乙 Boss 行集多行合法——零内容期仍单行)")
# ---- 第三关内容批(2026-09-20)L3 灰矮人矿城断言段 ----
# ⑧ L3 行集结构(复用 L2 九节点图模板——6 战 + Boss + 奖励 + 恢复,真实岔路 4 处)
var l3 := {}
@@ -3552,7 +3564,7 @@ func _smoke_level_graph() -> void:
check(int(tcount3.battle) == 6 and int(tcount3.boss) == 1, "L3 普通战斗节点 = 6 + Boss 恒一(编号最大恒终点)")
check(int(tcount3.reward) == 1 and int(tcount3.rest) == 1 and int(tcount3.shop) == 1, "L3 恢复节点 1(rest_heal_pct 行字段)+ 奖励节点 1(直通空态)+ 商店房节点 1(商店房批)")
check(branch3 == 3, "L3 真实岔路 = %d 处(0→[1,2,3,4,5] · 2→[6,7] · 5→[6,7]——U2 返工批五岔环廊层宽 5)" % branch3)
# ⑨ 跨表行完整:L3 每战斗/Boss 节点 (3, 编号) 在 board_gen 与 enemy 精确行(⑦ 的 bg_keys/en_keys 复用)
# ⑨ 跨表行完整:L3 每战斗/Boss 节点 (3, 编号) 在 board_gen 与 enemy 精确行集(⑦ 的 bg_keys/en_keys 复用)
var cross_ok3 := true
for node in _level_active_nodes(3):
var t3 := String(node.get("type", ""))
@@ -3561,14 +3573,20 @@ func _smoke_level_graph() -> void:
var key3 := "3_%d" % int(node.node_depth)
if not bg_keys.has(key3) or not en_keys.has(key3):
cross_ok3 = false
check(cross_ok3, "跨表行完整:L3 全部战斗/Boss 节点在 board_gen + enemy 精确行(7 节点)")
# ⑩ 定点:L3 首遇行集与 Boss 行集(克制面重轻错开——重甲灰矮人首次登台)
var l3_lead: Dictionary = _db.resolve_enemy_rowset(3, 0)[0] # 同上(P2 甲)
check(String(l3_lead.get("enemy_id", "")) == "duergar" and String(l3_lead.get("armor_type", "")) == "heavy",
"灰矮人首遇行集 (3,0) = duergar(重甲首次登台——固定 1 只,首遇教学位不加骰)")
var l3_king: Dictionary = _db.resolve_enemy_rowset(3, 9)[0] # 同上(P2 甲)
check(String(l3_king.get("enemy_id", "")) == "duergar_king" and String(l3_king.get("armor_type", "")) == "heavy",
"灰矮人石君王 Boss 行集 (3,9) = duergar_king(重甲收束「轻甲→魔法甲→重甲」教学梯度;商店房批重编号 (3,8)→(3,9))")
check(cross_ok3, "跨表行完整:L3 全部战斗/Boss 节点在 board_gen + enemy 精确行集(7 节点)")
# ⑩ 定点:L3 首遇行集与 Boss 行集(克制面重轻错开——重甲灰矮人首次登台;行集口径断言同 ⑦a)
var l3_lead_set: Array = _db.resolve_enemy_rowset(3, 0)
var l3_lead_ids := {}
for r in l3_lead_set:
l3_lead_ids[String(r.get("enemy_id", ""))] = true
check(l3_lead_ids.has("duergar") and l3_lead_ids.size() == 1 and String(l3_lead_set[0].get("armor_type", "")) == "heavy",
"灰矮人首遇行集 (3,0) = {duergar} 恰一行且首行重甲(重甲首次登台——固定 1 只,首遇教学位不加骰)")
var l3_king_set: Array = _db.resolve_enemy_rowset(3, 9)
var l3_king_ids := {}
for r in l3_king_set:
l3_king_ids[String(r.get("enemy_id", ""))] = true
check(l3_king_ids.has("duergar_king") and l3_king_ids.size() == 1 and String(l3_king_set[0].get("armor_type", "")) == "heavy",
"灰矮人石君王 Boss 行集 (3,9) = {duergar_king} 恰一行且首行重甲(重甲收束「轻甲→魔法甲→重甲」教学梯度;商店房批重编号 (3,8)→(3,9))")
# ⑪ L3 成长代理档首遇绿档:模拟 GP 商店成长后的入关参数(atk_pct=200 / hp_pct=100)· 完美档多种子首遇战稳过
# (零加成基线红档属预期——断言口径 = 成长代理档,非基线;跑法同 ⑥)
var green3_ok := true
@@ -3594,6 +3612,31 @@ func _smoke_level_graph() -> void:
check(green3_ok, "L3 首遇位绿档:成长代理档口径(atk_pct=200/hp_pct=100)完美档 12 种子首遇战(节点 0 灰矮人)全胜且零踩雷")
check(green3_min_hp > 0, "L3 首遇战胜后 HP 下探读数 %d(>0——绿档记录)" % green3_min_hp)
# ⑫ 混编行集运行面正反断言(2026-10-07 行集多行批——T4 组 D 新增):注入坐标 (1,5)/(1,6) =
# L1 节点图与全部既有扫描面之外的新深度(ceil 请求命中注入行集——新 L 会被 ceil 封顶到 L3 不可用),
# 断言后立即还原表,后续断言面零污染。R3 fail-fast 正反验证 + R6 行序运行面验证。
var mix_row_a := {"level_index": 1, "node_depth": 5, "enemy_id": "goblin", "display_name": "地精", "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}
var mix_row_b := {"level_index": 1, "node_depth": 5, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 2, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]}
_db.enemy.rows.append(mix_row_a)
_db.enemy.rows.append(mix_row_b)
var mix_sys = load("res://core/unit_container.gd").new(_db)
check(mix_sys.spawn(1, 5, int(_db.hero.max_hp)), "混编行集 spawn 受理((1,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)")
var mix_units: Array = mix_sys.all_enemies()
check(mix_units.size() == 3, "混编实例总数 = 各行数量求和(1+2=3——行集实例总数 ≤4 守卫内)")
check(String(mix_units[0].get("enemy_id")) == "goblin" and String(mix_units[1].get("enemy_id")) == "bugbear" and String(mix_units[2].get("enemy_id")) == "bugbear",
"混编生成序 = 文件行序(行序即契约——地精在前熊地精在后,handle_id 跨行连续派发)")
check(String(mix_units[0].get("display_name")) == "地精" and int(mix_units[1].get("hp")) == 20 and int(mix_units[1].get("atk")) == 3,
"混编各行行值独立投影(display_name/hp/atk 按各自行值——同场不同种不同值)")
_db.enemy.rows.append({"level_index": 1, "node_depth": 6, "enemy_id": "goblin", "display_name": "地精", "unit_count": 3, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]})
_db.enemy.rows.append({"level_index": 1, "node_depth": 6, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 3, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]})
var over_sys = load("res://core/unit_container.gd").new(_db)
check(not over_sys.spawn(1, 6, int(_db.hero.max_hp)), "行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)")
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
check(int(_db.enemy.rows.size()) == 19, "混编断言段注入行全量还原(表回 19 行——后续断言面零污染)")
# 冒烟隔离用全新 RngService(同 master 种子——回放确定性纪律)
func _fresh_rng(seed_val: int):
@@ -23,6 +23,7 @@ func _init() -> void:
_test_shop_accessors(db)
_test_meta_accessors(db)
_test_manifest_dir_sync(db)
_test_rowset_discipline()
quit(Kit.finish("config_db_test"))
@@ -191,3 +192,52 @@ func _test_manifest_dir_sync(db) -> void:
if not disk_files.has(String(manifest[key])):
synced = false
Kit.check("TABLE_MANIFEST 与 data/tables 目录逐文件同步(无缺登/无悬登)", synced)
# ---- 行集构成纪律(2026-10-07 敌人表行集多行批——EnemyTable.validate 正反验证;资源直构不碰生产表)----
# 纪律三条(敌人系统 v0.15):①行集内 enemy_id 唯一(dup_key 既有通道)②同坐标多行文件内相邻
# (rowset_rule——行序即契约,R6 加载期兜底)③行集实例总数 ≤4(骰后值运行面,spawn 守卫——冒烟侧验证)。
func _test_rowset_discipline() -> void:
const EnemyTableScript := preload("res://data/schemas/enemy_table.gd")
var mk_row := func(l: int, d: int, eid: String) -> Dictionary:
return {"level_index": l, "node_depth": d, "enemy_id": eid, "display_name": eid + "名",
"unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}
# 正向:同坐标两行不同敌种(相邻)= 混编合法,validate 零错
var t_mix = EnemyTableScript.new()
t_mix.rows = [mk_row.call(1, 5, "goblin"), mk_row.call(1, 5, "bugbear")]
var errs: Array = t_mix.validate()
Kit.check("混编行集加载通过(同坐标两行不同敌种相邻——行集恰一敌种校验退役的正向面)", errs.is_empty())
# 正向:Boss 坐标两行同构合法(P4 乙——Boss 行集校验同样放开,加载层无 Boss 单行硬闸)
var t_boss = EnemyTableScript.new()
t_boss.rows = [mk_row.call(2, 9, "drider"), mk_row.call(2, 9, "bugbear")]
errs = t_boss.validate()
Kit.check("Boss 坐标混编行集加载通过(P4 乙——Boss 多行合法,无硬闸)", errs.is_empty())
# 负向:同坐标行不相邻 = rowset_rule 且报错定位到坐标(R6 行序纪律 + R3 错误类别可定位)
var t_gap = EnemyTableScript.new()
t_gap.rows = [mk_row.call(1, 5, "goblin"), mk_row.call(1, 9, "hobgoblin"), mk_row.call(1, 5, "bugbear")]
errs = t_gap.validate()
var gap_hit := false
for e in errs:
if String(e.get("category", "")) == "rowset_rule" and String(e.get("message", "")).contains("(1,5)"):
gap_hit = true
Kit.check("行序相邻校验:同坐标行被隔断报 rowset_rule 且定位坐标(R6——行序即契约加载期兜底)", gap_hit)
# 负向:同敌种第二行仍被 dup_key 拦(键含 enemy_id——纪律①既有通道不回归)
var t_dup = EnemyTableScript.new()
t_dup.rows = [mk_row.call(1, 5, "goblin"), mk_row.call(1, 5, "goblin")]
errs = t_dup.validate()
var dup_hit := false
for e in errs:
if String(e.get("category", "")) == "dup_key":
dup_hit = true
Kit.check("行集内同敌种第二行仍被 dup_key 拦(纪律①——enemy_id 唯一)", dup_hit)
# 负向:域校验不回归(单行 unit_count 越域仍拦——UNIT_COUNT_MAX 单种数语义下的同款域闸)
var t_dom = EnemyTableScript.new()
t_dom.rows = [mk_row.call(1, 5, "goblin")]
t_dom.rows[0]["unit_count"] = 4
errs = t_dom.validate()
var dom_hit := false
for e in errs:
if String(e.get("category", "")) == "domain" and String(e.get("message", "")).contains("unit_count"):
dom_hit = true
Kit.check("单行数量域 [1,3] 域闸不回归(UNIT_COUNT_MAX 语义 = 单种数)", dom_hit)