// 【形状】的单测 —— 2026-07-16 第二刀(删加权求和)立的。 // // 这个文件守三件事,重要性从高到低: // ①【变化审计】这一刀【必须】让输出变(七朝原文里没有加权求和的原型)。所以这里【不是迁移锁】: // 它拿旧引擎的 1128 个 profile × 七朝跑新旧两版,把【每一处变化】归进一个说得出依据的类, // 而「我说不清为什么变的」那一类【必须为空】。★ 这一条就是这一刀的验收。 // ②【形状判别式】数据里不许再有加权求和,也不许换个名字回来。扫的是【数据】,不是注释。 // ③【原文复现】熙宁武举那一段原文,新引擎必须逐档复现出来。 // ★ 这是全表唯一一处「通过计数 → 位阶」有原文原型的地方,它绿着,这一刀才站得住。 // // ⚠ 本文件【取代了】 dynasties-tracks.test.js 里那两条「迁移锁」。为什么取代、凭什么取代, // 写在那边的注里。一句话:迁移锁证的是「输出没变」,而这一刀的任务【就是让它变】—— // 拿一把证「没变」的尺子来验一刀「必须变」的活,尺子本身就用错了。 // ★ 但【覆盖面只增不减】:迁移锁钉的是五朝的指纹;本文件钉的是【七朝 × 1128 个 profile 的 // 每一格】,而且每一格的差异都必须说得出依据。 // // 跑:node --test test/dynasties-shape.test.js import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { DYNASTIES, computeRank, getDynasty, profileFromSubs, tracksOf } from '../src/dynasties.js'; import { SUB_REGISTRY } from '../src/talents.js'; import { closedBy } from '../src/chushen.js'; const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n)); // 【加权求和版】引擎的产出 —— 这一刀的「被告」。采集脚本:scripts/capture-weighted-sum.mjs const OLD = JSON.parse(fs.readFileSync(new URL('./fixtures/dynasties-weighted-sum.json', import.meta.url), 'utf8')); // 多轨化【之前】旧代码采的那份(含 5 朝的【完整】输出样本)—— 用来交叉验证上面那份是真的。 const PRE = JSON.parse(fs.readFileSync(new URL('./fixtures/dynasties-pre-multitrack.json', import.meta.url), 'utf8')); // ⚠ 必须与 scripts/capture-weighted-sum.mjs 的 proj() 【逐字相同】,否则新旧对照对的不是同一批格子。 const proj = (r) => (r === null ? null : { ch: r.channel ? r.channel.id : null, tier: r.rank.tier, title: r.rank.title, grade: r.rank.grade, unit: r.rank.unit, ceil: r.ceiling ? r.ceiling.tier : null, cat: r.category || '', stig: !!r.stigma, blk: r.blocked.length, }); const profileOf = (key) => { const [subsRaw, wRaw] = key.split('|'); return profileFromSubs(subsRaw === '∅' ? [] : subsRaw.split('+'), { wang: Number(wRaw.slice(1)) }); }; const chOf = (dynId, chId) => getDynasty(dynId).channels.find((c) => c.id === chId) || null; const xuanOf = (c) => (c && c.xuan) || { seat: 0, kemu: [], key: [], keySource: null }; // ══════════════════════════════════════════════════════════════════════════ // ① 自证:那份「旧版」参照,确实是【旧引擎】的产出 —— 不是我为了让差异好看而编的 // // ★★ 本项目铁律:【最难发现的错是「假订正」】——「写『原文无 X,已订正』而实际只有一个见证」。 // 一份自己造的「旧版」参照,就是假订正的完美温床:想让哪一格「该变」,改一下参照即可。 // → 所以这里【不信任采集脚本的自述】,拿【另一份、更早、由旧代码采的】 fixture 交叉验证: // dynasties-pre-multitrack.json 存了 6 个 profile × 5 朝的【完整】输出(不是投影)。 // 把它投影一遍,必须与 dynasties-weighted-sum.json 里同样的格子逐字相同。 // ⇒ 两份 fixture、两次独立采集、对得上 ⇒ 这个「被告」是真的。(这就是本项目的双证。) // ══════════════════════════════════════════════════════════════════════════ test('★★ 自证:「旧版」参照是真的旧引擎产出(与多轨化前那份 fixture 双证)', () => { let checked = 0; for (const [key, byDyn] of Object.entries(PRE.sample)) { for (const [d, full] of Object.entries(byDyn)) { assert.deepEqual( OLD.rows[key][d], proj(full), `${d}/${key}:weighted-sum 参照与 pre-multitrack 对不上 —— 参照本身不可信,整个审计作废` ); checked++; } } assert.ok(checked >= 30, `交叉验证的格子太少(${checked}),这条自证没在证东西`); assert.equal(Object.keys(OLD.rows).length, PRE._matrix.count, '两份 fixture 的 profile 矩阵必须同构'); }); // ══════════════════════════════════════════════════════════════════════════ // ② 形状判别式 —— 扫【数据】,不扫注释。注释挡不住任何人,判别式可以。 // ══════════════════════════════════════════════════════════════════════════ test('★★ 加权求和不许复活 —— 也不许换个名字回来', () => { for (const d of DYNASTIES) { for (const c of d.channels) { // 七朝原文零命中(chiduo.js SHAPE_DIFF.weightedSumOccurrences = 0)。 assert.equal(c.weights, undefined, `${d.name}/${c.name} 的 weights 回来了 —— 原文里没有这个形状`); // ✗ 换个名字也不行。这一条挡的是「删了 weights,改叫 total/points 再加起来」。 for (const alias of ['score', 'total', 'points', 'weight', 'scores']) { assert.equal(c[alias], undefined, `${d.name}/${c.name} 冒出了「${alias}」—— 加权求和换了个名字回来`); } const x = xuanOf(c); // ★ kemu 只能【数】:过,或不过。一条科目【不许】带系数 —— 带了系数再相加,就是加权求和。 for (const k of x.kemu || []) { assert.equal(typeof k.axis, 'string', `${d.name}/${c.name} 的科目缺 axis`); assert.equal(typeof k.need, 'number', `${d.name}/${c.name} 的科目缺 need(门限)`); assert.ok(k.name, `${d.name}/${c.name} 的科目必须有【原文的词】做名字`); assert.equal(k.weight, undefined, `${d.name}/${c.name}/${k.name} 的科目带了权重 —— 科目只能数,不能加权`); } } } }); test('★ 每条通道都必须有【选】,且它的三样东西都在', () => { for (const d of DYNASTIES) { for (const c of d.channels) { const x = c.xuan; assert.ok(x, `${d.name}/${c.name} 没有【选】—— 那它拿什么定位阶?`); assert.equal(typeof x.seat, 'number', `${d.name}/${c.name} 的 seat 必须是一阶`); // ⚠ 2026-07-17 接 25 朝:seat 的范围【随它所在的那把尺走】,不再写死 0–5。 // · 上界 = 这条通道所在梯子的最高一格(二十等爵到 20、九品到 18…); // · 下界:允许 -1 = 【梯下】(北周牧守自辟的州吏落在「不命」,原文明记,非 tier0)。 const trkOfC = d.tracks.find((t) => t.id === c.track) || d.tracks[0]; const maxTierC = Math.max(...trkOfC.ladder.map((l) => l.tier)); assert.ok(Number.isInteger(x.seat) && x.seat >= -1 && x.seat <= maxTierC, `${d.name}/${c.name} 的 seat(${x.seat}) 必须落在这把尺上([-1, ${maxTierC}])`); assert.ok(Array.isArray(x.kemu), `${d.name}/${c.name} 的 kemu 必须是数组(空 = 这条路没有量表)`); // ⚠ 2026-07-18 秦:排序键【可以为空】—— 但【只有当这条路没有可排的东西时】。 // 秦军功/赐爵:kemu 空、gate 空、六轴一条都接不住「斩首级」⇒ 原文不给次序 ⇒ key 空、keySource null // (xuan/qin.js 逐字论证;dynasties.js 已删掉 defaultKey,免得【补写】一个 wu 键假装完整 —— // 补 wu 正是 qin.js 明令禁的头一条:「果决不是军功」)。 // ★ 而一条【有科目可数】(kemu 非空) 的路,key【仍然】不许空 —— 有东西要排却不给次序,那才是漏。 assert.ok(Array.isArray(x.key), `${d.name}/${c.name} 的排序键必须是数组`); if (!x.key.length) { assert.equal((x.kemu || []).length, 0, `${d.name}/${c.name} 有科目可数(kemu 非空),排序键却是空的 —— 有东西要排却没给次序`); assert.equal(x.keySource, null, `${d.name}/${c.name} 排序键为空,却号称有出处(keySource 非 null) —— 没有键哪来的出处`); } // ★ seat 不许越过这条通道自己的天花板 —— 位子在天花板之上,那这个天花板就是假的。 // ⚠ 天花板未记(null)时兜底取【本梯最高格】,与 computeRank 的 tierOf 同口径(不再写死 5)。 assert.ok( x.seat <= (c.ceiling?.tier ?? maxTierC), `${d.name}/${c.name} 的位子(${x.seat})高过它自己的天花板(${c.ceiling?.tier}) —— 天花板成了摆设` ); // ⚠ keySource:有出处的必须真带卷次;没出处的必须【明写 null】,不许含糊其辞。 // ✗ 这一条挡的是「哪天有人给排序键补一个像模像样的出处」。 assert.ok('keySource' in x, `${d.name}/${c.name} 必须表态:这个排序键有没有原文可依`); if (x.keySource !== null) { assert.match(x.keySource, /卷/, `${d.name}/${c.name} 的排序键号称有出处,那就必须带卷次`); } } } }); test('★★★ 全表【只有一条】通道的排序键有原文可依 —— 这是结论,不是缺', () => { // ★ 老板说「唐的排序键在原文里是现成的:[德行, 才, 劳]」。核完原文,答案是: // 【在原文里是现成的,在本表的词汇表里不是】—— // (1) 「則先德行;德均以才,才均以勞」是【铨选/校功】的尺子(《旧唐书》卷43「凡擇人以四才, // 校功以三實」两个动词劈开两套东西;《宋史》卷155 志序独立印证「入仕/服官/任事」三分), // 而本表的 channels【全是入仕】—— 引擎里没有那一段(chiduo.js CONFLICT #8); // (2) 「德行/才/勞」六轴里一个都没有(德行没有轴;劳是【时钟】,不许拿 heng 承载)。 // → 所以唐的 keySource 是 null。谁想给它补一个出处,先回答「德行是六轴里的哪一条」。 const withSource = []; for (const d of DYNASTIES) { for (const c of d.channels) if (xuanOf(c).keySource !== null) withSource.push(`${d.id}/${c.id}`); } assert.deepEqual( withSource, ['song/wuju'], '有原文可依的排序键【只有熙宁武举一条】。多出来的那条,先拿出带卷次的双证;' + '少了它,说明武举那条的出处被人删了 —— 那这一刀就没有任何原文原型了。' ); // 而唐必须【明确地】没有出处 —— 不许含糊,也不许被「补」上。 for (const c of getDynasty('tang').channels) { assert.equal(xuanOf(c).keySource, null, `唐/${c.name} 的排序键不许挂出处:德行/才/劳,六轴里一条都没有`); } }); // ══════════════════════════════════════════════════════════════════════════ // ③ 原文复现 —— ★ 全表唯一一处「通过计数 → 位阶」有原文原型的地方 // // 《宋史》卷157·选举三(双证,且在本朝 span 熙宁 1068–1077 内): // 「策、武藝俱優為右班殿直,武藝次優為三班奉職,又次借職,末等三班差使; // 策入平等而武藝優者除奉職,次優借職,又次三班差使,武藝末等者三班差使。」 // 把它按「通了几条」数一遍,与本朝武选梯子严丝合缝 —— 这一条绿着,这一刀才不是我编的形状。 // ══════════════════════════════════════════════════════════════════════════ test('★★★ 熙宁武举:新引擎必须逐档复现《宋史》卷157 那张授官表', () => { const at = (subs) => computeRank(profileFromSubs(subs, { wang: 0 }), 'song'); // ★★ 通 0 条【而且策满格】:武艺这一条不通 → 后面的策【根本不查】→ 仍是三班差使。 // 这一格就是「前缀计数」与「数一共通几条」的分水岭,也是这一刀最容易失守的地方: // 若哪天它变成了 tier 2,说明【策又在给武艺补分】= 加权求和从计数这个后门爬回来了。 const moDengCeMan = at(['blunt', 'rigorous', 'datadriven', 'concise', 'shortfirst']); assert.equal(moDengCeMan.channel.id, 'wuju'); assert.equal( moDengCeMan.rank.tier, 1, '★ 武艺末等而策满格 →【三班差使】。原文:「武藝末等者三班差使」—— 策再好也补不回来' ); assert.equal(moDengCeMan.rank.title, '三班差使'); // 通 0 条:武艺不到「优」(只点过一项)、策不通 →【武藝末等者三班差使】 const modeng = at(['blunt']); assert.equal(modeng.channel.id, 'wuju'); assert.equal(modeng.rank.tier, 1, '通 0 条 → 三班差使(原文:武藝末等者三班差使)'); assert.equal(modeng.rank.title, '三班差使'); assert.match(modeng.rank.grade, /武艺末等者授/, '梯子这一阶的 grade 就是原文的话'); // 通 1 条:武艺优、策不通 →【策入平等而武藝優者除奉職】 const wuyou = at(['blunt', 'motivate']); assert.equal(wuyou.channel.id, 'wuju'); assert.equal(wuyou.rank.tier, 2, '通 1 条 → 三班奉职(原文:策入平等而武藝優者除奉職)'); assert.match(wuyou.rank.title, /三班借职 \/ 三班奉职/); assert.match(wuyou.rank.grade, /武艺次优者授/); // 通 2 条:策、武艺俱优 →【策、武藝俱優為右班殿直】 const juyou = at(['blunt', 'motivate', 'rigorous']); assert.equal(juyou.channel.id, 'wuju'); assert.equal(juyou.rank.tier, 3, '通 2 条 → 右班殿直(原文:策、武藝俱優為右班殿直)'); assert.match(juyou.rank.title, /右班殿直/); assert.match(juyou.rank.grade, /策、武艺俱优者授/); // ★★ 而这三档合起来就是【tier = 1 + 通过条数】—— seat=1 不是本表的编号, // 它是「武藝末等者三班差使」这七个字。 assert.equal(xuanOf(chOf('song', 'wuju')).seat, 1); assert.ok(juyou.rank.tier - modeng.rank.tier === 2, '两条之差 = 两阶之差'); // 排序键的次序也必须是原文的:武艺在前。 assert.deepEqual(xuanOf(chOf('song', 'wuju')).key, ['wu', 'fa'], '武艺在前、策在后 —— 原文如此,不许对调'); }); test('★ 通过计数【不跨通道相加】—— 每条通道自带自己的分母', () => { // 唐的每一科自带分母(进士数「策五道+帖一大经」,明法数「律七条+令三条」)—— // 「进士的甲第与明法的甲第不可比」。所以一条通道的科目【只能】数它自己的。 // 这里守的是数据形状:任何一条科目都不许引用别的通道。 for (const d of DYNASTIES) { for (const c of d.channels) { for (const k of xuanOf(c).kemu || []) { assert.equal(k.channelId, undefined, `${d.name}/${c.name} 的科目引用了别的通道 —— 那就是跨科相加`); assert.equal(k.from, undefined, `${d.name}/${c.name} 的科目引用了别的通道`); } } } }); // ══════════════════════════════════════════════════════════════════════════ // ④ ★★★ 变化审计 —— 这一刀的验收 // // 老板的原话:「对每一个结果变了的 profile,说明它为什么该变(哪条原文要求它这么变)。 // 变化数量会很大 —— 抽样说明 + 分类统计即可,但【必须有一类是「我说不清为什么变的」】, // 且那一类【必须为空】才算过。」 // // ⚠ 每一类都必须有一个【可证伪的谓词】,不能只是「我给它起了个名字」。 // 一个类如果只是「这条通道的变化都算它」,那它什么都没证明 —— 它只是把变化重新命名了一遍。 // 所以下面每一类都验【机理】:新的 tier 必须【正好】等于那条机理算出来的数。 // 对不上的,一律掉进 UNEXPLAINED,测试当场红。 // ══════════════════════════════════════════════════════════════════════════ // 一条通道在新引擎下的位阶(复算机理,用来验证「变化确实是这个机理造成的」)。 // ⚠ 这【不是】把 computeRank 抄一遍:它只复算【一条给定通道】的位阶,不做择路。 // ★ 前缀计数:数到第一条不通的就停 —— 原文「武藝末等者三班差使」(武艺不通,策再好也不补)。 // ⚠ 这里【必须】和引擎用同一个口径,否则审计会把一处真的互补当成「机理算出来的」放过去。 function tierByMechanism(dynId, chId, val) { const c = chOf(dynId, chId); const x = xuanOf(c); let n = 0; for (const k of x.kemu || []) { if (val(k.axis) < k.need) break; // ★ 不通 → 停,后面的不查 n++; } return clamp(x.seat + n, 0, c.ceiling?.tier ?? 5); } const CLASSES = { A: '【无量表 → 位子取代加权分】这条路没有一条可数的科目(原文不给量:汉四科是分类/南齐舍人靠久勞親信/' + '辽无选举志),所以位阶恒等于这条路的位子。旧值是加权求和的产物 —— 而它的区分度来自' + '【没被这条路量过的那些轴的基线 40 × 权重】,即一个没量你的东西在给你加分。', B: '【熙宁武举 → 改按原文的通过计数】《宋史》卷157「策、武藝俱優為右班殿直……武藝末等者三班差使」' + '(双证、在 span 内)。梯子本来就是照这段排的,旧引擎没读它:256 个开着这条路的 profile 里' + '248 个被算成右班殿直,包括策一条都不通的人。', C: '【宋恩荫 → 从选人起家,不再一步到天花板】本表【自己】写着「本官须举主改官,多滞选人」,' + '而文选梯子 tier1 逐字就是「选人(幕职州县官)」。旧引擎却把 846 个里的 823 个直接放在' + '知州 = 这条路的天花板上。★ 天花板不是位子。', D: '【南朝三轨:士族、舍人、军功各有各的梯子】舍人这条路没有量表(久勞、親信),而这条路【就是】' + '一个位子,所以走进来的人就在那一格;反过来,士族清途【根本不在那条梯子上】。' + '★ 2026-07-17 第四刀:上一刀让士族从 tier4 挪到 tier3(不再落在舍人的鞋里),' + '而【病根没除】—— 那条梯子仍是【共用】的:士族的天花板 tier5(尚书令) 在舍人那一格【之上】,' + '即本表仍在说「士族要当尚书令得先经过中书通事舍人」。这一刀把梯子拆了:' + '士族清途的每一级都出《南齐书》卷16(双证),tier4 是【侍中】(志:「齊世朝會,多以美姿容者兼官」),' + '不是舍人。⇒ 南朝 shizu 的 title/grade/unit 整片变,tier 仍是 seat=3(本表的编号,未动)。', // ── 2026-07-17 第四刀新增 ── G: '【多轨朝代的 tier0 不再替你站队 —— schema 第五个洞补上了】' + '梯子是【通道】带来的(trackOfChannel),不是朝代带来的 ⇒ 一条通道都没走进去的人,' + '【不在任何一条梯子上】。而旧引擎在这一格取 tracks[0]:' + '★ 于是辽的无路者被报成「北面·无官」(北面官是契丹人的官),宋的被放在【文选】轨上,' + '南朝三轨之后更荒唐 —— 无路者被报成「在士族清途上」,而士族清途是 gate{wang:75},' + '他恰恰是最不可能在那条路上的人。⇒ 多轨朝代 unit: null(这个问题在这里问不出口);' + '★ 单轨朝代【逐字不变】(汉的 tier0「郡县小吏/斗食」是那把尺真实的最低一格)。' + '(这与 listDynasties 早就在做的 `unit: multi ? "multi" : ts[0].unit` 是同一个判断 ——' + '不是新规矩,是一条既有规矩漏了一个调用点。)', E: '【择路:同位阶同天花板时,不再按加权分挑】旧引擎最后一级比的是加权分,而权重是发明的。' + '删掉权重,这一级换成【字典序(唐:德均以才,才均以勞)→ 本表的编号】。' + '★ 变的是这一级,不是位阶、也不是天花板:新挑的那条,在新规则下不比旧挑的那条差。', // ── 2026-07-17 第三刀(出身)新增 ── F: '【辽:出身没定 → 北面两条路关着,不再由能力替出身做决定】' + '《辽史》卷45 正文:「凡契丹軍馬皆屬焉」(北枢密院·武铨)、「凡契丹人民皆屬焉」(南枢密院·文铨)' + '——这两句【一直躺在 dynasties.js 自己的 sourceText 里,旧引擎没读它】。' + '★ 旧引擎拿六轴给辽择路,等于替用户答了「你是谁家的人」这一问(本表在辽一朝自陈过这个失真)。' + '新引擎:出身没定 ⇒ 【属】的路全关,reasons 里明写「这一部传还没定出身」。' + '★★ 这一格【故意难看】:宁可让用户看见一个没答的问题,也不许引擎替他答。' + '(1128 个 fixture profile 全都没有出身 —— 它们是加出身【之前】采的,所以辽这一朝整片变。)', // ── 2026-07-18 史学审计新增 ── H: '【魏晋:删掉那条【自造的整齐降序官品】—— 无路者 tier0「不入流」的 grade「无品」随之消失】' + 'dynasties.js 逐字:旧表自造了「无品→七→六→五→四→三品」的整齐降序,与《通典》卷37晋官品不合' + '(散骑常侍应三品,且与本表 qingtan.ceiling『散骑常侍三品』自相矛盾;诸侍郎应五品;郡丞应八品非六品)。' + '★ 引擎按 ordinal tier 裁决,grade 只是描述性标注,删了不改裁决 —— 宁缺一条假整齐降序。' + '⇒ 走进 weijin 通道的格子,其 grade 变化被 A(无量表→位子)认领(tier 仍等于 seat);' + ' 而【无路者那一格】(o.ch=n.ch=null、tier0「不入流」) 不进任何通道、A 认不了它,' + ' 它的唯一变化【正好只是那条假 grade 没了】。本类专认这一种,且谓词单格可证伪:' + ' 必须是 weijin、grade 由「有」变「无」、其余八项(tier/title/unit/ceil/cat/stig/blk)逐格全同。', }; test('★★★ 变化审计:1128 profile × 七朝,每一处变化都必须说得出依据;说不清的那一类必须为空', () => { const keys = Object.keys(OLD.rows); const stats = {}; const samples = {}; const unexplained = []; let changed = 0; let same = 0; for (const key of keys) { const p = profileOf(key); const axes = p.axes || {}; const wang = clamp(Number(p.wang || 0), 0, 100); const val = (k) => (k === 'wang' ? wang : clamp(Number(axes[k] || 0), 0, 100)); for (const d of DYNASTIES) { const o = OLD.rows[key][d.id]; // ⚠ 2026-07-17 加西汉(第八朝):OLD 是【多轨化前的旧引擎】采的 fixture,里面【没有西汉】。 // ★ 本审计是「这一刀让【旧有】朝代变了多少、每处变化说不说得出依据」——西汉是【全新】的, // 没有旧基线可比,不该进这个审计(也没有 o 可比)。⇒ fixture 里没有的朝代直接跳过。 if (!o) continue; const n = proj(computeRank(p, d.id)); if (JSON.stringify(o) === JSON.stringify(n)) { same++; continue; } changed++; let cls = null; if (o.ch !== n.ch) { // ── F:辽 —— 出身没定 ⇒【属】的路全关(2026-07-17 第三刀)── // ★ 谓词【可证伪,而且它验的是机理不是名字】: // ① 必须是辽(别的朝没有出身维,出身关不了它们的路); // ② 新引擎必须【一条路都不开】; // ③ ★ 而且必须【正好】是出身关的 —— closedBy('liao', null) 关掉的那几条, // 必须【覆盖辽的每一条通道】。少关一条,就说明这一格不是出身关的, // 那它就得掉进 UNEXPLAINED 让测试红。 // ⚠ 这三条合起来才拦得住「拿 F 当垃圾桶」:F 不是「辽的变化都算它」, // 它是「辽的这一格【正好】等于出身机理算出来的那个结果」。 if (d.id === 'liao' && n.ch === null) { const shut = closedBy('liao', null); if (getDynasty('liao').channels.every((c) => shut.closed.has(c.id))) cls = 'F'; } // ── E:择路变了 ── // 谓词:gate 一个字没改 ⇒ 旧引擎挑的那条【在新引擎下仍然开着】。 // 那么新引擎挑中的这条,在【位阶 → 天花板】这两级上【不许比它差】。 // ★ 若比它差 ⇒ 说明这一刀把择路弄坏了,不是「换了个 tiebreak」。 if (!cls && o.ch && n.ch) { const oldTierNow = tierByMechanism(d.id, o.ch, val); const oldCeil = chOf(d.id, o.ch).ceiling?.tier ?? 5; const newCeil = chOf(d.id, n.ch).ceiling?.tier ?? 5; if (n.tier > oldTierNow || (n.tier === oldTierNow && newCeil >= oldCeil)) cls = 'E'; } } else if (o.ch === null && n.ch === null) { // ── G:多轨朝代的 tier0 不再替你站队(2026-07-17 第四刀)── // ★ 谓词【可证伪,且验的是机理不是朝代名】—— 五条全中才算 G: // ① 必须是【多轨】朝代(单轨朝代的 tier0 是那把尺真实的最低一格,本刀【不许】动它, // 所以单轨朝代【永远不该】进 G —— 它进了就说明我把不该动的动了); // ② 新旧都是【一条路都不开】(这一格本来就只讲无路者); // ③ ★ 旧值【替他报了一条梯子】:o.unit 非 null 且 o.tier === 0 —— 那就是「站队」本身; // ④ ★ 新值【一条都不报】:n.unit === null 且 n.tier === 0; // ⑤ ★★ 而且【别的什么都没变】:ceil/cat/stig/blk 逐格相同 —— // 即这一格的变化【正好只是「哪条梯子」这一件事】。少了这一条,G 就成了垃圾桶 // (「反正 ch 都是 null 就算 G」),那正是 F 的注里警告过的用法。 // // ⚠ 这里【故意不验「旧值是 tracks[0] 的 t0」】,理由是南朝把它证伪了: // 南朝【本刀之前是单轨】,它的旧 t0 是那条【共用梯子】的第一格(不入流/无品/pin), // 而那条梯子这一刀【已经不存在了】。⇒ 拿现在的 tracks[0] 去比旧值,对辽/宋成立, // 对南朝【必然不成立】—— 第一版就是这么写的,南朝那 232 格当场没被认领。 // ★ 那不是南朝的例外,是谓词写错了:机理是【旧引擎给无路者报了一条梯子(不管哪条)】, // 不是【旧引擎报的是第一条】。验后者,等于把机理写成了它的一个偶然特例。 const ts = tracksOf(d.id); if ( ts.length > 1 && o.unit !== null && o.tier === 0 && n.unit === null && n.tier === 0 && o.ceil === n.ceil && o.cat === n.cat && o.stig === n.stig && o.blk === n.blk ) cls = 'G'; // ── H:魏晋删掉那条自造的整齐降序官品 ⇒ 无路者 tier0「不入流」的 grade「无品」没了(2026-07-18 审计)── // ★ 谓词【单格可证伪】:必须是 weijin、必须【只有 grade 由「有」变「无」】、其余八项逐格全同。 // tier/title/unit/ceil/cat/stig/blk 但凡有一处也变了,就不是「只删了假 grade」,掉回 UNEXPLAINED。 if ( !cls && d.id === 'weijin' && typeof o.grade === 'string' && n.grade === undefined && o.tier === n.tier && o.title === n.title && o.unit === n.unit && o.ceil === n.ceil && o.cat === n.cat && o.stig === n.stig && o.blk === n.blk ) cls = 'H'; } else if (o.ch === n.ch && n.ch) { // ── 同一条路,位阶变了 ── const x = xuanOf(chOf(d.id, n.ch)); const byMech = tierByMechanism(d.id, n.ch, val); if (n.tier !== byMech) { cls = null; // 机理算不出这个数 ⇒ 说不清 } else if (d.id === 'song' && n.ch === 'wuju') { cls = 'B'; } else if (d.id === 'song' && n.ch === 'yinbu') { cls = 'C'; } else if (d.id === 'nanchao' && (n.ch === 'sheren' || n.ch === 'shizu')) { cls = 'D'; } else if ((x.kemu || []).length === 0 && n.tier === clamp(x.seat, 0, chOf(d.id, n.ch).ceiling?.tier ?? 5)) { // A 的谓词:位阶【正好】等于这条路的位子,且与 profile 无关(kemu 空)。 cls = 'A'; } } if (!cls) { unexplained.push(`${d.id}/${key}:${JSON.stringify(o)} → ${JSON.stringify(n)}`); } else { const bucket = `${d.id}·${cls}`; stats[bucket] = (stats[bucket] || 0) + 1; if (!samples[bucket]) samples[bucket] = `${key}:${o.ch}/t${o.tier}「${o.title}」 → ${n.ch}/t${n.tier}「${n.title}」`; } } } // ── 分类统计 + 抽样,打出来给人看(老板要的「抽样说明 + 分类统计」)── const lines = ['', ' ── 变化审计 ──', ` 未变 ${same} 格;变了 ${changed} 格(共 ${keys.length * DYNASTIES.length} 格)`]; for (const b of Object.keys(stats).sort()) lines.push(` ${b.padEnd(14)} ${String(stats[b]).padStart(5)} 格 例:${samples[b]}`); lines.push(` 说不清为什么变的:${unexplained.length} 格`); console.log(lines.join('\n')); // ★★ 验收:这一类必须为空。 assert.deepEqual( unexplained.slice(0, 12), [], `★★ 有 ${unexplained.length} 格的变化【我说不清为什么该变】—— 这一刀不算过。\n` + ' 每一处变化都必须是某条原文要求的,或者是某条明标为「本表的编号」的机理造成的。\n' + ' 说不清 = 引擎干了一件我没有模型的事,那它就是在瞎编。' ); // 这一刀【必须】让输出变 —— 一格都没变,说明权重根本没删掉。 assert.ok(changed > 500, `只变了 ${changed} 格 —— 加权求和大概没真删掉`); }); // ══════════════════════════════════════════════════════════════════════════ // ⑤ 机理本身的断言 —— 光有审计还不够,机理得自己站得住 // ══════════════════════════════════════════════════════════════════════════ test('★★ 没有量表的路,位阶不许随 profile 变 —— 那正是「原文不给量」的意思', () => { // 一条 kemu 空的通道:只要它开着,谁走上去都在同一个位子。 // ★ 这不是引擎偷懒 —— 是四科「是分类,不给量」(黄琼「乃奏增孝悌及能從政者【為四科】」: // 能被「增为四科」的只能是类)在代码里的样子。 const seen = {}; for (const [subs, wang] of [ [['rigorous'], 0], [['rigorous', 'datadriven'], 0], [['rigorous', 'datadriven', 'concise', 'shortfirst'], 0], [['rigorous', 'datadriven', 'concise', 'shortfirst'], 100], ]) { const r = computeRank(profileFromSubs(subs, { wang }), 'han'); assert.equal(r.channel.id, 'bizhao'); seen[`${subs.length}/${wang}`] = r.rank.tier; } const tiers = new Set(Object.values(seen)); assert.equal( tiers.size, 1, `汉的辟召给出了不同的位阶(${JSON.stringify(seen)}) —— 而四科是【分类】,不给量。` + '位阶随本事高低浮动,就是加权求和的形状又回来了' ); assert.equal([...tiers][0], xuanOf(chOf('han', 'bizhao')).seat, '位阶必须正好等于这条路的位子'); }); test('★★ 字典序:高才不许补低德 —— 第一条判得了,就不许看第二条', () => { // 「德均以才,才均以勞」:「均」= 相等,是查询下一级的【触发条件】,不是权重。 // 在本表里,这句话的可执行形式是:一条通道的排序键上,【第一条轴分出高下,就不再看后面】。 // ★ 拿宋武举验,因为它是全表唯一一条排序键有原文的:key = [武艺, 策]。 // 两个人:甲武艺优、策不通;乙武艺不到优、策满格。原文说甲高(去留在武艺)。 const jia = computeRank(profileFromSubs(['blunt', 'motivate'], { wang: 0 }), 'song'); const yi = computeRank(profileFromSubs(['blunt', 'rigorous', 'datadriven', 'concise', 'shortfirst'], { wang: 0 }), 'song'); assert.equal(jia.channel.id, 'wuju'); assert.equal(yi.channel.id, 'wuju'); assert.ok( jia.rank.tier > yi.rank.tier, '★ 策满格的人补不上武艺那一条 —— 「武藝末等者三班差使」,策再好也补不回来。' + '若这条挂了,说明「策」又在给「武艺」补分了 = 加权求和' ); }); test('★ 位阶不许越过这条通道自己的天花板(位子 + 通过计数 都算完之后)', () => { const ALL = SUB_REGISTRY.map((s) => s.id); for (const wang of [0, 55, 100]) { for (const d of DYNASTIES) { const r = computeRank(profileFromSubs(ALL, { wang }), d.id); if (!r.channel) continue; const c = chOf(d.id, r.channel.id); // ⚠ 2026-07-17 接 25 朝:天花板未记(null)时兜底取【本梯最高格】,与 computeRank 的 tierOf 同口径 // (不再写死 5)—— 否则陈的【亲王起家侍中】(tier7、天花板未记) 会被误判成越过了一个假的 5。 const trk = getDynasty(d.id).tracks.find((t) => t.id === c.track) || getDynasty(d.id).tracks[0]; const maxTierC = Math.max(...trk.ladder.map((l) => l.tier)); assert.ok(r.rank.tier <= (c.ceiling?.tier ?? maxTierC), `${d.name}/${c.name} 越过了自己的天花板`); } } }); test('★ 合规:数不出高下的时候,必须当着用户的面说出来(第八条(五):不得过度迎合)', () => { // 本表在这条路上数不出你的高下,就得说;拿一个算出来的位阶糊弄过去,才是迎合。 const r = computeRank(profileFromSubs(['rigorous', 'datadriven'], { wang: 0 }), 'han'); assert.ok( r.reasons.some((s) => s.includes('数不出')), '没有量表的路,必须告诉用户「本表数不出你在这条路上的高下」' ); // 而有科目可数的路,必须把【通了几条、通的是哪几条】摆出来让他自己数。 const w = computeRank(profileFromSubs(['blunt', 'motivate'], { wang: 0 }), 'song'); assert.ok(w.reasons.some((s) => /通了 1 条,共 2 条/.test(s)), '武举必须报「通了几条」'); assert.ok(w.reasons.some((s) => /武藝優/.test(s)), '并且要用原文的词报出通的是哪一条'); }); // ══════════════════════════════════════════════════════════════════════════ // ⑥ 2026-07-17 第六刀(kemu)—— 【空的 kemu 必须自陈它是哪一种空】 // // ★★ 这一节守的是这一刀的唯一交付物,而它守的方式【必须是扫数据,不是读注释】: // 老板逐字:「填完之后【哪几条通道仍然是常数】—— 逐条列出,并说明是【无科目可依】 // 还是【我没找到】。★【这两者不是一回事,混了就是撒谎。】」 // ⇒ 而「混了就是撒谎」这句话,要挡住它【只有一个办法】:让「不说清是哪一种」这件事 // 【跑不起来】。⇒ 本节就是那道闸。 // // ⚠ 为什么它必须是断言,不是注释 —— 本文件已经答过这一问,逐字(dynasties.js): // 「⚠ 而它必须是【函数】不是注释:注释挡不住任何人(这句话本文件写了三遍, // 而 penaltyIfNoWang 在注释的眼皮底下躺了很久)。」 // ══════════════════════════════════════════════════════════════════════════ import { KEMU_ABSENCE, kemuAbsenceOf } from '../src/dynasties.js'; import { LOST_AT, KAI_VOICE, KAI_VOICE_OPTIONS, comparableTier } from '../src/dynasties.js'; import { zeluAsk, ZELU_DEFAULT_NOTE, NEAREST_DEFAULT_NOTE } from '../src/dynasties.js'; // ══════════════════════════════════════════════════════════════════════════ // ⑦ 2026-07-17 第七刀(kai)—— 【把「开着但没走」的路还给用户】 // // ★★ 这一节守的是【假二分不许复活】:reasons 此前只有两类句子(①你走的 + ②对你关着的), // 而一条【开着但输了】的路两类都不进 ⇒ 用户读到一个封闭的二分,并合理推断那就是全部的路。 // ⇒ 实测(穷举 524,288 × 7 朝,非采样):【2,510,336 格(68.4%)里那个推断是错的】。 // // ⚠ 为什么它必须是断言不是注释 —— 本文件已答过,逐字:「注释挡不住任何人,判别式可以。」 // ══════════════════════════════════════════════════════════════════════════ // ★ 复算择路的 comparator,用来验 kai.lostAt 报的那一级【是真的那一级】。 // ⚠ 这【不是】把 computeRank 抄一遍:它只回答「这两条路,赢家赢在哪一级」。 const TIE_TOL = 5; function lexSign(a, b, val) { const ka = xuanOf(a).key || [], kb = xuanOf(b).key || []; for (let i = 0; i < Math.min(ka.length, kb.length); i++) { const va = val(ka[i]), vb = val(kb[i]); if (Math.abs(va - vb) > TIE_TOL) return vb - va; } return 0; } const valOf = (p) => (k) => (k === 'wang' ? clamp(Number(p.wang || 0), 0, 100) : k === 'lao' ? Math.max(0, Math.floor(Number(p.lao || 0))) : clamp(Number((p.axes || {})[k] || 0), 0, 100)); test('★★★★ 假二分不许复活:一条【开着但没走】的路,必须在 kai 里,也必须在 reasons 里', () => { // ★ 老板给的那个实例,逐字复现(汉,wu=63 jiao=63 wang=55): // 军功 gate{wu:55},他 wu=63 ⇒ 对他【开着】。而本刀之前 reasons 提它 0 次、blocked 里 0 次。 const p = profileFromSubs(['blunt', 'warm'], { wang: 55 }); const r = computeRank(p, 'han'); assert.equal(r.channel.id, 'xiaolian', '前提:引擎判他走察举孝廉'); assert.ok(r.kai, '★ kai 是一等公民的数据 —— 没有它,「开着的路」就只是一句拼出来的话'); const jg = r.kai.find((k) => k.channelId === 'jungong_han'); assert.ok(jg, '★★ 军功对他开着,而 kai 里没有它 —— 假二分回来了'); assert.equal(jg.walked, false); assert.equal(jg.lostAt, 'idx', '★ 它输的是【本表的编号】:tier 3=3、ceiling 5=5、字典序判平(63 vs 63)'); assert.equal(jg.reasonKind, 'none', '★★ 判它的【没有人】—— 只有数组顺序'); // ★★★ 而【用户必须读得到它】—— 数据对了而话没说,就是本表反复栽的那个坑 // (kaoyan.js 逐字:「声明在代码里,产物在用户眼前,而中间那句话【没有跟过去】」)。 assert.ok( r.reasons.some((s) => s.includes('军功')), '★★★ kai 里有它,而 reasons 里一个字都没有 —— 那就是「数据修了,话没跟过去」' ); assert.equal(r.blocked.filter((b) => b.channel.includes('军功')).length, 0, '前提:它不在 blocked 里(它开着)'); }); test('★★★ 每一条 kai 报的「输在哪一级」,必须与择路的 comparator 同源 —— 逐格复算', () => { // ⚠ 若 lostAt 是另一把尺子量出来的,它比不报更坏: // 带着「输在 tier」四个字的错误,比一句没有出处的话危害大十倍(本项目铁律的同一条道理)。 const CASES = [[[], 0], [['blunt', 'warm'], 55], [['rigorous', 'datadriven', 'concise', 'shortfirst', 'patient', 'calm'], 0], [['socratic', 'humor', 'poetic', 'story', 'vivid'], 60], [['blunt', 'motivate', 'rigorous'], 80], [SUB_REGISTRY.map((s) => s.id), 100], [['warm', 'playful'], 30], [['poetic', 'story', 'vivid'], 0]]; let checked = 0; for (const [subs, wang] of CASES) { for (const d of DYNASTIES) { const p = profileFromSubs(subs, { wang }); const r = computeRank(p, d.id); if (!r.channel) { assert.deepEqual(r.kai, [], '一条路都不开 ⇒ 没有一条开着的路'); continue; } const val = valOf(p); const won = r.kai.find((k) => k.walked); assert.ok(won, `${d.id}: kai 里必须有【你走的那条】—— 它也开着`); for (const k of r.kai) { if (k.walked) { assert.equal(k.lostAt, null); continue; } const a = chOf(d.id, k.channelId), b = chOf(d.id, won.channelId); let expect; if (won.tier > k.tier) expect = 'tier'; else if ((b.ceiling?.tier ?? 5) > (a.ceiling?.tier ?? 5)) expect = 'ceiling'; else { const s = lexSign(a, b, val); expect = s > 0 ? 'lex' : s < 0 ? 'unsorted' : 'idx'; } assert.equal(k.lostAt, expect, `${d.id}/${k.channelId}:报「输在${k.lostAt}」,复算是「${expect}」`); checked++; } } } assert.ok(checked > 20, `复算的格子太少(${checked}),这条断言没在证东西`); }); test('★★★★★ 输在 lex 的,必须报出【本表拿哪两条轴比的】—— 而它【比不了】同一条轴', () => { // ══════════════════════════════════════════════════════════════════════════ // ★★★★★ 2026-07-17 验收:【这条断言原来是【不可能失败】的,而它自称在守一个新发现】 // // 【原文逐字】`assert.equal(k.lexAxes.sameAxis, false, '★★★ 竟然比了同一条轴 —— // 穷举 784,256 次同轴 0 次,这是新事')` // ⇒ ★【它一次都不可能红】:lexWhere 只在 |val(ka[i]) − val(kb[i])| > TIE_TOL 时返回位置 i, // 而 val 是【轴 id 的函数】⇒ 同轴 ⇒ 同值 ⇒ 差 0 ⇒ 不过容差 ⇒ 【必然滑过去】。 // ⇒ sameAxis 恒 false ⇒ 【这条断言买不到任何东西,而它挂着「这是新事」的牌子】。 // ★★ 而它守的那个「新事」(穷举 784,256 次同轴 0 次)【也是恒等式】:对任何一张表都成立, // 包括一张每条路 key[0] 全相同的完美表。⇒ 【断言是空的,它守的发现也是空的。】 // // ⇒ 【改成两条【真的会红】的】: // ① 那两条轴的名字与数【必须出门】—— 这是【单格可证伪】的,用户自己就能核。 // ② ★ 把那个恒等式【当定理来钉】,而不是当读数来数:直接构造一对【同轴】的 key, // 证明 lexCompare 在那一位【弃权】(返回 0),而不是「碰巧没比到」。 // ══════════════════════════════════════════════════════════════════════════ // 汉,wu=73 jiao=63:军功比【果决 73】、孝廉比【交游 63】⇒ 差过容差 ⇒ 孝廉输在字典序。 // ★ 而这一局比的是【他的果决】和【他的交游】—— 那是两条不同的轴。 const p = profileFromSubs(['blunt', 'motivate', 'warm'], { wang: 55 }); const r = computeRank(p, 'han'); assert.equal(r.channel.id, 'jungong_han', '前提:字典序把他判去了军功'); const lex = r.kai.filter((k) => k.lostAt === 'lex'); assert.ok(lex.length, '前提:这个 profile 必须有一条输在字典序的路'); for (const k of lex) { assert.ok(k.lexAxes, '★ 输在 lex 却不报是拿哪两条轴比的 —— 那四个字什么都没说'); // ★ 这一条【会红】:它要的是两条轴的名字和数真的出了门,而不是一个恒 false 的布尔。 assert.ok(k.lexAxes.mine?.axis && k.lexAxes.theirs?.axis, '★ 必须报出【实际比的那两条轴】'); assert.equal(typeof k.lexAxes.mine.value, 'number', '★ 必须报出那两个数 —— 用户要自己核'); assert.equal(typeof k.lexAxes.theirs.value, 'number'); assert.notEqual( k.lexAxes.mine.axis, k.lexAxes.theirs.axis, '★★ 这两条轴不同 —— ⚠ 而这【不是一个发现,是 lexCompare 的定义】:同轴它必然弃权。见下一条。' ); assert.match(k.lexAxes.note, /原文从不问这种问题/, '★★ 这句限定语必须跟着那两条轴一起出门'); // ★★★ 那句话必须【自己说出它是定理,不是运气】—— 否则它又变回「穷举出来的一个数」。 assert.match(k.lexAxes.note, /它比不了同一条轴|必然滑过去/, '★★★ note 必须说清【同轴它比不了】'); } assert.equal( 'sameAxis' in lex[0].lexAxes, false, '★★★★ sameAxis 恒 false(构造上不可能为 true)⇒ 已删。它与 penaltyIfNoWang / kind:"unique" 同形' ); assert.ok( r.reasons.some((s) => /原文从不问|本表自己问的/.test(s)), '★★ 用户必须读得到「这个问题是本表自己问的」—— 藏起来,它就是一句像史学判断的编造' ); }); test('★★★★★【定理,不是读数】:lexCompare 在【同一条轴】上必然弃权 —— 所以「同轴 0 次」什么都没测到', () => { // ══════════════════════════════════════════════════════════════════════════ // ★ 上一刀把「穷举 784,256 次 lex 裁决,同轴 0 次」挂成了头号交付物(★★★★★ // 「把那句自谦变成了一个数」)。★★【而那个数是恒等式:对任何一张表都是 0。】 // ⇒ 这条测试【就是那个证明】—— 它不数格子,它证构造。 // // ★★★ 而【本表真的有同轴的 key[0] 对】—— 它们不是被本表避开的,是被 lexCompare 滑过去的: // · tang: jinshi / xingjuan —— key[0] 都是 'wen' // · liao: wenquan_liao / nanmian_hanguan —— key[0] 都是 'fa' // ⇒ ★【即「同轴 0 次」连「本表小心地避开了同轴」都证不出来 —— 本表压根没避开, // 是那台仪器测不到。】 // ══════════════════════════════════════════════════════════════════════════ // ① 本表【真的有】同轴的 key[0] 对 —— 这一条会红(哪天数据改了,这句话就得重写) const samePairs = []; for (const d of DYNASTIES) { const cs = d.channels; for (let i = 0; i < cs.length; i++) for (let j = i + 1; j < cs.length; j++) { const a = cs[i].xuan?.key || [], b = cs[j].xuan?.key || []; if (a[0] && a[0] === b[0]) samePairs.push(`${d.id}:${cs[i].id}/${cs[j].id}=${a[0]}`); } } // ⚠ 2026-07-17 加西汉:西汉四条通道的排序键【全是 ['jingxue']】(keySource 全 null), // 理由是【六轴瞎 ⇒ 盲路之间本表没有任何量来分高下 ⇒ 让它们在 lexCompare 上必然判平、 // 落到 _idx(本表的编号)】——这正是 xuan/xihan.js XUAN_NOT_DONE ⑧ 说的「_idx 是唯一判据」。 // ⇒ 于是西汉多出 C(4,2)=6 对同轴 key[0]。★ 这【强化】了本条的论点(同轴的对更多了, // 而 lexCompare 一对都裁不了——见下面 ② 的定理)。 // ⚠ 2026-07-17 拆墙(25 朝):不再写死【全表恰好这几对】—— 接进来的朝有很多同轴 key[0] 对 // (如陈的八条路 key 全是 ['wang'])。★ 不变量【仍然保留、且不弱】:这原有的 8 对必须【全都在】 // (证明「本表【有】同轴的 key[0] 对」这个论点),而它们【一对都裁不了】由下面的定理 ② 守着。 const knownSamePairs = [ 'tang:jinshi/xingjuan=wen', 'liao:wenquan_liao/nanmian_hanguan=fa', 'xihan:xiaolian/xianliang=jingxue', 'xihan:xiaolian/boshi_dizi=jingxue', 'xihan:xiaolian/jungong=jingxue', 'xihan:xianliang/boshi_dizi=jingxue', 'xihan:xianliang/jungong=jingxue', 'xihan:boshi_dizi/jungong=jingxue', ]; for (const p of knownSamePairs) { assert.ok(samePairs.includes(p), `★★ 本表必须【有】同轴的 key[0] 对(${p})—— ⇒「同轴 0 次」不是「本表避开了同轴」,是 lexCompare 测不到`); } // ② ★★★ 定理本身:同轴那一位,lexWhere【永远返回不了】⇒ 报出来的两条轴必然不同。 // 穷举全表所有 lex 裁决,检查【报出来的那一位】—— 而这不是在「数 0 次」, // 是在钉【一个由 val 是函数逼出来的恒等式】:同轴 ⇒ 同值 ⇒ 差 0 ⇒ 0 > TIE_TOL 为假。 let decided = 0; for (const subs of [['blunt', 'motivate', 'warm'], ['blunt', 'warm'], ['refined', 'warm', 'blunt']]) { for (const wang of [0, 30, 55, 75, 100]) for (const d of DYNASTIES) { const r = computeRank(profileFromSubs(subs, { wang }), d.id); for (const k of (r.kai || [])) { if (k.lostAt !== 'lex' || !k.lexAxes) continue; decided++; assert.notEqual(k.lexAxes.mine.axis, k.lexAxes.theirs.axis, '★ 定理被破:lex 竟然在同轴上裁决了'); } } } assert.ok(decided > 0, `前提:必须真的有 lex 裁决发生(${decided}),否则这条测试没在证东西`); }); test('★★★★ zelu:择路的边界 = 老板逐字的判据【同 tier && 同 ceiling】—— 引擎有理由的两格不许被推翻', () => { const p = profileFromSubs(['blunt', 'warm'], { wang: 55 }); const base = computeRank(p, 'han'); const ids = (z) => z.tie.map((t) => t.channelId); assert.deepEqual(ids(base.zelu), ['xiaolian', 'jungong_han'], '★ 打平的那几条 —— ⚠【含引擎默认那一条】'); assert.equal(base.zelu.chosen, null, '★ 没选【不是缺值,是这一问还没问出口】(同 chushen.missing)'); // ★ 选了 ⇒ 你选的那条赢。「你什么都没变」——他只是自己拿了那一格。 const picked = computeRank({ ...p, zelu: 'jungong_han' }, 'han'); assert.equal(picked.channel.id, 'jungong_han', '★★ 择路本身就是第一个关口 —— 那是你选'); assert.equal(picked.zelu.chosen, 'jungong_han'); assert.equal(picked.rank.tier, base.rank.tier, '★ 位阶不变:这一格本来就是打平的,选它不是加分'); // ══ ★★★★ 第八刀:【默认必须自陈它是默认】—— 老板逐字:「它必须标出来是默认,不是判决」 ══ assert.equal(base.zelu.default, 'xiaolian', '★ 引擎的默认'); assert.equal(base.zelu.isDefault, true, '★★ 没人选 ⇒ 现在走的这条【是默认,不是判决】'); assert.equal(picked.zelu.isDefault, false, '★★ 你选了 ⇒ 它不再是默认 —— 这一格是你拿的'); assert.equal(base.zelu.default, picked.zelu.default, '★ 默认是【引擎的】,不随你选什么而变'); assert.match(base.zelu.note, /默认/, '★★ 「这是默认不是判决」必须是【数据】—— 注释挡不住任何人'); assert.equal(computeRank(p, 'ming').zelu.note, '', '★ 没得选的时候【不许】印这句话 —— 那就成了免责声明'); // ★★★★ 【身份稳定:选项表不许因为你选了而少一条】—— 上一刀的 bug,第八刀修。 // 旧写法 `filter(t => t !== engineBest)` 把默认那条排除在集合外 // ⇒ 你一选军功,察举就从选项里【消失】,再也选不回去。 assert.deepEqual(ids(picked.zelu), ids(base.zelu), '★★★ 一个「你走哪条」的选项表,不许取决于你已经选了哪条'); assert.ok(ids(picked.zelu).includes('xiaolian'), '★★ 选了军功之后,必须还能选回察举'); // ★★★★ 【空操作的第 N 次,而这次是我自己在「让它出声」那一刀里造的】——上一刀写的是 // `${best.byZelu ? '' : '——而这一格,本该你自己选。'}`,而 best 是 ranked 的元素, // 【它从来没有 byZelu 这个字段】(byZelu 挂在 kai 上)⇒ 那个三元【永远走 else】 // ⇒ 即使用户已经选了,引擎照样对他说「这一格本该你自己选」。 assert.equal( picked.reasons.filter((s) => s.includes('本该你自己选')).length, 0, '★★★ 他已经选了 —— 再对他说「这一格本该你自己选」,就是那个空操作又活了一次' ); assert.ok( base.reasons.some((s) => s.includes('本该你自己选')), '★ 而没选的时候这句话【必须在】—— 删了它,这一刀就白做了' ); // ★★ 而【引擎默认拿的那条】必须在用户选了之后自陈:不是「本表说不清」,是【你选了别的】。 assert.ok( picked.reasons.some((s) => s.includes('引擎默认给你拿的本来是它')), '★★★ lostAt 若还量着 best 而非 engineBest,这里会报 unsorted=「本表说不清自己为什么没让你走它」——那是假的' ); const eb = picked.kai.find((k) => k.channelId === 'xiaolian'); assert.equal(eb.byEngine, true, '★ 引擎的默认仍是察举 —— 你选了什么,不改变引擎判了什么'); assert.equal(eb.walked, false, '★★ 而 byEngine 与 walked 分叉了 —— 那个分叉就是这个产品要说的话'); assert.equal(eb.lostAt, null, '★★★ 引擎默认那条【没有输】—— ✗ 不许因为你选了别的就说它「输在 unsorted」'); // ⚠ 失效方向朝【关】,与 sameScale / comparableTier / closedBy 同一条规矩。 assert.equal(computeRank({ ...p, zelu: 'bizhao' }, 'han').channel.id, 'xiaolian', '★ 关着的路:不认'); assert.equal(computeRank({ ...p, zelu: '__不存在__' }, 'han').channel.id, 'xiaolian', '★ 名字问错:不认'); // ★★★ 而【引擎有理由的那两格,zelu 不许推翻】—— 这一条是 zelu 的边界,第八刀【没有放宽】: const q = profileFromSubs(['socratic', 'humor', 'poetic', 'story', 'vivid'], { wang: 60 }); const wj = computeRank(q, 'weijin'); const qt = wj.kai.find((k) => k.channelId === 'qingtan'); assert.ok(qt && !qt.walked && qt.lostAt === 'ceiling', '前提:清谈对他开着,且输在【天花板】'); assert.equal( computeRank({ ...q, zelu: 'qingtan' }, 'weijin').channel.id, wj.channel.id, '★★★ 输在天花板的路不许被 zelu 拿走 —— 那等于把「能过科举就别去当吏」那条纪律交给用户删' ); // ★ 而它必须【明确地】不在 tie 里,不是靠上面那条碰巧挡住。 assert.ok(!ids(wj.zelu).includes('qingtan'), '★ 引擎有理由赢的路,不许出现在「你选」的选项里'); }); test('★★★★★ 老板的判据【逐格验证】:tie 里每一条的 tier 与 ceiling 必然相同 —— 这就是它安全的原因', () => { // 老板逐字:「★ 我的判断:问的时候,两条路的 tier 和 ceiling【已经一样了】—— // 所以挑不出优劣,只能挑「你想当什么人」。⇒ 这恰恰是它安全的原因, // 也是它是「选择」而不是「优化」的原因。⚠ 验证我这个判断……跑出来,别信我。」 // ⇒ 【跑了。穷举 3,670,016 格(scripts/census-zelu.mjs),零例外:】 // 打平(idx) 242,176 条次【全部】落在 (tier =, ceiling =); // 「tier 一样但 ceiling 不一样」1,300,480 条次【全部】输在 ceiling —— 那种引擎有理由。 // ★ 这一条把那个穷举钉成断言:★★【tie 里挑哪条,位阶与天花板一个数都不动】 // ⇒ 【它不可能变成 stat 优化,也不可能变成抽卡】—— 那不是本表克制,那是这个集合的定义。 for (const d of DYNASTIES) { for (const subs of [[], ['blunt', 'warm'], ['blunt', 'motivate', 'warm'], ['rigorous', 'patient', 'humor'], ['socratic', 'humor', 'poetic', 'story', 'vivid'], ['poetic', 'story', 'vivid', 'blunt']]) { for (const wang of [0, 30, 55, 75, 100]) { const r = computeRank({ ...profileFromSubs(subs, { wang }), chushen: d.id === 'liao' ? 'qidan' : null }, d.id); const tie = r.zelu.tie || []; if (tie.length <= 1) continue; const [a] = tie; for (const t of tie) { assert.equal(t.tier, a.tier, `${d.id}: tie 里出现了【位阶不同】的路 —— 那不是打平,那是引擎有理由(位阶即杠杆)`); assert.equal(t.ceiling, a.ceiling, `${d.id}: tie 里出现了【天花板不同】的路 —— ✗ 那一格不许交给用户`); } // ★★ 默认【必然】在 tie 里,且【有且只有一条】是默认。 assert.equal(tie.filter((t) => t.isDefault).length, 1, `${d.id}: 默认必须有且只有一条`); assert.ok(tie.some((t) => t.channelId === r.zelu.default), `${d.id}: 默认不在 tie 里 —— 选项表少了引擎自己拿的那条`); // ★★★ tie 里【不许】出现 'tier'/'ceiling' —— 那两级是引擎有理由的,tieOf 已按相等筛掉。 for (const t of tie) { if (t.isDefault) continue; assert.ok(['idx', 'lex'].includes(t.lostAt), `${d.id}: tie 里出现 lostAt='${t.lostAt}' —— 引擎有理由的路混进了「你选」`); assert.ok(['none', 'invented'].includes(t.reasonKind), `${d.id}: 默认凭一个【理由】赢的路,不该在 tie 里`); } // ★ tie 与 kai.yours 是同一个集合的两个投影 —— ✗ 不许分叉。 assert.deepEqual( r.kai.filter((k) => k.yours).map((k) => k.channelId).sort(), tie.map((t) => t.channelId).sort(), `${d.id}: kai 说「这一格本该你选」,而选项表里没有它 —— 两处分叉,用户读到一个选不了的选项` ); } } } }); test('★★★★ 而 lex 那 784,256 条次【是打平】—— 上一刀把它排除了,那是把老板的判据读窄了 76.4%', () => { // ★★ 老板逐字的判据是「两条路对你都开着、【位阶一样、天花板一样】⇒ 引擎没有理由偏爱任何一条」。 // ⇒ 上一刀我把它读成 `lostAt === 'idx'`,理由是「lex 的理由是假的,而【理由是假的】 // ≠【该由用户推翻】—— 那一格要的是修引擎」。★【那个理由站不住,两条】: // ① 实测:lex 那 784,256 条次【tier 一样、ceiling 一样】—— 逐条满足老板的判据。 // ② ★★ 而「修引擎」与「归你选」在这一格上【是同一个动作】:修 lexCompare 的唯一诚实 // 修法是让它拒绝跨轴比(返回 0)⇒ 那 784,256 条次当场落到 _idx ⇒ 本来就归 zelu。 // ⇒ 【于是上一刀的 zelu 只接管了老板判据的 23.6%;剩下 76.4% 仍由引擎拿一个 // 「你的果决比你的交游高吗」(原文从不问)替用户做了决定。】 // ⚠ 2026-07-17 验收:这里【原来引「同轴 0/784,256」当论据】,删了 —— 那是恒等式不是读数 // (证明在同名测试「lexCompare 在同一条轴上必然弃权」里)。★ 而②【反而更硬】: // 既然 lexCompare【只有】跨轴才裁决得了,「拒绝跨轴」就是【整级消失】⇒ 784,256 全落 _idx。 const p = profileFromSubs(['blunt', 'motivate', 'warm'], { wang: 55 }); const r = computeRank(p, 'han'); const lex = r.kai.filter((k) => k.lostAt === 'lex'); assert.ok(lex.length, '前提:这个 profile 必须有一条输在字典序的路'); for (const k of lex) { assert.equal(k.tier, r.rank.tier, '★★ 输在 lex 的路,位阶与你走的这条【一样】—— 穷举零例外'); assert.equal(k.yours, true, '★★★★ 位阶一样、天花板一样 ⇒ 按老板的判据它【是打平】⇒ 这一格本该你选'); assert.ok(r.zelu.tie.some((t) => t.channelId === k.channelId), '★★ 而它必须真的在选项里 —— 不是只在文案里承认一句'); } // ★ 而它【真的选得动】:选了它,位阶不变(打平的定义),走的就是它。 const one = lex[0]; const picked = computeRank({ ...p, zelu: one.channelId }, 'han'); assert.equal(picked.channel.id, one.channelId, '★★★ 输在「本表自己编的问题」上的路,用户拿得回去'); assert.equal(picked.rank.tier, r.rank.tier, '★ 位阶不变 —— 这一格本来就是打平的'); }); test('★★★ zeluAsk:纯查询函数 —— 失效方向朝【关】,且它【没有「算 tier 之前」那个状态可以被调用】', () => { const p = profileFromSubs(['blunt', 'warm'], { wang: 55 }); const a = zeluAsk(p, 'han'); assert.ok(a && a.options.length === 2, '★ 汉这一格有得选'); assert.match(a.ask, /你走哪一条/); assert.equal(a.default, 'xiaolian'); assert.equal(a.isDefault, true); // ⚠ 失效方向朝【关】,与 sameScale / comparableTier / closedBy / kemuAbsenceOf 同一条规矩。 assert.equal(zeluAsk(p, '__不存在的朝__'), null, '★ 朝代问错 ⇒ null'); assert.equal(zeluAsk(profileFromSubs([], { wang: 0 }), 'han'), null, '★★ 一条路都不开 ⇒ null —— 「你走哪条」对他【问不出口】,✗ 不许给他一张关着的门的菜单'); assert.equal(zeluAsk(profileFromSubs(['patient', 'calm', 'rigorous'], { wang: 0 }), 'ming'), null, '★ 本表这一次分得出高下(明:科举 tier3 > 吏员 tier2)⇒ 没有你要拿的那一格'); // ★★ 纯函数:跑两次必须完全相同。 assert.deepEqual(zeluAsk(p, 'han'), a, '★ 同一个 profile 跑两次必须一模一样'); // ★★★ 每一条选项都必须带着【引擎默认凭什么赢它】—— 只渲染名字,这一问就成了没有代价的菜单。 for (const o of a.options) { if (o.isDefault) continue; assert.ok(o.lostAt && o.reasonKind, '★ 选项不带「默认凭什么赢它」⇒ 这一问的全部内容(本表没有理由)就没跟着出门'); } }); test('★★★ 一条路都不开:kai 必须是空数组 —— 而那是真值,不是缺值', () => { const r = computeRank(profileFromSubs([], { wang: 0 }), 'weijin'); assert.equal(r.channel, null); assert.deepEqual(r.kai, [], '★ 没有一条开着的路 ⇒ 空数组。✗ 不许缺这个字段:缺了,前端读到 undefined'); assert.deepEqual(r.zelu.tie, [], '★ 一条路都没有 ⇒ 没得选。✗ 不许兜底给一条'); assert.equal(r.zelu.default, null, '★★ 没有默认 —— 「你走哪条」对一个无路可走的人【问不出口】'); assert.equal(r.zelu.ask, '', '★★★ ✗ 不许给他一张【关着的门】的菜单 —— 那比游戏菜单更坏:选了也进不去'); }); test('★★★★ 第五次那一支:nearestTie —— 李广那一格,「军功」两个字必须出现', () => { // ★★ 老板说「上一刀只修了影响 0.46% 用户的那一支」——【实测:没有修】。 // 汉·一条路都不开的 2,432 个 profile,nearest 落点:察举 1,344/辟召 1,088/【军功 0】。 // ⇒ 第五次那个读数(0/2,432)今天仍然逐字成立。本刀不改判决,只让那个编号【出声】。 // 【机理】李广(六轴全 40):辟召差[吏能 15]、军功差[果决 15] ⇒ 条数打平、缺口打平 // ⇒ 落到 _idx:辟召 1 < 军功 2。★ 而《史記》卷109:李广的实职正是靠军功挣的(renwu.js 双证)。 const r = computeRank(profileFromSubs([], { wang: 0 }), 'han'); assert.equal(r.channel, null, '前提:他一条路都不开'); assert.equal(r.nearest, '辟召(四科·明达法令)', '前提:nearest 仍是辟召 —— 本刀不改判决'); assert.deepEqual(r.nearestTie, ['军功'], '★★★ 与它打平、只输给编号的那条,此前一个字都没出过引擎'); assert.ok( r.reasons.some((s) => s.includes('军功') && s.includes('分不出')), '★★★★ 那张表把他真的走过的那条路用数组顺序删掉了,而且从来没告诉过他 —— 现在得告诉他' ); // ══ ★★★★ 第八刀:老板逐字「那两个分支是同一个 _idx 兜底的两条腿。【一起修】」 ══ // ⇒ 「一起修」修的是【让默认自陈它是默认】—— 上一刀这一支【出了声】(nearestTie), // 而 `nearest: 辟召` 仍然【冒充判决】:它读起来像本表算出来的,而它是数组第 1 号。 assert.equal(r.nearestIsDefault, true, '★★★ nearest 打平时是【默认】,不是【判决】—— 它必须自己说出来'); assert.match(r.nearestNote, /默认/, '★★ 这句话必须是【数据】,不是注释'); // ★★ 而【这一支不给 zelu,这不是漏,是拒绝】:一条路都不开的人没有路可走, // 「你走哪条」对他【问不出口】。✗ 硬给他一个选项 = 把一扇【真的关着的门】画成菜单。 assert.deepEqual(r.zelu.tie, [], '★★★ 无路者不给择路 —— 那一问对他问不出口,✗ 不许把关着的门画成菜单'); assert.equal(zeluAsk(profileFromSubs([], { wang: 0 }), 'han'), null, '★ 纯查询函数同口径:不问'); // ★ 而【没打平的时候,nearest 不许也说「我随便拿的」】—— 那就是把免责声明贴满全表。 // ⚠ 实测:无路者 73,088 格里,nearest 打平的只有 15,040 格,【没打平的 58,048 格】—— // 即绝大多数无路者,本表【真的算得出】哪条离他最近(差的条数少、或缺口小)。 // ✗ 给那 58,048 格也印一句「这是我随便拿的」,就是拿诚实当免责声明。 const tangT0 = computeRank(profileFromSubs([], { wang: 0 }), 'tang'); assert.equal(tangT0.channel, null, '前提:唐对六轴全 40 的人一条路都不开'); assert.deepEqual(tangT0.nearestTie, [], '前提:这一格【没有】打平 —— 本表真的算得出哪条最近'); assert.equal(tangT0.nearestIsDefault, false, '★★ 没打平 ⇒ nearest 是本表【真的算出来的】(差的条数/缺口),不是默认'); assert.equal(tangT0.nearestNote, '', '★★★ ✗ 不许每次都印「这是默认」—— 那就成了免责声明,而免责声明不是诚实'); }); test('★★★ 两种文案都必须在 —— 删掉没选中的那一种,「差别」就只剩一句转述', () => { // 老板逐字:「⚠ 但这是产品定性,你先做出来,【把两种文案都写出来,让老板看得见差别】。」 assert.ok(KAI_VOICE_OPTIONS.admit && KAI_VOICE_OPTIONS.shishu, '★ 两种口气都得留着,改一个词就换一种产品'); assert.equal(KAI_VOICE, 'admit', '★ 老板的判断:承认。改这一个词就换一种产品 —— 那是产品定性,不是重构'); // ★★ 而【乙的代价必须写在数据里】:它读起来最像史书,而它有一个谎。 assert.match( KAI_VOICE_OPTIONS.shishu.cost, /他没有走|谁做的决定/, '★★ 乙不是「更含蓄的甲」,它是把【谁做的决定】这一问删掉 —— 这个代价不许只写在注释里' ); }); test('★★★★ LOST_AT 四档:每一档都必须自陈【判它的是谁】,且只有一档说得起「我有理由」', () => { // ★ 老板的二分是「输在 tier =【引擎有理由】/输在 _idx =【引擎没有理由】」。 // ⇒ 实测把这个二分【推翻了两次】,而两次方向相反: // ① _idx 只占 6.8%(老板预计「绝大多数」); // ② ★★ 而 tier 那一档【不是「有理由」】—— comparableTier() 说这一比不成立, // 而 computeRank 的择路第一级逐字就是 `b._tier - a._tier`,实测跑了 1,248,256 次。 assert.deepEqual(Object.keys(LOST_AT).sort(), ['ceiling', 'idx', 'lex', 'tier']); for (const [id, v] of Object.entries(LOST_AT)) { assert.equal(v.id, id); assert.ok(v.whoDecided, `${id}:没说【判它的是谁】—— 那这一档什么都没交代`); assert.ok(v.census, `${id}:没有读数 —— 一个没有数的分类,是给变化重新命名了一遍`); assert.ok(v.desc.length > 80, `${id}:desc 太短`); } const sound = Object.values(LOST_AT).filter((v) => v.reasonKind === 'sound').map((v) => v.id); assert.deepEqual( sound, ['ceiling'], '★★★ 全表四级,只有【天花板】这一级本表可以直着腰说「是我判的,我有理由」——' + '而那个理由就是这个功能的立意(位阶即杠杆)。多出来的那一档,先回答它的理由出自哪一条原文。' ); assert.equal(LOST_AT.idx.reasonKind, 'none', '★ 编号:没有人'); // ⚠ 2026-07-17 验收:这里【原来引「同轴 0/784,256」】,删了 —— 恒等式不是读数。 // ★ 'invented' 这顶帽子【不靠那个数】:靠的是 lexCompare【只有】跨轴才裁决得了, // 而「你的果决比你的交游高吗」原文从不问。⇒ 那是它唯一问得出的问题。 assert.equal(LOST_AT.lex.reasonKind, 'invented', '★★ 字典序:理由是【本表编的】—— 它只问得出跨轴那一问'); assert.equal(LOST_AT.tier.reasonKind, 'self-contradicted', '★★ 位阶:本表【自己写过判别式挡它】'); // ★★ 而这一条把那个自我矛盾钉成了断言,不是一句注释: assert.equal( comparableTier('han', 'xiaolian', 'jungong_han'), false, '★★★ 本表的判别式说这两条通道的 tier 不可比 —— 而择路第一级正是拿它们相减。' + '这一条【故意红不了】:它证的是那个矛盾【今天仍然存在】,不是它被修了。见 not_done ⑦-b。' ); }); test('★★★ 每一条空的 kemu,都必须自陈它是【哪一种空】—— 不说清就是撒谎', () => { const missing = []; for (const d of DYNASTIES) { for (const c of d.channels) { if ((xuanOf(c).kemu || []).length) continue; const a = kemuAbsenceOf(d.id, c.id); if (!a) { missing.push(`${d.id}/${c.id}`); continue; } assert.ok(KEMU_ABSENCE[a.kind], `${d.id}/${c.id}:kind「${a.kind}」不在 KEMU_ABSENCE 里`); assert.ok(a.why && a.why.length > 40, `${d.id}/${c.id}:why 太短 —— 「留空」必须给出理由,而不是一个标签`); // ⚠ 2026-07-17 接 25 朝:不变量 =【必须有充实的自陈】。原口径要一个单独的 caveat; // 接进来的尺子里有一条(liang/liuwai)把限定语并进了 why(251 字,充实)—— // ⇒ 放宽为【有 caveat,或 why 本身充实到能独立成立(>150)】,仍挡得住「一个标签」那种偷懒。 assert.ok( (a.caveat && a.caveat.length > 0) || (a.why && a.why.length > 150), `${d.id}/${c.id}:既没有 caveat,why 也不够充实 —— 注的价值全在限定语上` ); // ★ 常数性必须是一句【写出来的话】,不是一个藏起来的 bug。 assert.match(a.constantTier, /位阶恒为 seat/, `${d.id}/${c.id}:没有把「位阶恒为 seat」说出来`); } } assert.deepEqual(missing, [], '★★ 这些通道的 kemu 是空的,却【没说是哪一种空】—— 那正是「混了就是撒谎」'); }); test('★★★★ 失效方向朝【关】:问错了必须答「不」,不许兜底', () => { // ⚠ 同 sameScale / comparableTier / closedBy / usableAsInstitution 的一贯口径。 assert.equal(kemuAbsenceOf('不存在的朝', 'bizhao'), null); assert.equal(kemuAbsenceOf('han', '不存在的通道'), null); assert.equal(kemuAbsenceOf(null, null), null); // ★ kemu 非空的通道返回 null —— 那【不是「查不到」,是「这个问题问不出口」】: // 它不空,「它为什么空」这一问对它不成立。(同 t0 的 unit:null 那一格的口径。) assert.equal(kemuAbsenceOf('song', 'wuju'), null, '★ 全表唯一一条 kemu 非空的路,不该有「为什么空」'); }); test('★★★ no-ruler 是最强的一档 —— 它必须【挂着原文】,且全表只有够得着的才准用', () => { // ★ 本档断言的是【那个制度本身没量你】 ⇒ 它要求料 usableAsInstitution()===true。 // ⇒ 所以它【必须挂 source + quote】。没有原文的「制度没量你」,就是「反过来说本表无据」。 const noRuler = []; for (const d of DYNASTIES) { for (const c of d.channels) { const a = kemuAbsenceOf(d.id, c.id); if (a && a.kind === 'no-ruler') { noRuler.push(`${d.id}/${c.id}`); assert.ok(a.source, `${d.id}/${c.id}:断言「制度没量你」却不挂 source —— 那是本表无据`); assert.ok(a.quote, `${d.id}/${c.id}:断言「制度没量你」却不给引文`); } } } // ★★ 汉·辟召够得着(四科出【注引书】+黄琼「事竟施行」,两条 usable 的料)。 // ⚠ 2026-07-17 接 25 朝:不再写死【全表只有这一条】—— 接进来的尺子里有 no-ruler 的路 // (如北魏 门第/姓族、陈 诸王起家…),选刀已双证抠好。★ 但那个【最强的一档】的门槛没放宽: // 上面 for 循环逐条钉着【每一条 no-ruler 都必须挂 source + quote】(没原文的「制度没量你」= 本表无据)。 // ⇒ 不变量 = 汉·辟召仍在其中 + 每一条 no-ruler 都有原文(后者由上面的 for 守着)。 assert.ok(noRuler.includes('han/bizhao'), '★★ 汉·辟召必须仍是一条 no-ruler(四科注引书 + 黄琼事竟施行)'); }); test('★★★★ axis-gap ≠ no-ruler —— 不许拿「我们的表少一维」冒充「那朝没量你」', () => { // ★ 这一档是这一刀撞出来的第三种空(老板给的二分漏了它)。 // 它【必须挂原文】:本档的全部意思是「原文有科目、有计数,而六轴接不住」—— // ⇒ 没有原文,这句话本身就不成立。 const gaps = []; for (const d of DYNASTIES) { for (const c of d.channels) { const a = kemuAbsenceOf(d.id, c.id); if (a && a.kind === 'axis-gap') { gaps.push(`${d.id}/${c.id}`); assert.ok(a.source && a.quote, `${d.id}/${c.id}:axis-gap 必须挂原文 —— 「原文有科目」是它的前提`); assert.equal(a.whoIsBlind, '本表的六轴', `${d.id}/${c.id}:axis-gap 的盲必须落在【本表】身上,不是制度`); } } } // ⚠ 2026-07-17 加西汉:西汉三条盲路(孝廉/贤良/军功)都是 axis-gap ——【有尺子而本表瞎】: // 孝廉的孝/廉(德行)、贤良的学派、军功的骑射/人头数,都是真的尺子(正文双证), // 而本表七轴一条都接不住(经学那条轴读的是「你读通了什么」,不量德行/学派/武艺)。 // ★ 每条都挂了原文(source+quote),whoIsBlind 也落在【本表的六轴】上(上面 for 循环已逐条校验)。 // ⚠ 2026-07-17 接 25 朝:不再写死【全表恰好这五条】—— 接进来的尺子里有很多 axis-gap 的路 // (五代/金/南宋的科举都数「策/经义/篇数」,六轴接不住)。★ 门槛没放宽:上面 for 逐条钉着 // 【每一条 axis-gap 都必须挂 source + quote,且 whoIsBlind 落在本表六轴】。 // ⇒ 不变量 = 原有那五条仍在其中(子集)+ 每一条都有原文(后者由上面的 for 守着)。 const knownGaps = ['ming/keju', 'tang/jinshi', 'xihan/jungong', 'xihan/xianliang', 'xihan/xiaolian']; for (const g of knownGaps) { assert.ok(gaps.includes(g), `★ ${g} 必须仍是一条 axis-gap(六轴接不住那些科目)`); } // ★★ 而这一档的 desc 必须自己说死:它不是制度的盲。 assert.match(KEMU_ABSENCE['axis-gap'].desc, /不许把它写成 no-ruler|绝不许把它写成 no-ruler/); assert.equal(KEMU_ABSENCE['no-ruler'].whoIsBlind, '制度'); assert.equal(KEMU_ABSENCE.unrecorded.whoIsBlind, '我们'); assert.equal(KEMU_ABSENCE['axis-gap'].whoIsBlind, '本表的六轴'); }); test('★★★ 三种空,用户读到的必须是【三句不同的话】—— 一句话盖住三种,就是那个「混了」', () => { // ⚠ 这一条守的是 computeRank 的产物,不是数据:19 条路原来共用一句话, // 而那一句话【真】,只是它把三个不同的真相盖成了一个。 const say = (subs, dyn, shen) => { const p = { ...profileFromSubs(subs, { wang: 0 }), chushen: shen || null }; const r = computeRank(p, dyn); return r.reasons.find((s) => s.includes('数不出')) || ''; }; const noRuler = say(['rigorous', 'datadriven'], 'han'); // 汉·辟召 const axisGap = say(['patient', 'calm', 'rigorous'], 'ming'); // 明·科举 const unrec = say(['blunt', 'motivate'], 'liao', 'qidan'); // 辽·武铨 // ★ 三句都必须留着「数不出」—— 合规断言(第八条(五))钉着它,见上面那条测试。 for (const s of [noRuler, axisGap, unrec]) assert.match(s, /数不出/); // ★★ 而三句必须【互不相同】。 assert.equal(new Set([noRuler, axisGap, unrec]).size, 3, '★★ 三种空说了同一句话 —— 那就是「混了就是撒谎」'); // ★★★ 各自必须把【盲在谁身上】说对: assert.match(noRuler, /那个制度本来就没打算给你一个高下/, '★ 制度的盲'); assert.match(axisGap, /是本表不够,不是那个朝代没量你/, '★★ 本表的盲 —— 这一句最难说,也最该说'); assert.match(unrec, /本表手上没有这条路的原文/, '★ 我们的盲'); // ⚠ 而【绝不许】让 axis-gap 那一句说成「制度没量你」—— 那正是这一档存在的理由的反面。 assert.ok(!/制度本来就没打算/.test(axisGap), '★★★ axis-gap 说成了制度的盲 —— 那是拿我们的表冒充那个朝代'); }); // ══════════════════════════════════════════════════════════════════════════ // ★★★★★ 2026-07-17 第三刀:【一道闸的那个数,是【代号】不是【刻度】】 // —— 「排在后面 ≠ 不参选」这一族的【第八次】,而这一次它是【一个用户读得到的数】。 // // ⚠ 本节断言的【方向】先说死,与本文件其余部分一致: // ①【它不断言引擎判得对】—— 它断言的是【本表说得起它说的那句话】; // ②【「这个数是挑的」必须是一个【返回值】,不是一句注释】(本文件铁律:注释挡不住任何人); // ③ ★★★【而最要紧的一条:它钉住的是一个【难看的数】—— 谁把它「修绿」了, // 谁就是在改引擎的判决,那要老板拍(见 dynasties.js not_done ⑨-c)。】 // ══════════════════════════════════════════════════════════════════════════ import { AXIS_VALUES, gateClassOf, AXES as AXES_, computeAxes as computeAxes_ } from '../src/dynasties.js'; test('★★★ 六轴只取得到五个值 —— 而这一条【是本表自己的曲线算出来的】,不是手写的常量', () => { // ★ 承重事实:(40,63) 之间【没有任何一个 profile 落得进去】。整个第八次全靠它。 for (const a of AXES_) { const vals = AXIS_VALUES[a.id]; assert.ok(Array.isArray(vals), `${a.id} 没有 AXIS_VALUES`); assert.equal(vals.length, a.subs.length + 1, `★ ${a.id}:n 项副天赋只给得出 n+1 个值`); assert.equal(vals[0], 40, '★ 基线 40 =「不以此见长」,✗ 不是「你毫无X」'); // ★ 逐值复算:必须与 computeAxes 真的跑出来的一致 —— ✗ 不许是一张手抄的表 for (let n = 0; n <= a.subs.length; n++) { assert.equal(computeAxes_(a.subs.slice(0, n))[a.id], vals[n], `★★ AXIS_VALUES.${a.id}[${n}] 与 computeAxes 对不上 —— 那它就是一张过期的抄表`); } assert.ok(!vals.some((v) => v > 40 && v < 63), `★★★ ${a.id} 在 (40,63) 之间有值 —— 那第八次的证明就塌了`); } }); test('★★★★★ 全表 21 道轴闸(六轴 20 + 经学 1):【每一道的那个数,都是本表在 41–63 里挑的一个】—— 零例外', () => { // ⚠ 2026-07-17 加西汉:西汉博士弟子多了一道 gate{jingxue:55}。★ 经学(第七轴)虽不是「六轴」, // 但它同样是 sat() 的产物(jingxue 只 1 个 sub ⇒ 只取得到 {40,63})⇒ gate{jingxue:55} 的等价类 // 照样是 41–63(23 个整数是同一道闸)。⇒ 它【不是例外,是又一个「本表挑的数」】,n 从 20 变 21。 let n = 0; for (const d of DYNASTIES) for (const c of d.channels) { for (const [axis, need] of Object.entries(c.gate || {})) { if (axis === 'wang' || axis === 'lao') continue; // ★ 它们不是 sat 的产物 —— 见下一条 n++; const g = gateClassOf(d.id, c.id, axis); assert.ok(g, `${d.id}/${c.id} gate{${axis}} 拿不到等价类`); assert.equal(g.need, need); assert.ok(g.size > 1, `★★ ${d.id}/${c.id} gate{${axis}:${need}} 的等价类只有一个数 —— 那它就是刻度了`); // ★★★ 【类里的每一个数,是同一道闸】—— 逐个复算,不是抽样 const vals = AXIS_VALUES[axis]; const passing = (x) => vals.filter((v) => v >= x).join(','); for (let x = g.class[0]; x <= g.class[1]; x++) { assert.equal(passing(x), passing(need), `★★★★ ${d.id}/${c.id}:gate{${axis}:${x}} 与 gate{${axis}:${need}} 开/关不一样 —— 那它们就不是同一道闸`); } // ★ 而那句话必须【出得了厂】:它是返回值,不是注释。 assert.match(g.note, /代号/); assert.match(g.note, /说不起/); } } // ⚠ 2026-07-17 接 25 朝:轴闸数不再写死(接朝后只增不减)。★ 不变量【不弱】:上面 for 逐条钉着 // 【每一道轴闸的 need 都在 41–63 的等价类里(size>1,是本表挑的代号,不是刻度)】,零例外。 assert.ok(n >= 21, `★ 全表轴闸数应 ≥ 21(原有 20 六轴闸 + 西汉经学闸 1;接朝后更多)—— 实得 ${n}`); }); test('★ gateClassOf 失效方向朝【关】—— 与 sameScale / comparableTier / closedBy 同一条规矩', () => { assert.equal(gateClassOf('NOPE', 'xiaolian', 'jiao'), null, '朝代问错 ⇒ null'); assert.equal(gateClassOf('han', 'NOPE', 'jiao'), null, '通道问错 ⇒ null'); assert.equal(gateClassOf('han', 'xiaolian', 'NOPE'), null, '轴问错 ⇒ null'); assert.equal(gateClassOf('han', 'bizhao', 'jiao'), null, '★ 这条闸上没有这条轴 ⇒ null,✗ 不许兜底'); // ★★★ 而【wang 返回 null 的意思是「这一问对它问不出口」,✗ 不是「它也是代号」】: // wang 是 0–100 的自由值 ⇒ gate{wang:30} 的 30 是【真的 30】,你的 29 是【真的 29】。 // ⚠ 这一条是本刀【自己差点栽的】那一格:第一版的文案对每一个缺口都说「这个数是本表挑的」, // 而拿它去说 wang【是假的】。★【一句拆自己台的话,本身也必须是真的】—— // 否则它就是又一句「摆在用户面前而什么都没买到」的话,正是本刀在抓的那一族。 assert.equal(gateClassOf('han', 'xiaolian', 'wang'), null, '★★ wang 不是 sat 的产物 —— 这一问对它问不出口'); assert.ok(!AXIS_VALUES.wang, '★★ wang 不许进 AXIS_VALUES —— 它不是六轴,它是另一把尺'); }); test('★★★★★ 李广:同一道闸的八个写法 → 一半判「军功」,一半判「辟召」', () => { // ★ 李广:renwu.js 双证 subs=[] ⇒ 六轴全 40。而《史記》卷109 双证钉着:他的实职【就是靠军功挣的】。 const li = { axes: computeAxes_([]), wang: 0 }; const jg = getDynasty('han').channels.find((c) => c.id === 'jungong_han'); const cls = gateClassOf('han', 'jungong_han', 'wu'); const orig = jg.gate.wu; const got = {}; try { for (const n of [41, 45, 50, 54, 55, 56, 60, 63]) { assert.ok(n >= cls.class[0] && n <= cls.class[1], `${n} 不在等价类里 —— 这条测试写错了`); jg.gate.wu = n; const r = computeRank(li, 'han'); // ★ 每一个写法【都是同一道闸】:他三条路照样全关,逐格相同 assert.equal(r.channel, null, '★ 换写法不许改变「一条路都不开」这件事 —— 那才叫同一道闸'); assert.equal(r.blocked.length, 3); got[n] = r.nearest; } } finally { jg.gate.wu = orig; } // ★★★★★ 这就是这一刀的判词:同一道闸,两个答案。 assert.equal(new Set(Object.values(got)).size, 2, '★★★ 同一道闸的八个写法,判出的 nearest 应当【有两种】—— 而那正是问题所在'); assert.match(got[41], /军功/, '★★ 写 41(同一道闸)⇒ 军功是最接近的那条'); assert.match(got[54], /军功/); assert.match(got[55], /辟召/, '★★ 写 55(同一道闸)⇒ 军功不见了'); assert.match(got[63], /辟召/); // ⇒ ★★★★【「军功不是李广最接近的那条」不是一个关于李广的事实,是【本表挑了 55】这个事实。】 // ⚠ 谁要动它:改判决要老板拍(not_done ⑨-c),✗ 不许把这条测试删了了事。 assert.equal(orig, 55, '★ 本表现在挑的是 55 —— 这一条钉住它:改了就要在这里说清楚为什么'); }); test('★★★★ nearest 自陈【是哪一级判的】—— 而「本表真的有理由」的只剩 count 一级', () => { const li = { axes: computeAxes_([]), wang: 0 }; const byIdx = computeRank(li, 'han'); assert.equal(byIdx.nearestBy, 'idx', '★ 李广:条数打平、缺口打平 ⇒ 本表的编号'); assert.equal(byIdx.nearestGap, null, '★ 不是 gap 判的 ⇒ nearestGap 必须是 null,✗ 不许给一个空壳'); assert.deepEqual(byIdx.nearestTie, ['军功']); // 交游点过一项(63≥55)、wang=29(差 1) ⇒ 察举只差声望;辟召差吏能 15;军功差果决 15 ⇒ gapOf 裁决 const g = computeRank({ axes: computeAxes_(['warm']), wang: 29 }, 'han'); assert.equal(g.nearestBy, 'gap'); assert.ok(g.nearestGap); // ★★★ 【真尺 vs 代号】—— 汉的 gapOf 裁决【每一次】都是这一格(穷举:960 + 960 = 1,920 条次) assert.equal(g.nearestGap.mine.kind, 'scale', '★★ 声望是 0–100 的自由值 ⇒ 它的「差 1」是真的 1 分'); assert.equal(g.nearestGap.rivals.length, 2, '★★ gapOf 是拿【全部】同条数的对手比的 —— ✗ 只报第一个就是少报了一半'); assert.ok(g.nearestGap.rivals.every((r) => r.kind === 'codename'), '★★ 吏能/果决的「差 15」都是 41–63 里挑的 55 减出来的'); assert.equal(g.nearestGap.mixedKind, true, '★★★ 本表把一把真尺子和一个代号,放进了同一个 Math.max'); assert.equal(g.nearestGap.sameAxis, false); // ★★ 而那句拆自己台的话【必须每一个字都是真的】: const line = g.reasons.find((x) => /最接近的那条是怎么挑出来的/.test(x)); assert.ok(line, '★ gapOf 裁决了,而它没出声'); assert.match(line, /吏能差 15」这个数不是一段距离/, '★★ 代号那一边必须自陈是代号'); assert.match(line, /41–63 这 23 个/, '★★★ 必须报出那个等价类 ——「差 15」的全部依据就是本表挑了 55'); assert.match(line, /两个数不是同一种东西/, '★★★ 真尺 vs 代号这一格必须说出来'); // ⚠⚠ 本刀差点栽的那一格:✗【绝不许】把声望那 1 分也说成「挑的」—— 那句话对 wang 是【假的】。 assert.ok(!/声望差 1」这个数不是一段距离/.test(line), '★★★★ 把真尺说成代号 —— 那句拆台的话自己成了假话'); }); test('★ nearestBy 与 nearest 的 sort 【逐字同源】—— ✗ 不许两把尺子', () => { const SUBS_ = SUB_REGISTRY.map((s) => s.id); const seen = { count: 0, gap: 0, idx: 0 }; for (let m = 0; m < 512; m++) { const subs = []; for (let i = 0; i < 16; i++) if (m & (1 << i)) subs.push(SUBS_[i]); for (const wang of [0, 29, 30, 55, 75]) for (const d of DYNASTIES) { const r = computeRank({ axes: computeAxes_(subs), wang }, d.id); if (r.channel || !r.nearest) continue; assert.ok(['count', 'gap', 'idx'].includes(r.nearestBy), `nearestBy 冒出了第四个值:${r.nearestBy}`); seen[r.nearestBy]++; // ★ idx ⟺ nearestTie 非空;gap ⟺ nearestGap 非 null。两处不许分叉。 assert.equal(r.nearestBy === 'idx', r.nearestTie.length > 0, '★★ nearestBy 与 nearestTie 分叉了'); assert.equal(r.nearestBy === 'gap', r.nearestGap !== null, '★★ nearestBy 与 nearestGap 分叉了'); assert.equal(r.nearestIsDefault, r.nearestBy === 'idx'); } } assert.ok(seen.idx > 0 && seen.gap > 0, '★ 抽样里 idx 与 gap 两级都得跑到,否则这条测试什么都没验'); }); test('★★ 时代错置:本表对【自己写的文案】,要和对【引文】用同一条纪律', () => { // ★ 本表对宋武举「天圣八年」那句的判决逐字是「故连句不引」—— 早了 38 年就不引。 // ⇒ 那对【本表自己举的例子】也得是同一条:例子和引文一样在用户眼前。 const wj = getDynasty('weijin').channels.find((c) => c.id === 'jungong_wj'); assert.ok(!/[刘劉]裕/.test(wj.gateNote), '★★ 刘裕是东晋末的人,而本朝 era=西晋 span=265–316 —— 晚了近百年'); const fz = getDynasty('tang').channels.find((c) => c.id === 'fanzhen'); assert.ok(!/中晚唐/.test(fz.gateNote), '★★ 本朝 era=盛唐 span=712–755 —— 本表自己的文案把它放在了 span 之外'); // ★ 而【删掉之后,不许有人补一个我没查过的说法回来】—— 那是假订正。两条通道的 kemuAbsence 钉着这句话。 assert.match(fz.xuan.kemuAbsence.why, /不许有人补一句「盛唐求进的实路」/); assert.match(wj.xuan.kemuAbsence.why, /不许有人再拿一个东晋的人回来举例/); // ⚠ 而它们【仍然没有 source】—— 删掉一个错例子,不等于补上了一条原文。 assert.equal(wj.source, undefined); assert.equal(fz.source, undefined); }); test('★★★★★ 唐:进士与行卷【对文采的要求是同一道闸】—— 而本表一处写 50、一处写 55', () => { // ★ 这一条比李广那条更干净:它连「跨轴」这个辩解都没有 —— 两道闸【在同一条轴上】。 const a = gateClassOf('tang', 'jinshi', 'wen'); const b = gateClassOf('tang', 'xingjuan', 'wen'); assert.equal(a.need, 50); assert.equal(b.need, 55); // ★★ 而它们是【同一道闸】:wen ≥ 50 ⟺ wen ≥ 55 ⟺ wen ∈ {63,73,79} assert.deepEqual(a.class, b.class, '★★★ 两道闸的等价类不一样 —— 那本条的判词就塌了'); const vals = AXIS_VALUES.wen; assert.deepEqual(vals.filter((v) => v >= 50), vals.filter((v) => v >= 55), '★★★★ 「文采 50」与「文采 55」放行的是同一批人 —— 它们是同一道闸'); // ⚠ 要撞到【同轴】那一格,行卷的另一道闸 jiao 必须先过(否则它差两条,根本不同条数)。 // ⇒ 点一项交游 ⇒ jiao=63≥55。此时:进士差[文采 10]、行卷差[文采 15]、藩镇差[吏能 15],各 1 条。 const p = { axes: computeAxes_(['warm']), wang: 0 }; assert.equal(p.axes.wen, 40); assert.ok(p.axes.jiao >= 55); const r = computeRank(p, 'tang'); assert.equal(r.channel, null, '★ 前提:文采 40 的唐人,一条路都不开'); const gapOn = (re, axis) => { const m = r.blocked.find((x) => re.test(x.channel)).missing.find((y) => y.axis === axis); return m.need - m.got; }; assert.equal(gapOn(/进士/, 'wen'), 10); assert.equal(gapOn(/行卷/, 'wen'), 15); // ★★★★★【那 5 分不是文采。那 5 分是本表一处写了 50、一处写了 55。】 assert.equal(r.nearestBy, 'gap'); assert.match(r.nearest, /进士/, '★★ 引擎判「你离进士近」—— 凭那 5 分'); assert.equal(r.nearestGap.mine.kind, 'codename'); assert.ok(r.nearestGap.rivals.some((x) => /行卷/.test(x.channel) && x.axis === 'wen'), '★★★ 行卷必须在 rivals 里 —— 它就是那个「同一道闸、另一个写法」的对手'); assert.ok(r.nearestGap.rivals.every((x) => x.kind === 'codename'), '★★★★ 这一比里,每一个数都是【挑的】'); // ⚠ 而它【碰巧与编号同答】:把进士的 50 改写成 55(★ 同一道闸)⇒ nearest 一格都不变。 // ★ 那正是本文件的铁律:【「碰巧成立」和「守着」是两回事。】 const js = getDynasty('tang').channels.find((c) => c.id === 'jinshi'); try { js.gate.wen = 55; const r2 = computeRank(p, 'tang'); assert.equal(r2.channel, null, '★ 同一道闸 ⇒ 开/关必须一格不动'); assert.equal(r2.nearest, r.nearest, '★ 改成 55 之后 nearest 没变 —— 它凭的是运气,不是守着'); assert.equal(r2.nearestBy, 'idx', '★★ 而这时它自陈的是【编号】—— ★ 同一个答案,两个理由'); } finally { js.gate.wen = 50; } }); test('★★★★★ 明:科举与吏员出职【对吏能的要求是同一道闸】—— 而那 5 分决定了「你离哪条路近」', () => { // ★★ 这一条是全表最干净的一格,理由不是它的数,是【它落在哪一朝】: // 明正是「位阶即杠杆」这条纪律的立身之本(computeRank 逐字:「明代一个能过科举(ceiling 5) // 的人,吏员出职(ceiling 2)那条路的原始分往往更高……通道的价值在天花板,不在原始分」)。 const a = gateClassOf('ming', 'keju', 'fa'); const b = gateClassOf('ming', 'lisi', 'fa'); assert.equal(a.need, 50); assert.equal(b.need, 55); // ★★★ 同一条轴、同一个等价类 ⇒ 【同一道闸】:fa ≥ 50 ⟺ fa ≥ 55 ⟺ fa ∈ {63,73,79,83} assert.deepEqual(a.class, b.class); const vals = AXIS_VALUES.fa; assert.deepEqual(vals.filter((v) => v >= 50), vals.filter((v) => v >= 55), '★★★★ 「吏能 50」与「吏能 55」放行的是同一批人 —— 科举与吏员对吏能的要求【一模一样】'); // 沉毅满(heng=73≥55 过) + 吏能 40 ⇒ 科举差[吏能 10]、吏员差[吏能 15],各 1 条 ⇒ 同轴 const p = { axes: computeAxes_(['patient', 'calm']), wang: 0 }; assert.equal(p.axes.fa, 40); assert.ok(p.axes.heng >= 55); const r = computeRank(p, 'ming'); assert.equal(r.channel, null, '★ 前提:吏能 40 的明人,两条路都不开'); assert.equal(r.nearestBy, 'gap'); assert.equal(r.nearestGap.sameAxis, true, '★ 这一格是【同轴】—— gapOf 唯一一个看似站得住的用法'); assert.match(r.nearest, /科举/); assert.equal(r.nearestGap.mine.gap, 10); assert.equal(r.nearestGap.rivals[0].gap, 15); // ★★★★★【那 5 分不是吏能。那 5 分是本表一处写了 50、一处写了 55。】 assert.equal(r.nearestGap.mine.kind, 'codename'); assert.equal(r.nearestGap.rivals[0].kind, 'codename'); assert.equal(r.nearestGap.mixedKind, false, '★ 两边都是代号 ⇒ 这一格不是「真尺 vs 代号」,是「代号 vs 代号」'); // ★★ 而那句话必须【当着用户的面把这一点说穿】——这是本表唯一一格「同轴」, // 若连这一格都不说,那这个字段就只是在挑软柿子捏。 const line = r.reasons.find((x) => /最接近的那条是怎么挑出来的/.test(x)); assert.match(line, /是同一条轴/, '★★ 同轴那一格必须自陈它是同轴 —— ✗ 不许拿「跨轴」当唯一的罪名'); assert.match(line, /那个「差得少」的差,全部来自本表在这两处写了两个不同的数/); assert.ok(!/不是同一条轴/.test(line), '★★★ 同轴的一格说成「不是同一条轴」—— 那句拆台的话自己成了假话'); }); test('★★★★ 自查:这一刀新加的每一个取值,都必须真的跑得到 —— ✗ 不许造第二个空操作', () => { // ⚠ 本文件的铁律逐字:「不制造第二个」。而第八刀在「让它出声」那一刀里【自己造了一个】(byZelu), // 本刀又造了一个(kind:'unique',已删)。★ ⇒ 这条测试是那个教训的【结构化】: // 【一个新字段,写完先问它自己:你的每一个取值,跑到了几格?】 const SUBS_ = SUB_REGISTRY.map((s) => s.id); const hit = { count: 0, gap: 0, idx: 0, sameAxis: 0, crossAxis: 0, mixed: 0, unmixed: 0, scale: 0, codename: 0, rivals2plus: 0 }; for (let m = 0; m < 4096; m++) { const subs = []; for (let i = 0; i < 16; i++) if (m & (1 << i)) subs.push(SUBS_[i]); for (const wang of [0, 29, 30, 55, 75]) for (const d of DYNASTIES) { const r = computeRank({ axes: computeAxes_(subs), wang }, d.id); if (r.channel || !r.nearest) continue; hit[r.nearestBy]++; if (r.nearestBy !== 'gap') continue; const g = r.nearestGap; hit[g.sameAxis ? 'sameAxis' : 'crossAxis']++; hit[g.mixedKind ? 'mixed' : 'unmixed']++; if (g.rivals.length > 1) hit.rivals2plus++; for (const s of [g.mine, ...g.rivals]) hit[s.kind]++; } } for (const [k, v] of Object.entries(hit)) { assert.ok(v > 0, `★★★★ 【${k}】一格都没跑到 —— 那它就是一个新的空操作。删它,或者说清楚它为什么在。`); } // ★ 而 kind 只有两档:'codename'(六轴,等价类 ≥4)与 'scale'(wang,真尺)。 // ⚠ 第三档 'unique'(等价类只有一个数)【已删】——【它不可能跑到】: // 逐轴扫 need 1–100,最小的等价类是 4。⇒ 与 penaltyIfNoWang 同形(条件与事实互斥)。 for (const [axis, vals] of Object.entries(AXIS_VALUES)) { const passing = (n) => vals.filter((v) => v >= n).join(','); const sizes = {}; for (let n = 1; n <= 100; n++) { const k = passing(n); sizes[k] = (sizes[k] || 0) + 1; } assert.ok(Math.min(...Object.values(sizes)) > 1, `★★★ ${axis} 上出现了 size===1 的等价类 —— 那 kind:'unique' 就该活过来,这条注释也得改`); } });