// 开考 —— 成绩单的断言。 // // ★★★ 这个文件的立场,先说死,因为它决定了每一条断言的【方向】: // // 本文件【不断言引擎是对的】。它断言的是【成绩单没有说谎】。 // ⇒ 所以下面凡是「引擎答了什么」的断言,钉的都是【难看的那个数】: // 0/8、tier 恒 3、通道 0/3、军功 0 次。 // ★ 【谁把这些断言改绿了,谁就是在改引擎 —— 而那是这一刀的头号红线。】 // 老板原话:「引擎错了就让它错着 —— 报上来,别偷偷调参数。 // 调参数让它及格 = 作弊,而作弊的对象是司马迁。」 // // ⇒ ★★ 于是本文件的断言【故意是反着的】:它们守的不是「引擎及格」,是【「引擎没有偷偷及格」】。 // 一条断言变红有两种可能,而它们的处置【完全相反】: // · 成绩单算错了 ⇒ 修 kaoyan.js。 // · 【引擎变了】 ⇒ ★ 那正是这张表该响的时刻(renwu.js WHY_NO_IMPORT 逐字)。 // ✗ 【不许改断言去迁就它】,先去看引擎为什么变。 // // ══════════════════════════════════════════════════════════════════════════ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { RUBRIC, DIVISION, REPORT, FINDINGS, NOT_DONE, CONSTANT_PROOF, TIAN_HAS_NO_SAMPLE, gradeOne, gradeAll, tierScore, toleranceCurve, nullModels, channelScore, constantTierOf, discriminationOf, discriminationAll, varyingChannels, reportLines, axesSeenBy, unreachableChannels, unreachableAll, } from '../src/kaoyan.js'; import { RENWU, subsOf, GRADE as RENWU_GRADE } from '../src/renwu.js'; import { computeRank, computeAxes, profileFromSubs, DYNASTIES, AXES } from '../src/dynasties.js'; import { SUB_REGISTRY } from '../src/talents.js'; import { assertNoGameMechanics } from '../src/decisions.js'; // 2026-07-17 成绩单的冻结输入。7 月 31 日新增的三项擅长属于后续版本, // 不应偷偷改写这张历史成绩单的精确分布;当前全量注册表另有结构断言守住。 const EXAM_SUBS_2026_07_17 = SUB_REGISTRY .map((item) => item.id) .filter((id) => !['eloquent', 'analytical', 'bold'].includes(id)); // ══════════════════════════════════════════════════════════════════════════ // ① 定位与分工 —— ★ kaoyan 是叶子:谁都不许 import 它 // ══════════════════════════════════════════════════════════════════════════ test('★ 分工:renwu 是钉死的快照(证词),kaoyan 是实时的成绩单(报警器)', () => { assert.ok(DIVISION.claim.includes('钉死')); assert.ok(DIVISION.direction.includes('不许改 renwu.GRADE'), '断言的方向必须写死在数据里'); }); test('★★★ 成绩单【不许】手上有一份标准答案的抄件 —— kaoyan.js 不许 import renwu 的 GRADE', async () => { const { readFileSync } = await import('node:fs'); const src = readFileSync(new URL('../src/kaoyan.js', import.meta.url), 'utf8'); const importBlock = src.match(/import\s*\{[^}]*\}\s*from\s*['"]\.\/renwu\.js['"]/s); assert.ok(importBlock, '找不到它 import renwu.js 那一行'); assert.ok( !/\bGRADE\b/.test(importBlock[0]), '★★ 判卷子的人手上有一份标准答案的抄件 —— 那就有一百种「顺手对一下」的写法。\n' + ' ⇒ 比对必须放在【这个文件】里做,因为只有这里断言的方向是写死的:\n' + ' 分叉 = 引擎变了,✗ 不许改 GRADE 去迁就实时结果。' ); }); test('★★ kaoyan 是叶子 —— src/ 里不许有任何文件 import 它(监考官不许坐进考场)', async () => { const { readdirSync, readFileSync } = await import('node:fs'); const { join } = await import('node:path'); const { fileURLToPath } = await import('node:url'); const dir = fileURLToPath(new URL('../src/', import.meta.url)); const offenders = []; for (const f of readdirSync(dir)) { if (!f.endsWith('.js') || f === 'kaoyan.js') continue; const txt = readFileSync(join(dir, f), 'utf8'); // ⚠ 只扫【真的 import】,不扫注释里提到这个名字(本项目多处注释会引用文件名)。 if (/^\s*import[^;]*['"]\.\/kaoyan\.js['"]/m.test(txt)) offenders.push(f); } assert.deepEqual(offenders, [], '★ 判卷子的文件被考生 import 了 —— 那不叫成环,那叫监考官坐进考场'); }); test('★ 红线:成绩单是数据,会走到人眼前 —— 不许有手游机制的字样', () => { // 照 lao.test.js / chushen.test.js 的先例:扫【数据】,不扫注释。 assert.deepEqual(assertNoGameMechanics(REPORT), []); assert.deepEqual(assertNoGameMechanics(RUBRIC), []); assert.deepEqual(assertNoGameMechanics(FINDINGS), []); assert.deepEqual(assertNoGameMechanics(gradeAll()), []); }); // ══════════════════════════════════════════════════════════════════════════ // ② 口径 —— ★ 先定死,且它的【代价】必须跟着它一起写在数据里 // ══════════════════════════════════════════════════════════════════════════ test('★★★ 容差焊死在 0 —— 而且它的理由是 shiliao.js 的铁律,不是本刀的洁癖', () => { assert.equal(RUBRIC.tolerance, 0, '★ 容差是这场考试里唯一一个我有权调的参数。它被焊死了。'); assert.ok(RUBRIC.toleranceWhy.includes('一个「比」字差一级'), '口径必须挂着它的依据'); assert.ok(RUBRIC.toleranceCost.includes('反作弊'), '口径必须连着它的代价一起写'); }); test('★★★★ 「先定口径再看结果」这条规矩,本刀在字面上没做到 —— 而这必须招在数据里', () => { const confess = NOT_DONE.find((x) => x.id === 4); assert.ok(confess, '★ 招认必须是数据,不是注释 —— 注释挡不住任何人(本项目写了三遍)'); assert.ok(confess.detail.includes('已经知道每一格的答案了')); assert.ok(confess.detail.includes('我说了不算'), '★ 不许把「我保证」写成一条交付物'); }); test('★★ 三种结果分开统计 —— 不许混成一个百分比', () => { assert.deepEqual(Object.keys(RUBRIC.outcomes).sort(), ['right', 'tooHigh', 'tooLow', 'unjudgeable']); assert.ok(RUBRIC.outcomes.tooLow.means.includes('引擎缺了一个变量')); assert.ok(RUBRIC.outcomes.tooHigh.means.includes('「天」')); // ★ 「判不了」既不许算错,也不许算对 —— 照 renwu.js GRADE.score.ungradeableNote 的口径。 assert.ok(RUBRIC.outcomes.unjudgeable.means.includes('不许算成「错」')); assert.ok(RUBRIC.outcomes.unjudgeable.means.includes('不许算成「对」')); }); // ══════════════════════════════════════════════════════════════════════════ // ③ ★★★ 成绩单 —— 钉死难看的那个数 // ══════════════════════════════════════════════════════════════════════════ test('★★★★ 位阶:0 / 8。★ 这条断言是钉住「引擎没有偷偷及格」的,不是钉住它不及格', () => { const s = tierScore(); assert.equal(s.tolerance, 0); assert.equal(s.gradeable, 8, '八个可判分;另两个答案不在这把尺子上'); assert.equal(s.right, 0, '★ 0 / 8'); assert.equal(s.tooLow, 8, '★★ 八格【全部是「算低了」】—— 引擎缺了一个变量,逐格见 whichVariable'); assert.equal(s.tooHigh, 0, '★★★ 「算高了」= 0 格 —— 这就是「天」,而这场考试给不了它样本'); assert.equal(s.unjudgeable, 2, '董仲舒(诸侯王相)、程不识(比千石)—— 答案不在这把尺子上'); }); test('★★★ 「算高了」= 0,而这【不是引擎不高估,是样本里没有一个没做到的人】', () => { assert.equal(tierScore().tooHigh, 0); assert.ok(TIAN_HAS_NO_SAMPLE.why.includes('幸存者偏差')); assert.ok(TIAN_HAS_NO_SAMPLE.why.includes('只记【坐上了位子的人】')); // ★ 唯一的候选人是李广 —— 而拿它当答案就是让辩护人当考官。这条限定语不许丢。 assert.ok(TIAN_HAS_NO_SAMPLE.theOnlyCandidate.includes('李广')); assert.ok(TIAN_HAS_NO_SAMPLE.theOnlyCandidate.includes('让辩护人来当考官')); }); test('★★ 通道准确率与位阶准确率【分开】—— 老板:通道对不对比位阶对不对更重要', () => { const c = channelScore(); assert.equal(c.gradeable, 3, '★ 十人里只有三个走的是引擎【有】的路:张汤·辟召,李广/李蔡·军功'); assert.equal(c.right, 0, '★★★ 通道 0 / 3'); assert.equal(c.tooLow, 3, '★★★★ 三个全是「引擎说他一条路都不开」—— 不是指错了门,是说他一扇门都进不去'); assert.equal(c.wrong, 0); assert.equal(c.unjudgeable, 7, '★ 七个走的路引擎没有:文学/任子/贤良/博士/侍中/选诣博士/直谏'); assert.equal(c.notOnTheExam.length, 7); }); test('★★★ 而通道那三格,比老板设想的那一版更难看', () => { // 老板的原话是「引擎说张汤走【察举孝廉】而他其实是权贵援引」。实测:引擎说他【一条路都不开】。 const zt = gradeOne('zhangtang'); assert.equal(zt.engine.channelId, null, '★ 引擎没有指错门 —— 它说张汤一扇门都进不去'); assert.equal(zt.real.engineChannel, 'bizhao', '★★ 而他真的从引擎自己有的那扇门(辟召)进去了'); assert.equal(zt.real.office, '御史大夫'); assert.equal(zt.channelVerdict, 'tooLow'); assert.ok(RUBRIC.channelWhy.includes('它说张汤一条路都走不了'), '★ 这个出入必须写在口径里,不许悄悄换题'); }); test('★★★★ 李广:引擎给 0,史官给 5 —— 而他正是靠军功起家的,却被军功闸挡在门外', () => { const g = gradeOne('liguang'); assert.deepEqual(g.engine.subs, [], '★ subs 空 —— 「訥口少言」落不到十六项说话风格上,这是定义不是巧合'); assert.equal(g.engine.tier, 0); assert.equal(g.engine.title, '郡县小吏'); assert.equal(g.real.tier, 5, '★★ 《史記》卷109 逐字:「官不過九卿」= 本表 ladder 的 tier5'); assert.equal(g.tierVerdict, 'tooLow'); assert.equal(g.tierGap, -5, '★★★ 满格的错'); assert.equal(g.real.engineChannel, 'jungong_han', '★ 军功 —— 引擎【有】这条路'); assert.equal(g.engine.channelId, null, '★★★★ 而引擎把他挡在门外'); assert.ok(g.whichVariable.includes('两项都是说话'), '★ 军功闸量的是他最不擅长的那一样,而那一样与打仗无关'); }); test('★★★★ 李蔡:史官明说「爲人在下中」,而他位至三公 —— 这是史料自带的对照实验', () => { const guang = gradeOne('liguang'); const cai = gradeOne('licai'); // ★ 制度变量【完全不变】:同朝、同帝、同军、同族(李蔡是李广的从弟)。 assert.equal(guang.engine.tier, cai.engine.tier, '两人引擎给的位阶相同(都是 0)'); assert.equal(guang.real.tier, cai.real.tier, '两人实职都落在 ladder tier5'); assert.equal(guang.real.office, '郎中令'); assert.equal(cai.real.office, '丞相', '★★ 而「在下中」的那个做到了丞相,「天下无双」的那个官不過九卿'); assert.ok(cai.huaxiang.quote.includes('蔡爲人在下中')); assert.ok(cai.huaxiang.quote.includes('位至三公')); // ⚠ 而这一条【必须带限定语】:一个样本 + 一个有立场的记录者,够不上全称判断。 const f = RENWU_GRADE ? RENWU.find((r) => r.id === 'licai') : null; assert.ok(f.mappingNote.includes('够不上一个全称判断'), '★ 限定语不许丢'); }); test('★★ 每一格都能点回原文 —— quote + 卷次 + 见证,一格都不许空', () => { for (const g of gradeAll()) { assert.ok(g.huaxiang.quote.length > 0, `${g.name} 缺画像原文`); assert.ok(/《.+》卷/.test(g.huaxiang.source), `${g.name} 的画像出处没有卷次:${g.huaxiang.source}`); assert.ok(g.huaxiang.verified.length > 0, `${g.name} 的画像没有 verified`); assert.ok(g.real.quote.length > 0, `${g.name} 缺实职原文`); assert.ok(g.real.source.length > 0, `${g.name} 缺实职出处`); assert.ok(g.real.channelQuote.length > 0, `${g.name} 缺入仕途径原文`); // ★ 论赞【不许】进映射 —— 那是拿答案当题目。 assert.ok(['weiren', 'xushi'].includes(g.huaxiang.level), `${g.name} 的画像层级不许是论赞:${g.huaxiang.level}`); } }); test('★ 成绩单能读,且一行就是老板要的那个格式', () => { const lines = reportLines(); assert.equal(lines.length, 10); // 「晁错,錯為人陗直刻深。(《史記》卷一百一)→ 引擎:辟召…→ 实:御史大夫(tier5)✗低」 assert.ok(lines[0].startsWith('晁错,')); assert.ok(lines[0].includes('《史記》卷一百一')); assert.ok(lines[0].includes('→ 引擎:')); assert.ok(lines[0].includes('→ 实:御史大夫')); for (const l of lines) assert.ok(/→ 引擎:.*→ 实:/.test(l), `读不了:${l}`); }); test('★★ 每一格都标着 anachronism —— 这场考试不合法,而它是数据里的字段,不是注释里的话', () => { for (const g of gradeAll()) assert.equal(g.anachronism, true, `${g.name} 没标时代错置`); assert.equal(REPORT.invalid, true); assert.ok(REPORT.invalidWhy.includes('那个数不存在,因为题目错配')); // ★ 四个御史大夫 —— 那个官在引擎的那个汉里根本不存在。 const ysdf = gradeAll().filter((g) => g.real.office === '御史大夫'); assert.equal(ysdf.length, 4, '晁错/张汤/桑弘羊/兒宽'); for (const g of ysdf) assert.ok(g.anachronismNote.includes('根本不存在')); }); // ══════════════════════════════════════════════════════════════════════════ // ④ ★★★★ 反作弊 —— 容差曲线与空模型。【这两样不依赖我诚不诚实。】 // ══════════════════════════════════════════════════════════════════════════ test('★★★ 容差曲线:0..5 全报 —— 「选一个容差」这个动作被取消了', () => { const curve = toleranceCurve(); assert.equal(curve.length, 6, '★ 六个值全报,不许只报一个'); assert.deepEqual(curve.map((c) => c.right), [0, 0, 4, 4, 4, 8], '★ 容差一放到 2,分数从 0 跳到 4'); // ★★ 曲线的牙齿:给定本卷的答案(全是 tier 5),容差 t 实际放行哪几个预测值。 // ⚠ 【订正标本】:本刀第一版报的是名义窗宽 2t+1(「容差 2 覆盖 83%」)—— // ✗ 错的:答案全在梯子顶端,窗口被边界截掉一半。实际是 {3,4,5} = 50%。 assert.deepEqual(curve[0].accepts, [5], '容差 0:只有「三公」算命中'); assert.deepEqual(curve[1].accepts, [4, 5]); assert.deepEqual(curve[2].accepts, [3, 4, 5], '★★★ 容差 2:「县令」开始算命中'); assert.equal(curve[2].acceptsPct, '50%', '★ 不是 83% —— 那是名义窗宽,不是这张卷子上的实际'); assert.deepEqual(curve[5].accepts, [0, 1, 2, 3, 4, 5], '容差 5:什么都算命中 ⇒ 8/8'); // ★★★★ 而这一栏是这条曲线真正的判词: // 【容差一到 2,「县令」就算命中 —— 而引擎 99.5% 的时候就说县令。】 // ⇒ 容差 ≥ 2 时,这场考试问的不再是「引擎算得准不准」,是「引擎说没说县令」。 assert.deepEqual(curve.map((c) => c.acceptsTheEngineMode), [false, false, true, true, true, true]); assert.equal( curve.findIndex((c) => c.acceptsTheEngineMode), 2, '★ 引擎的众数(tier 3)从容差 2 开始被放行 —— 而它的分数正好从容差 2 开始从 0 跳到 4。这不是巧合。' ); }); test('★★★★ 空模型:一个「永远说三公」的预测器满分 —— 这张卷子测不出准确率', () => { const n0 = nullModels(0); assert.equal(n0.of, 8); assert.equal(n0.engine, 0, '引擎 0 / 8'); assert.equal(n0.alwaysTop5, 8, '★★★ 【一个只会说「你能做到三公」的东西,容差 0 拿 8/8 满分】'); assert.equal(n0.alwaysConstant3, 0); assert.equal(n0.alwaysBottom0, 0); // ⇒ ★ 八个可判分的人,实职【全是 tier 5】—— 一张所有答案都相同的卷子,只奖励猜那个答案的人。 const reals = gradeAll().filter((g) => g.real.tier !== null).map((g) => g.real.tier); assert.deepEqual([...new Set(reals)], [5], '★★ 答案栏里只有一个值。这就是这张卷子废掉的原因。'); }); test('★★★ 引擎被一个常数支配:没有一个容差上它赢过「永远说县令」', () => { const rows = []; for (let t = 0; t <= 5; t++) { const n = nullModels(t); rows.push({ t, engine: n.engine, const3: n.alwaysConstant3 }); assert.ok(n.engine <= n.alwaysConstant3, `★ 容差 ${t}:引擎 ${n.engine} > 常数 ${n.const3}?那这条断言该重写`); } assert.deepEqual(rows.map((r) => r.engine), [0, 0, 4, 4, 4, 8]); assert.deepEqual(rows.map((r) => r.const3), [0, 0, 8, 8, 8, 8]); // ★★★★ 而【引擎输的那四格,正是它「敢于不说 3」的那四格】。 const deviants = gradeAll().filter((g) => g.engine.tier !== 3); assert.equal(deviants.length, 4); for (const d of deviants) assert.equal(d.engine.tier, 0, '偏离常数的四次,全是判 tier 0'); assert.deepEqual(deviants.map((d) => d.name), ['张汤', '李广', '李蔡', '兒宽']); // ⚠⚠ 【订正 —— 这条断言的上一版是错的,本文件自己的测试当场抓出来的,标本留在这里】: // 我本来断言「四个人的 subs 【全部】是空的」。✗ 兒宽的 subs 是 [warm, calm],不是空的。 // ⇒ 准确的说法:三个 subs 空;第四个(兒宽)点了两项,而那两项【在汉一项都没有闸看得见】。 const empties = deviants.filter((d) => d.engine.subs.length === 0); assert.deepEqual(empties.map((d) => d.name), ['张汤', '李广', '李蔡']); const nikuan = deviants.find((d) => d.name === '兒宽'); assert.deepEqual(nikuan.engine.subs, ['warm', 'calm'], '★ 他点了两项 —— 而结果与 subs 全空的人一模一样'); }); // ══════════════════════════════════════════════════════════════════════════ // ⑤ ★★★★ 老板点名要验的那一条:区分度 // 「所有人都算成 tier 3 —— 那说明什么?(lisi 480/480 全是 2。字典序换掉它了,但请验证。)」 // ══════════════════════════════════════════════════════════════════════════ test('★★★ 解析:kemu 空 ⇒ tier ≡ clamp(seat,0,cap) —— 这是恒等式,不是采样', () => { const han = constantTierOf('han', 'bizhao'); assert.equal(han.constant, true); assert.equal(han.tier, 3); assert.ok(han.why.includes('与 profile 无关')); // ★ 失效方向朝【关】:问错了必须答「不」(同 sameScale / usableAsInstitution / closedBy)。 assert.equal(constantTierOf('TYPO', 'bizhao'), null); assert.equal(constantTierOf('han', 'TYPO'), null); }); test('★★★★ 全表 24 条通道,22 条是常数 —— 两条例外(宋武举 + 西汉博士弟子),都有原文可依的科目', () => { // ══════════════════════════════════════════════════════════════════════════ // ⚠ 2026-07-17 加西汉(第八朝)+ 经学轴。这条断言【真的变了】,因为结构真的变了: // · 通道 20 → 24(西汉四条:孝廉/贤良/博士弟子/军功)。 // · 常数 19 → 22(西汉三条盲路 kemu 空 ⇒ 恒 tier0;博士弟子 kemu 非空 ⇒ 非常数)。 // · 非常数 1 → 2:宋武举【依旧是唯一挂 keySource 的那条】,而西汉博士弟子是【第二条 kemu 非空的】。 // ★ 差别是【一档 vs 多档】:宋武举 kemu 两条(武艺、策)且有【次序】(《宋史》卷157)⇒ keySource 非空; // 博士弟子 kemu 只一条(通一藝)⇒ 没有次序可排 ⇒ keySource=null(排序键无原文可依)。 // ⇒ 【但两条的 kemu 都有原文】:宋武举「策、武藝俱優…」、博士弟子「能通一藝以上…」(卷88 制曰可)。 // ★★ 判词照旧成立:引擎【在有原文的科目处区分,在没有的地方不区分】——一处不多,一处不少。 // 补了经学这一条轴,西汉博士弟子就从「常数」变成了「能跑」——正是「补一维、当场落地」。 // ══════════════════════════════════════════════════════════════════════════ const all = discriminationAll(); const totalChannels = all.reduce((a, d) => a + d.channels, 0); const totalConstant = all.reduce((a, d) => a + d.constantChannels, 0); // ⚠⚠ 2026-07-17 拆墙(25 朝):朝数/通道数【不再写死】—— 从引擎动态派生。 // ★ 这不是「改测试迁就代码」,是让测试描述【不变量】: // 不变量 = 【全表只有两条 kemu 非空的通道(宋武举 + 西汉博士弟子),其余全是常数】。 // ⇒ 接进来的每一朝,通道 kemu 都是空的(尺子已抠好,但六轴接不住那些科目)—— // 所以「非常数通道恒为 2」是接朝之后仍然成立的不变量,不是一个会随朝数漂移的数。 const channelSum = DYNASTIES.reduce((a, d) => a + d.channels.length, 0); assert.equal(totalChannels, channelSum, '★ 通道总数 = 全表逐朝通道数之和(动态派生,不写死朝数)'); const varying = varyingChannels(); assert.equal(varying.length, 2, '★ 全表只有两条非常数通道:宋武举 + 西汉博士弟子(kemu 非空)'); assert.equal(totalConstant, totalChannels - varying.length, '★★★ 除那两条外,其余通道位阶全是常数函数'); // ⚠ varyingChannels 按 DYNASTIES 顺序。2026-07-30【朝代按编年排序】后:西汉(前202) 在 宋/北宋(960) 之前, // ⇒ 顺序反转为 [0]=西汉博士弟子、[1]=宋武举(旧序是 song 在 xihan 前 ⇒ 反的)。★ 内容不变,只是次序随编年排序换了。 // ★★★ 第一条(编年在前):西汉博士弟子 —— 补了经学轴就能跑,一档(通一藝),排序键无原文 ⇒ keySource=null。 assert.equal(varying[0].dynastyId, 'xihan'); assert.equal(varying[0].channelId, 'boshi_dizi'); assert.deepEqual(varying[0].range, [0, 1], '★ seat=0 + 一档 kemu(通一藝)⇒ tier 0↔1'); assert.equal(varying[0].kemuSource, null, '★★ 一档无次序 ⇒ 排序键无原文可依(keySource=null)—— 全表仍只有宋武举挂 keySource'); // 第二条(编年在后):宋武举 —— 唯一挂 keySource 的那条(两档且有次序:武艺在前、策在后,《宋史》卷157)。 assert.equal(varying[1].dynastyId, 'song'); assert.equal(varying[1].channelId, 'wuju'); assert.deepEqual(varying[1].range, [1, 3], '★ 「tier = 1 + 通过条数」—— 《宋史》卷157 熙宁武举授官那一段'); assert.ok(varying[1].kemuSource, '★★ 宋武举是唯一挂着 keySource 的那条(排序键有原文:武艺在前、策在后)'); assert.ok(varying[1].kemuSource.includes('《宋史》卷一百五十七')); // ⇒ ★ 判词:引擎在【有原文的科目】处区分,在没有的地方不区分。 // ⚠ 注:kaoyan.js 的 CONSTANT_PROOF.claim/soWhat 里写的「19/20、唯一宋武举」是加西汉【之前】的读数, // 现在已过时(本表不改 src/kaoyan.js —— 不在授权范围)。而 soWhat 那句【总判词】仍然成立: assert.ok(CONSTANT_PROOF.soWhat.includes('完全一致')); }); test('★★★★ 老板点名的那一格:ming/lisi 恒 tier 2 —— 一格都没换掉', () => { const lisi = constantTierOf('ming', 'lisi'); assert.equal(lisi.constant, true); assert.equal(lisi.tier, 2, '★★ 旧引擎 480/480 全是 2;新引擎【解析上就是常数】'); assert.ok(CONSTANT_PROOF.lisiCheck.includes('122,880'), '★ 穷举读数必须写在数据里'); // ★★★ 而字典序【换不掉它】,理由是结构性的:lexCompare 是【择路】的排序键, // 它从来不进 tier 的算式。⇒ 那个病在 tier 的算式里,而它不在那个算式里。 assert.ok(CONSTANT_PROOF.lisiCheck.includes('从来不进 tier 的算式')); // ⇒ 换掉的是【那个数的理由】,不是那个数。 assert.ok(CONSTANT_PROOF.whatDidChange.includes('同一个数,两个理由')); }); test('★★ 汉:tier 只能取两个值(0 与 3),且三条路的天花板【全是 5】—— 连杠杆都是平的', () => { const han = discriminationOf('han'); assert.deepEqual(han.tierValues, [0, 3], '★★★ 汉这一朝,引擎的位阶信息量 = 1 bit(你过没过闸)'); assert.equal(han.constantChannels, 3); assert.equal(han.varyingChannels, 0); assert.deepEqual(han.ceilings, { xiaolian: 5, bizhao: 5, jungong_han: 5 }); assert.equal(han.ceilingIsFlat, true, '★★★★ 「通道即杠杆」在汉一格都没兑现 —— 三条路一样高'); // ⇒ ★ 即:汉是七朝里史料最厚的一朝,而它恰恰是这个功能最没得可说的一朝。 assert.ok(NOT_DONE.find((x) => x.id === 1).detail.includes('最没得可说的一朝')); // ★★ 而【汉、辽、西汉是天花板全平的三朝】—— 另外五朝的通道之间至少有一处落差。 // ⚠ 2026-07-17 加西汉:西汉四条路的 ceiling 全是 5(三条盲路 ceiling=null ⇒ 引擎兜底 5; // 博士弟子 ceiling.tier=5,兒寬→御史大夫)⇒ ceilingIsFlat=true。这与「六轴瞎」一致: // 本表在西汉连「哪条路封得低」都读不出,四条路一样高。 const flat = DYNASTIES.filter((d) => discriminationOf(d.id).ceilingIsFlat).map((d) => d.id); // ⚠ 2026-07-17 拆墙:不再写死【全表恰好这三朝平】——接进来的朝有的天花板也平(如秦:两条路都封在同一格)。 // 不变量 = 【汉、辽、西汉这三朝的天花板确实全平】(这是关于这三朝的断言,其余朝不受它约束)。 for (const id of ['han', 'liao', 'xihan']) { assert.ok(flat.includes(id), `★ ${id} 的天花板必须是平的 —— 「通道即杠杆」在它这里没兑现`); } }); test('★★★ 七朝的 tier 值域 —— 只有宋给得出四个值,而那全部来自武举那一条', () => { const got = Object.fromEntries(DYNASTIES.map((d) => [d.id, discriminationOf(d.id).tierValues])); assert.deepEqual(got.han, [0, 3]); assert.deepEqual(got.weijin, [0, 3]); assert.deepEqual(got.nanchao, [0, 3, 4], '★ 舍人自己的梯子上那一格(tier4)'); assert.deepEqual(got.tang, [0, 3]); // ⚠⚠ 【订正标本】:本刀初稿这里报的是 [0,1,3] —— 【漏了 2】,因为我对非常数的通道 // 只收了 range 的两端。★ 而 2 是真的能取到的(wuju 通 1 条 → tier 2,穷举实测)。 // ⇒ 「拿两个端点冒充一个区间」—— 很小的错,但它会让「宋有几个位阶」这个读数少一格。 assert.deepEqual(got.song, [0, 1, 2, 3], '★★★ 宋是唯一给得出四个值的一朝 —— 全部来自 wuju'); assert.deepEqual(got.ming, [0, 2, 3]); assert.deepEqual(got.liao, [0, 3]); // ⚠ 2026-07-17 加西汉:三条盲路恒 tier0;博士弟子有一档 kemu(通一藝)⇒ range [0,1] ⇒ 值域 {0,1}。 // ★ 即经学轴一开,西汉从「四条路全是常数 0」变成「多了一个能取到 1 的通道」——博士弟子当场能跑。 assert.deepEqual(got.xihan, [0, 1], '★ 西汉:盲路恒 0 + 博士弟子 0↔1(通一藝)'); // ★ 而这八朝的值域宽度:宋仍是唯一的 4。 // ⚠ 2026-07-17 拆墙:不再写死【全表逐朝】的位阶值域宽度数组(接朝后数组会变长)—— // 改为逐 id 钉住【原有八朝】各自的值域宽度(这是关于这八朝的断言,新接的朝各有各的宽度,不进这条)。 const widthById = { han: 2, weijin: 2, nanchao: 3, tang: 2, song: 4, ming: 3, liao: 2, xihan: 2 }; for (const [id, w] of Object.entries(widthById)) { assert.equal(discriminationOf(id).tierValues.length, w, `★ ${id} 的位阶值域宽度应为 ${w}`); } }); test('★★★★ 穷举验证解析式 —— 汉按 2026-07-17 的 2^17 × 4 个 profile 逐个对', () => { // ⚠⚠ 断言的方向【写死】:解析式是【预测】,穷举是【验证】。 // ★ 分叉时,错的是 kaoyan.js(它复制了引擎的算术,见文件头 clamp 的注),不是引擎。 const SUBS = EXAM_SUBS_2026_07_17; const N = SUBS.length; // ⚠ 2026-07-17 加经学轴:talents.js 第 17 项 jingxue_tong(hobby 型,只落 jingxue 轴)。 // ★ 它对【汉】的六轴全无影响(汉三条闸看 jiao/wang/fa/wu,不看 jingxue)⇒ 每个旧组合分裂成 // 【含/不含 jingxue_tong】两份、汉的结果逐份相同 ⇒ 汉的 tier 值域仍是 {0,3},只是组合数翻倍。 assert.equal(N, 17, '★ 17 项闭合(加了 jingxue_tong)—— 变了就说明 talents.js 动了,那这场考试的输入变了'); const WANGS = [0, 29, 30, 100]; // 卡在 gate{wang:30} 的两侧 const seen = new Map(); // channelId -> Set let total = 0; for (let mask = 0; mask < 2 ** N; mask++) { const subs = []; for (let i = 0; i < N; i++) if (mask & (1 << i)) subs.push(SUBS[i]); const axes = computeAxes(subs); for (const w of WANGS) { const r = computeRank({ axes, wang: w }, 'han'); total++; const cid = r.channel ? r.channel.id : '(none-open)'; if (!seen.has(cid)) seen.set(cid, new Set()); seen.get(cid).add(r.rank.tier); } } assert.equal(total, 2 ** 17 * 4, '★ 穷举,不是采样(2^17 —— 加 jingxue_tong 后 17 项闭合)'); // ★★ 每一条通道,实跑出来的 tier 集合【必须只有一个值,且等于解析式预测的那个】。 for (const [cid, tiers] of seen) { if (cid === '(none-open)') { assert.deepEqual([...tiers], [0], '一条路都不开 ⇒ tier 0'); continue; } const pred = constantTierOf('han', cid); assert.equal(pred.constant, true, `${cid}:解析式说它不是常数,而汉的 kemu 全空?`); assert.deepEqual( [...tiers], [pred.tier], `★★★ ${cid}:穷举实跑得到 {${[...tiers]}},而解析式预测 ${pred.tier} —— 分叉了。` + '⇒ 错的是 kaoyan.js(它复制了引擎的算术),不是引擎。先看 dynasties.js 的 tierOf 变了没有。' ); } // ⇒ ★ 汉全朝只可能吐两个值。 const allTiers = new Set([...seen.values()].flatMap((s) => [...s])); assert.deepEqual([...allTiers].sort(), [0, 3]); }); test('★★★ 穷举验证:宋的武举是全表唯一一条真的会变的路', () => { const SUBS = SUB_REGISTRY.map((s) => s.id); const N = SUBS.length; const seen = new Map(); for (let mask = 0; mask < 2 ** N; mask++) { const subs = []; for (let i = 0; i < N; i++) if (mask & (1 << i)) subs.push(SUBS[i]); const r = computeRank({ axes: computeAxes(subs), wang: 0 }, 'song'); const cid = r.channel ? r.channel.id : '(none-open)'; if (!seen.has(cid)) seen.set(cid, new Set()); seen.get(cid).add(r.rank.tier); } const wuju = [...(seen.get('wuju') || [])].sort(); assert.deepEqual(wuju, [1, 2, 3], '★★ 三档,逐字对着《宋史》卷157:三班差使 / 三班奉職 / 右班殿直'); // ⇒ ★ 而同朝的科举正途仍然是常数。 assert.deepEqual([...(seen.get('keju_song') || [])], [3], '★ keju_song 恒 3 —— kemu 空'); // ★★★ 而这一条钉的是【解析式没有漏格】—— 本刀在这里栽过一次(宋报成 [0,1,3],漏了 2)。 // ⇒ 拿穷举实跑的值域,去对解析式预测的值域。分叉 = kaoyan.js 错了,不是引擎错了。 const exhaustive = [...new Set([...seen.values()].flatMap((s) => [...s]))].sort((a, b) => a - b); assert.deepEqual( discriminationOf('song').tierValues, exhaustive, '★★★ 解析式预测的 tier 值域,与穷举实跑的值域【必须逐格相同】。' + '⇒ 分叉时错的是 kaoyan.js(它复制了引擎的算术),不是引擎。' ); }); // ══════════════════════════════════════════════════════════════════════════ // ⑤' ★★★★ 汉的闸只看四条轴 —— 文采/沉毅/机锋在这一朝【不可观测】 // ══════════════════════════════════════════════════════════════════════════ test('★★★★ 汉的三条闸只看 jiao/wang/fa/wu —— wen、heng、tan 在汉没有任何一条闸看得见', () => { const a = axesSeenBy('han'); assert.deepEqual([...a.gatedOn].sort(), ['fa', 'jiao', 'wang', 'wu']); assert.equal(a.seesWang, true); assert.deepEqual(a.invisibleAxes.sort(), ['heng', 'tan', 'wen'], '★★★ 六轴里三条在汉是不可观测的'); assert.equal(axesSeenBy('TYPO'), null, '失效方向朝关'); }); test('★★★★ 就算给兒宽 wen=100,他还是 tier 0 —— 修好 wen 轴,这场考试一格不动', () => { // ⚠ 这里【故意直接构造 axes】—— 那不是改画像(画像还是 renwu.js 的那两项), // 是【量引擎对 wen 的敏感度】。★ 而它的答案是:零敏感。 const base = computeAxes(subsOf('nikuan')); // [warm, calm] → jiao=63, heng=63, wen=40 assert.equal(base.wen, 40, '★ 史官明说他「敏於文」,而本表给他基线 40(renwu.js FINDINGS ③)'); for (const wen of [40, 63, 73, 79, 100]) { const r = computeRank({ axes: { ...base, wen }, wang: 0 }, 'han'); assert.equal(r.rank.tier, 0, `★★★ wen=${wen} 时他还是 tier 0 —— 汉没有一条闸看 wen`); assert.equal(r.channel, null); } for (const heng of [40, 63, 79, 100]) { const r = computeRank({ axes: { ...base, heng }, wang: 0 }, 'han'); assert.equal(r.rank.tier, 0, `★ heng=${heng} 同理 —— renwu.js 在 calm 那一格早就招了这件事`); } // ⇒ ★★★★ 即:renwu.js 那个「文采轴不量写作」的诊断,在汉这一朝【是不可观测的】。 const f = FINDINGS.find((x) => x.id === 6); assert.ok(f.finding.includes('在汉这一朝是【不可观测的】')); assert.ok(f.proposal.includes('唐是唯一一朝有 wen 的闸'), '★ 要验那个诊断,得挑一个看得见那条轴的朝代'); }); test('★★★ 全表的「闸 × 轴」覆盖表 —— 没有一朝看得见超过三条轴', () => { const seesAxis = (axis) => DYNASTIES.filter((d) => axesSeenBy(d.id).gatedOn.includes(axis)).map((d) => d.id); // ⚠⚠ 【订正 —— 我的上一版断言错了,本文件的测试第二次抓我】: // 我写的是「宋是唯一看 heng 的一朝」。✗ 【明也看 heng】(keju{heng:55, fa:50})。 // ⇒ 只有 wen 与 tan 是「只有一朝看得见」的。 // ⚠ 2026-07-17 接 25 朝:不再写死【全表恰好哪几朝看某条轴】—— 接进来的朝也各有各的闸 // (五代/金看 wen,曹魏/东晋借魏晋的清谈看 tan,南宋看 heng…)。★ 改为【子集断言】: // 原有八朝里【看得见某轴的那几朝】必须仍然看得见(历史判断不变),新朝只增不减。 // 不变量【更硬的那两条】在下面 L481-484 与 L494 逐条守着:没有一朝看 >3 条六轴、每条六轴都有朝看。 for (const id of ['tang']) assert.ok(seesAxis('wen').includes(id), `★ ${id} 的闸必须看 wen`); for (const id of ['weijin']) assert.ok(seesAxis('tan').includes(id), `★ ${id} 的闸必须看 tan(清谈)`); for (const id of ['song', 'ming']) assert.ok(seesAxis('heng').includes(id), `★ ${id} 的闸必须看 heng`); for (const id of ['han', 'tang']) assert.ok(seesAxis('jiao').includes(id), `★ ${id} 的闸必须看 jiao`); for (const id of ['han', 'nanchao', 'tang', 'song', 'ming', 'liao']) assert.ok(seesAxis('fa').includes(id), `★ ${id} 的闸必须看 fa`); for (const id of ['han', 'weijin', 'nanchao', 'song', 'liao']) assert.ok(seesAxis('wu').includes(id), `★ ${id} 的闸必须看 wu`); // ★★★★ 而这张表真正的读数在这里:【没有一朝的闸看得见超过三条轴】。 const SIX = ['wen', 'fa', 'wu', 'jiao', 'heng', 'tan']; for (const d of DYNASTIES) { const seen = SIX.filter((a) => axesSeenBy(d.id).gatedOn.includes(a)); assert.ok(seen.length <= 3, `${d.id} 看得见 ${seen.length} 条轴?那这条断言该重写`); } // ⚠ 2026-07-17 加西汉:西汉唯一一道闸是 gate{jingxue:55}(博士弟子),而 jingxue 是【第七轴】, // 不在 SIX(六轴)里 ⇒ 西汉的「六轴闸」数 = 0。★ 这正是西汉的立意:它的四条尺子里, // 六轴一条都看不见,唯一看得见的是六轴之外那条【经学】。 // ⚠ 2026-07-17 拆墙:不再写死【全表逐朝】的六轴闸数数组 —— 改为逐 id 钉住原有八朝。 // ★ 不变量【仍然保留、且更严】:没有一朝的六轴闸数超过 3(上面 L481-484 逐朝守着,含新接的朝)。 const sixSeenById = { han: 3, weijin: 2, nanchao: 2, tang: 3, song: 3, ming: 2, liao: 2, xihan: 0 }; for (const [id, n] of Object.entries(sixSeenById)) { assert.equal( SIX.filter((a) => axesSeenBy(id).gatedOn.includes(a)).length, n, `★★★ ${id} 的六轴闸数应为 ${n}` ); } // ⇒ ★ 六条轴每一条至少被一朝看见(没有一条是全表死的)—— 这一条是好消息,也必须报。 for (const a of SIX) assert.ok(seesAxis(a).length >= 1, `★ ${a} 全表没有一条闸看它?`); }); // ══════════════════════════════════════════════════════════════════════════ // ⑤'' ★★★★ 空操作的第二个标本:一条走不到的通道 // ══════════════════════════════════════════════════════════════════════════ test('★★★★ 唐的行卷是一条走不到的路 —— 结构上可证(输在天花板)', () => { const dead = unreachableAll(); // ⚠⚠ 2026-07-17 接 25 朝:不再写死【全表只有一条走不到】。★ 这【不是墙塌了,是真相变多了】: // 接进来的朝里,凡是【同一道闸下有一条位子更高的同门路】的,位子低的那条就永远不会被选中 // (如金的律科 vs 经义进士,都试策/律而经义位子更高;南宋的特奏名 vs 正奏名)—— // 这与唐行卷【是同一个结构】(闸相同或更严 + 位子/天花板更低 ⇒ 一次都走不到), // 也与本表最早的纪律【同源】(「明代能过科举就别去当吏」=位阶即杠杆)。 // ⇒ 不变量【收紧到唐这一条本身】:唐·行卷【必须】仍是一条走不到的路,且【输在天花板】那一级。 const xj = dead.find((x) => x.dynastyId === 'tang' && x.channelId === 'xingjuan'); assert.ok(xj, '★ 唐·行卷必须仍是一条走不到的路'); assert.equal(xj.beatenBy, 'jinshi'); assert.equal(xj.at, 'ceiling', '★★ 它输在【天花板】那一级 —— 字典序那一步根本走不到'); assert.equal(xj.xCeiling, 4); assert.equal(xj.yCeiling, 5); assert.ok(xj.why.includes('一次都走不到')); // ★ 对照:藩镇幕府的闸【不蕴含】进士的闸 ⇒ 它可达。⇒ 死的不是「天花板低」,是「闸更严 + 天花板更低」。 assert.deepEqual(unreachableChannels('han'), [], '汉三条路全可达'); assert.deepEqual(unreachableChannels('song'), [], '宋三条路全可达'); // ⚠⚠ 【本判据在辽是「碰巧对的」,本刀自己招了 —— 这条断言把那个「碰巧」钉住】: // 辽的 wenquan_liao 与 nanmian_hanguan 的 gate 逐字相同({fa:55})⇒ 互相蕴含; // 它们【没被判成死路】,只是因为 tier(3=3)与 ceiling(5=5)正好打平。 // ⇒ ★ 若哪天有人改了其中一条的 ceiling,本判据会当场给出一个【假阳性】—— // 而真相是:那两条路根本不会同时开着,【出身把它们分开了】,本判据看不见出身。 const liao = DYNASTIES.find((d) => d.id === 'liao'); const wenquan = liao.channels.find((c) => c.id === 'wenquan_liao'); const nanmian = liao.channels.find((c) => c.id === 'nanmian_hanguan'); assert.deepEqual(wenquan.gate, nanmian.gate, '★ 两条路的闸逐字相同 —— 分开它们的是出身,不是闸'); assert.equal(wenquan.ceiling.tier, nanmian.ceiling.tier, '★★ 而它们的天花板【正好打平】—— 判据靠这个碰巧不误报'); assert.deepEqual(unreachableChannels('liao'), [], '★ 现在不误报 —— 而那是碰巧,不是守着'); }); test('★★★★ 穷举验证:唐按 2026-07-17 输入跑 1,048,576 格,行卷【一次都没被选中】', () => { const SUBS = EXAM_SUBS_2026_07_17; const N = SUBS.length; const WANGS = [0, 29, 30, 54, 55, 74, 75, 100]; const won = new Map(); for (let mask = 0; mask < 2 ** N; mask++) { const subs = []; for (let i = 0; i < N; i++) if (mask & (1 << i)) subs.push(SUBS[i]); const axes = computeAxes(subs); for (const w of WANGS) { const r = computeRank({ axes, wang: w }, 'tang'); const k = r.channel ? r.channel.id : '(none-open)'; won.set(k, (won.get(k) || 0) + 1); } } // ⚠ 2026-07-17 加经学轴(17 项闭合):唐的四条闸看 wen/jiao/fa,【都不看 jingxue】⇒ jingxue_tong // 对唐的每一个 profile 结果无影响 ⇒ 每个旧组合分裂成两份、逐份相同 ⇒ 每个落点计数【整齐翻倍】。 // 行卷仍是 0(翻倍后还是 0)。 assert.equal(won.get('xingjuan'), undefined, '★★★★ 【行卷 0 次。穷举 2^17×8 = 1,048,576 格。】'); assert.equal(won.get('jinshi'), 917504, '★ 458752×2(jingxue_tong 对唐无影响,组合数翻倍)'); assert.equal(won.get('fanzhen'), 122880, '★ 61440×2'); assert.equal(won.get('(none-open)'), 8192, '★ 4096×2'); assert.equal([...won.values()].reduce((a, b) => a + b, 0), 2 ** 17 * 8, '★ 穷举,不是采样(2^17)'); }); test('★★★ 而行卷【只在你不够格的时候才存在】—— 一够格它就从输出里消失', () => { // ① 够格(wen=79, jiao=83)⇒ 行卷的闸开着 ⇒ 而它既不是 channel,也不在 blocked ⇒ 消失。 const rich = computeAxes(['poetic', 'story', 'vivid', 'warm', 'playful', 'humor', 'signature']); assert.ok(rich.wen >= 55 && rich.jiao >= 55, '前提:这个人过得了行卷的闸'); const r = computeRank({ axes: rich, wang: 0 }, 'tang'); assert.equal(r.channel.id, 'jinshi', '★ 进士赢了'); assert.ok(!r.blocked.some((b) => b.channel === '行卷'), '★★ 闸开着 ⇒ 它不进 blocked'); assert.notEqual(r.channel.id, 'xingjuan'); // ⇒ ★★★ 作为一条【路】,它从输出里整个消失了。 // ⚠ 而「行卷」这两个字仍然出现在 reasons[0] 里 —— 因为这一朝【就叫「科举 · 行卷」】。 // ★ 那让这一格更难看,不是更好:朝代的名字里有它,而没有人走得到它。 assert.ok(r.reasons[0].includes('行卷'), '★ 朝代的名字与 systemNote 里都有它'); assert.equal(DYNASTIES.find((d) => d.id === 'tang').system, '科举 · 行卷'); // ② 不够格(subs 空)⇒ 它进 blocked ⇒ 用户【只有这时候】看得见它。 const r2 = computeRank({ axes: computeAxes([]), wang: 0 }, 'tang'); assert.ok(r2.blocked.some((b) => b.channel === '行卷'), '★★★ 不够格时它才出现'); const f = FINDINGS.find((x) => x.id === 7); assert.ok(f.measured.whatUserSees.includes('只可能在他不够格的时候看见它')); // ⚠ 而不许说「第二刀杀了它」—— 那个 112 是闸开着的 profile 数,不是走它的人数。招在数据里。 assert.ok(f.finding.includes('那两个数不是一回事'), '★ 差点做的第二次假发现,必须招在明处'); assert.equal(f.changed, false); }); // ══════════════════════════════════════════════════════════════════════════ // ⑥ ★★★ 第五次 —— 「排在后面 ≠ 不参选」这一族的镜像:【排在前面 ≠ 更接近】 // ══════════════════════════════════════════════════════════════════════════ test('★★★★ 军功【永远】不是 nearest —— 穷举 2,432 格,0 次。而它每一次都判平', () => { const SUBS = EXAM_SUBS_2026_07_17; const N = SUBS.length; const WANGS = [0, 29, 30, 54, 55, 74, 75, 100]; const dist = new Map(); let closed = 0; for (let mask = 0; mask < 2 ** N; mask++) { const subs = []; for (let i = 0; i < N; i++) if (mask & (1 << i)) subs.push(SUBS[i]); const axes = computeAxes(subs); for (const w of WANGS) { const r = computeRank({ axes, wang: w }, 'han'); if (r.channel === null) { closed++; dist.set(r.nearest, (dist.get(r.nearest) || 0) + 1); } } } // ══════════════════════════════════════════════════════════════════════════ // ★★ 2026-07-17 加经学轴:闭式解【重推】(老板要求:不许直接改成新数字了事,要写清怎么推的)。 // // 【机理不变】汉「一条路都不开」⟺ fa=40 且 wu=40 且(jiao<55 或 wang<30)—— // 即:辟召(gate{fa:55}) 关 ∧ 军功(gate{wu:55}) 关 ∧ 孝廉(gate{jiao:55,wang:30}) 关。 // 六轴取值是整数项 ⇒ fa<55⟺fa=40(无 fa-sub)、wu<55⟺wu=40(无 wu-sub)、jiao<55⟺jiao=40(无 jiao-sub)。 // // 【16 项时(加轴前)】fa∪wu∪jiao = 10 个 sub(4 fa + 2 wu + 4 jiao): // · A 档:fa=40 ∧ wu=40 ∧ jiao=40 ⇒ 孝廉恒关(与 wang 无关)⇒ 对全部 8 个 wang 都「不开」。 // 自由 sub = 16−10 = 6 ⇒ 2^6 = 64 个组合 × 8 wang = 512。 // · B 档:fa=40 ∧ wu=40 ∧ jiao≥55 ⇒ 孝廉仅在 wang<30 时关 ⇒ 只对 2 个 wang(<30) 「不开」。 // fa=40∧wu=40 的自由 sub = 16−6 = 10,其中「至少一个 jiao-sub」= 2^10 − 2^6 = 960 × 2 wang = 1,920。 // ⇒ 64×8 + 960×2 = 2,432。(这是 renwu 那条 8-wang 读数,见下面 reconciled 测试。) // // 【17 项时(加 jingxue_tong 后)】★ jingxue_tong 只落 jingxue 轴,【不在 fa/wu/jiao 里】⇒ 它是 // 一个纯粹的「自由 sub」,A/B 两档的自由 sub 各 +1 ⇒ 组合数各翻倍: // · A 档自由 sub = 17−10 = 7 ⇒ 2^7 = 128 × 8 wang = 1,024。 // · B 档「至少一个 jiao-sub」= 2^11 − 2^7 = 2048 − 128 = 1,920 × 2 wang = 3,840。 // ⇒ 128×8 + 1920×2 = 4,864 = 2 × 2,432。★ 即:加一个对汉全无影响的 sub,把每个组合分成两份, // 结果整齐翻倍——这本身就是「jingxue_tong 对汉不可观测」的又一个证明。 // ══════════════════════════════════════════════════════════════════════════ assert.equal(closed, 4864, '★ 一条路都不开的 profile 数(重推的闭式解:128×8 + 1920×2 = 4,864 = 2×2,432)'); assert.equal(128 * 8 + 1920 * 2, 4864, '★ 闭式解自洽(17 项:自由 sub 各 +1 ⇒ 64→128、960→1920)'); assert.deepEqual( [...dist.entries()].sort((a, b) => a[0].localeCompare(b[0])), // ★ 落点分布同样整齐翻倍:1344→2688、1088→2176(jingxue_tong 对汉的 nearest 判决无影响)。 [['察举孝廉', 2688], ['辟召(四科·明达法令)', 2176]] ); assert.equal(dist.get('军功'), undefined, '★★★★ 【军功 0 次。穷举。】'); // ★★ 而它是一个【恒等式】,不是一次采样: // 「一条路都不开」⟺ fa=40 且 wu=40 ⇒ 辟召的缺口 = 军功的缺口 = 15 ⇒ 逐级判平 // ⇒ 掉到 `a._idx - b._idx`,而 bizhao(_idx=1) 永远排在 jungong_han(_idx=2) 前面。 const f = FINDINGS.find((x) => x.id === 3); assert.ok(f.measured.proof.includes('恒相等')); assert.ok(f.measured.proof.includes('每一次都输')); }); test('★★★ 代价落在最不该落的两个人身上:李广、李蔡真的走军功,而引擎说他们最接近【辟召】', () => { for (const id of ['liguang', 'licai']) { const g = gradeOne(id); assert.equal(g.engine.channelId, null, `${g.name}:一条路都不开`); assert.equal(g.real.engineChannel, 'jungong_han', `${g.name}:他真的走的是军功`); assert.ok(g.engine.nearest.includes('辟召'), `★★ ${g.name}:而引擎说他最接近辟召 —— 判平之后由编号裁的`); } // ⚠⚠ 而【不许说引擎藏了这个编号】—— dynasties.js 明写着它是编号。 // ★ 可以说的只有一条:【声明在代码里,产物在用户眼前,中间那句话没跟过去。】 const f = FINDINGS.find((x) => x.id === 3); assert.ok(f.finding.includes('它没有藏'), '★ 不许拿他的谨慎当他的罪证(renwu.js FINDINGS ⑧ 的教训)'); assert.ok(f.finding.includes('产物在用户眼前')); // ★ 而这一条改了也不加分 —— 正因为如此它才值得报。 assert.ok(f.proposal.includes('一格都不动')); assert.equal(f.changed, false); }); // ══════════════════════════════════════════════════════════════════════════ // ⑦ ★★★ 报警器 —— 与 renwu.js 那张钉死的表逐格对。引擎一变,这里就响。 // ══════════════════════════════════════════════════════════════════════════ test('★★★★ 实时成绩单 vs renwu.js 的钉死快照 —— 逐格对。分叉 = 引擎变了', () => { const live = gradeAll(); assert.equal(live.length, RENWU_GRADE.rows.length, '人数对不上'); for (const row of RENWU_GRADE.rows) { const g = live.find((x) => x.id === row.id); assert.ok(g, `renwu.GRADE 有 ${row.id},实时成绩单里没有`); const hint = `★★★ ${row.name}:renwu.js 的钉死快照(2026-07-17)与实时结果分叉了。\n` + ' ⇒ 【这不是本表过时了,是引擎变了 —— 那正是这张表该报警的时刻】(renwu.js WHY_NO_IMPORT 逐字)。\n' + ' ✗ 【不许改 renwu.GRADE 去迁就实时结果】,也不许改这条断言。先去看 dynasties.js 变了什么。'; assert.deepEqual(g.engine.subs, row.subs, hint); assert.equal(g.engine.tier, row.engineTier, hint); assert.equal(g.engine.title, row.engineTitle, hint); assert.equal(g.real.tier, row.realTier, hint); } }); test('★ renwu.js 的穷举读数:本刀独立复算,逐个对上(含它没解释的那个 2,240)', () => { assert.equal(RENWU_GRADE.exhaustive.result.tier0, 2240); assert.equal(RENWU_GRADE.exhaustive.result.tier3, 325440); assert.equal(RENWU_GRADE.exhaustive.result.total, 327680); // ★★ 闭式解:tier0 = 64 × (wang 值个数) + 960 × (wang < 30 的个数) // ⚠ renwu.js 的 exhaustive 是【16 项闭合、5 个 wang】时采的【冻结快照】—— 它不随 talents.js 变, // 所以下面这两条仍用 16 项的自由 sub 数(64、960)、2^16,一个字不动。 // renwu 用 5 个 wang(0/29/30/55/100),其中 2 个 < 30 ⇒ 64×5 + 960×2 = 2,240 ✓ assert.equal(64 * 5 + 960 * 2, RENWU_GRADE.exhaustive.result.tier0); assert.equal(65536 * 5 - 2240, RENWU_GRADE.exhaustive.result.tier3); // ⇒ 【16 项、8 个 wang】的对照量:64×8 + 960×2 = 2,432(renwu 的 FINDINGS ⑤ reconciled 里记的那个数)。 assert.equal(64 * 8 + 960 * 2, 2432); // ★★★ 2026-07-17 加 jingxue_tong(第 17 项,对汉不可观测):【实跑现在是 17 项】⇒ 自由 sub 各 +1 // ⇒ 64→128、960→1920 ⇒ 8-wang 的一条路都不开 = 128×8 + 1920×2 = 4,864 = 2×2,432 // (上面那条穷举测试实跑验证的就是这个 4,864)。⇒ 冻结快照(2,432, 16 项) 与 实跑(4,864, 17 项) // 相差正好一个因子 2——那个因子就是「加了一个对汉全无影响的 sub」。 assert.equal(128 * 8 + 1920 * 2, 2432 * 2, '★ 17 项实跑 = 16 项快照 × 2(新增 sub 对汉不可观测)'); const f = FINDINGS.find((x) => x.id === 5); assert.ok(f.measured.reconciled.includes('2,240')); assert.ok(f.measured.reconciled.includes('2,432')); }); test('★★ 十人全是西汉 —— 这场考试的题目错配,是数据里的事实,不是一句话', () => { for (const r of RENWU) assert.equal(r.era, 'xihan', `${r.name} 不是西汉人?那 XIHAN_ONLY 的断言该先响`); const han = DYNASTIES.find((d) => d.id === 'han'); assert.equal(han.era, '东汉'); assert.equal(han.span, '25–220'); // ⇒ ★ 引擎的汉 = 东汉;十人 = 西汉。这两个字段就是「这场考试不合法」的全部证据。 }); // ══════════════════════════════════════════════════════════════════════════ // ⑧ ★ 红线 —— 这一刀不许碰的东西,用断言钉住 // ══════════════════════════════════════════════════════════════════════════ test('★★★ 红线:本刀不许改画像 —— subs 必须全部来自 renwu.js 的 mapping,一个都不许多', () => { for (const r of RENWU) { const fromMapping = r.mapping.map((m) => m.sub); assert.deepEqual(subsOf(r.id), fromMapping, `${r.name}:subs 与 mapping 对不上`); assert.deepEqual(gradeOne(r.id).engine.subs, fromMapping, `★ ${r.name}:成绩单偷偷改了画像`); // ★ 每一项映射都必须挂着【原文的字】与【它勉强在哪】—— 老板:「你的映射就是你的论证」。 for (const m of r.mapping) { assert.ok(m.from.length > 0, `${r.name}/${m.sub} 没说它接的是原文哪几个字`); assert.ok(m.strain.length > 0, `${r.name}/${m.sub} 没招它勉强在哪`); } } }); test('★★ 红线:六轴的分由 dynasties.js 自己算 —— 本刀碰不到那个数', () => { // ★ 结构上的保证:kaoyan 只递 subs,六轴由 computeAxes 算。这里把它钉死。 for (const r of RENWU) { const mine = gradeOne(r.id); const theirs = computeRank(profileFromSubs(subsOf(r.id), { wang: 0 }), 'han'); assert.equal(mine.engine.tier, theirs.rank.tier, `${r.name}:成绩单与引擎不一致`); assert.equal(mine.engine.title, theirs.rank.title); } }); test('★★★ 本刀自己写错的四处,必须招在数据里 —— 而且四处全是被断言抓出来的', () => { assert.ok(REPORT.myOwnErrors.includes('兒寬的 subs 是 [warm, calm]')); assert.ok(REPORT.myOwnErrors.includes('明也有')); assert.ok(REPORT.myOwnErrors.includes('名义窗宽')); assert.ok(REPORT.myOwnErrors.includes('漏了 2')); assert.ok( REPORT.myOwnErrors.includes('一个人对着自己的输出,是看不出自己错在哪的'), '★ 这一刀的方法论就在这一句上' ); // ★ 而这一刀【新】撞出来的四条,也必须在数据里 —— 不是注释里。 assert.ok(REPORT.newThisCut.includes('19 / 20')); assert.ok(REPORT.newThisCut.includes('没换掉,而且换不掉')); assert.ok(REPORT.newThisCut.includes('第二个已经在那里了')); assert.ok(REPORT.newThisCut.includes('排在前面 ≠ 更接近')); }); test('★ FINDINGS / NOT_DONE 的体例:一条都不许自作主张', () => { assert.ok(FINDINGS.length >= 5); for (const f of FINDINGS) { assert.equal(f.changed, false, `★ FINDINGS #${f.id} 标了 changed=true —— 这一刀一个字都不许改引擎`); assert.ok(f.title.length > 0); assert.ok(f.finding.length > 0); assert.ok(f.proposal.length > 0, `FINDINGS #${f.id} 没写 proposal`); assert.ok(f.measured || f.where, `FINDINGS #${f.id} 既没有读数也没有位置 —— 那它是个意见`); } assert.ok(NOT_DONE.length >= 4); for (const n of NOT_DONE) assert.ok(n.detail.length > 0); }); // ══════════════════════════════════════════════════════════════════════════ // ⑨ 残差 = 天 —— ★ 2026-07-17 第三刀 // // ★★ 本节断言的【方向】,与本文件其余部分一致,先说死: // 它【不断言引擎算对了】。它断言的是【残差没有说谎】,具体是三件事: // ① 残差是【纯函数、可复算】; // ② ★【正残差与负残差【绝不许混成一个「误差」】】—— 这一条是老板的红线, // 而本节把它钉成【结构】:本文件【不许】导出任何一个合并的量; // ③ ★★【「天 = 0 格」这个难看的数,必须【钉住】】—— // 谁把它改绿了,谁就是在改引擎(红线第一条)。 // ══════════════════════════════════════════════════════════════════════════ import { RESIDUAL_KINDS, RESIDUAL_NOT_ONE_NUMBER, RESIDUAL_SHAPE, TIAN_OUT_OF_RANGE, LIGUANG_YUANWEN, residualOf, residualAll, residualBoard, residualLines, } from '../src/kaoyan.js'; import * as KAOYAN_NS from '../src/kaoyan.js'; test('★ residual 是纯函数:同一个 id 跑两次,逐字段完全相同', () => { for (const r of RENWU) { const a = residualOf(r.id); const b = residualOf(r.id); assert.deepEqual(a, b, `${r.name}:residual 不是纯函数 —— 跑两次结果不一样`); } assert.deepEqual(residualAll(), residualAll()); }); test('★ residual 可复算:residual === engineTier − realTier,逐格由 gradeAll 独立验算', () => { const rows = gradeAll(); for (const g of rows) { const r = residualOf(g.id); if (g.real.tier === null) { assert.equal(r.residual, null, `${g.name}:答案不在尺子上,residual 必须是 null,✗ 不许是 0`); assert.equal(r.kind, 'unmeasurable'); } else { assert.equal(r.residual, g.engine.tier - g.real.tier, `${g.name}:residual 与引擎/史料对不上`); } } }); test('★ 失效方向朝【关】:查不到的人 → null,不许猜', () => { assert.equal(residualOf('不存在的人'), null); assert.equal(residualOf(''), null); assert.equal(residualOf(null), null); }); test('★★★ 量不了 ≠ 算对了:realTier=null ⇒ residual 必须是 null,且【不许参与任何统计】', () => { const board = residualBoard(); // 董仲舒(诸侯王相)、程不识(比千石)—— 梯子上没有他们站的地方 assert.equal(board.counts.unmeasurable, 2); assert.deepEqual(board.unmeasurable.map((r) => r.id).sort(), ['chengbushi', 'dongzhongshu']); for (const r of board.unmeasurable) { assert.equal(r.residual, null, '★ null 不许被写成 0 —— 那是把「我们没有这一格」冒充「引擎算对了」'); } // ★ 三堆之和 + 量不了 = 全部。⇒ 量不了的那两格【没有被偷偷算进任何一堆】。 const { tian, quebian, exact, unmeasurable, total } = board.counts; assert.equal(tian + quebian + exact + unmeasurable, total); for (const v of board.residualValues) assert.ok(Number.isInteger(v), 'residualValues 混进了非整数'); }); test('★★★★ 【天与缺变量绝不许混】:正/负是两堆,各是各的数组,符号就是意义', () => { const board = residualBoard(); for (const r of board.tian) { assert.ok(r.residual > 0, '★ tian 堆里混进了非正残差 —— 天的定义是「算高了」'); assert.equal(r.kind, 'tian'); } for (const r of board.quebian) { assert.ok(r.residual < 0, '★ quebian 堆里混进了非负残差'); assert.equal(r.kind, 'quebian'); } for (const r of board.exact) assert.equal(r.residual, 0); // ★★ 两堆【不相交】—— 没有一个人同时在两堆里 const ids = new Set(board.tian.map((r) => r.id)); for (const r of board.quebian) assert.ok(!ids.has(r.id), '★ 同一个人同时在天与缺变量两堆里'); // ★★★ 【天不许有「缺了什么变量」这一栏】—— 给天写一个「缺了什么」,就是把天解释掉 for (const r of board.tian) { assert.equal(r.whichVariable, '', '★★ 给「天」写了一个「缺什么变量」—— 那就是把残差解释掉了'); } }); test('★★★★ 反作弊(结构,不是自律):本文件【不许】导出任何一个合并的「误差」量', () => { // ★ 老板逐字:「两者绝不许混成一个「误差」…混了就什么都没说。」 // ⇒ 本条把它钉成结构:扫【导出的名字】与【board 的字段名】,一个合并量都不许有。 const FORBIDDEN = /mae|meanerror|meanabs|abserror|avgerror|averageerror|误差|绝对值|平均误差/i; for (const name of Object.keys(KAOYAN_NS)) { assert.ok(!FORBIDDEN.test(name), `★★ 导出了一个合并的误差量:${name} —— 那会让天与我们的错长得一模一样`); } const board = residualBoard(); for (const k of Object.keys(board)) { assert.ok(!FORBIDDEN.test(k), `★★ residualBoard 里有一个合并量字段:${k}`); } for (const k of Object.keys(board.counts)) { assert.ok(!FORBIDDEN.test(k), `★★ counts 里有一个合并量字段:${k}`); } assert.ok(RESIDUAL_NOT_ONE_NUMBER.claim.includes('不提供')); assert.ok(RESIDUAL_NOT_ONE_NUMBER.why.includes('混了就什么都没说')); }); test('★★★ 反作弊:不许报百分比 —— 十个人的百分比是没有意义的', () => { // ★ 老板逐字:「⚠ 不许报"准确率 75%"这种数 —— 十个人的百分比是没有意义的,报出来只会骗人。」 const board = residualBoard(); const json = JSON.stringify(board); assert.ok(!json.includes('%'), '★★ residualBoard 报了一个百分号 —— 十个人的百分比只会骗人'); for (const line of residualLines()) { assert.ok(!line.includes('%'), `★ residualLines 报了百分比:${line}`); } assert.ok(RESIDUAL_NOT_ONE_NUMBER.alsoNoPercent.includes('报个案,报形状')); }); test('★★★ residualLines:符号必须印出来 —— 不带符号的残差就是那个被禁止的「误差」', () => { const lines = residualLines(); assert.equal(lines.length, RENWU.length); for (const l of lines) { assert.ok(/[+−]\d/.test(l) || l.includes('量不了'), `★ 这一行的残差没有符号:${l}`); } // ★ 李广那一行【必须】写着「缺变量(不是天)」—— 这是这一刀的判词 const lg = lines.find((l) => l.startsWith('李广')); assert.ok(lg.includes('−5'), '★★ 李广的残差不是 −5 了 —— 引擎变了,去看它为什么变'); assert.ok(lg.includes('缺变量(不是天)'), '★★★ 李广那一格【不是天】—— 谁把它标成天,谁就是在解释残差'); }); // ── ★★★★ 这一刀的头号读数:天 = 0 格,而且它【测不到】 ────────────────── test('★★★★ 【天 = 0 格】—— 钉死。谁把它改绿了,谁就是在改引擎', () => { const board = residualBoard(); assert.equal(board.counts.tian, 0, '★★★ 出现了一个正残差 —— 那要么是引擎变了,要么是答案表变了。先去看,✗ 不许改这条断言'); assert.equal(board.counts.exact, 0, '★★ 算对了 0 格 —— 这是难看的那个数,它是钉子'); assert.equal(board.counts.quebian, 8); assert.ok(board.tianNote.includes('测不到')); }); test('★★★★ 【正残差的定义域是空的】—— max(引擎在汉)=3 < min(答案)=5,这是算术不是运气', () => { const rows = gradeAll(); const gradeable = rows.filter((r) => r.real.tier !== null); const maxEngine = Math.max(...rows.map((r) => r.engine.tier)); assert.equal(maxEngine, 3, '★ 引擎在汉的上界不再是 3 —— 引擎变了'); const minReal = Math.min(...gradeable.map((r) => r.real.tier)); assert.equal(minReal, 5, '★ 答案表的下界不再是 5 —— 答案表变了'); assert.ok(maxEngine < minReal, '★★★ 两个值域【不相交】—— 这就是「天测不到」的全部证明'); for (const r of residualAll()) { if (r.residual !== null) assert.ok(r.residual <= -2, `${r.name}:residual > −2,与值域证明矛盾`); } assert.deepEqual(residualBoard().residualValues, [-5, -2], '★ 残差的值域只有两个值'); assert.ok(TIAN_OUT_OF_RANGE.claim.includes('值域不相交')); // ★★ 而这一条与幸存者偏差是【两条独立的理由】——数据里必须说明白 assert.ok(TIAN_OUT_OF_RANGE.vsSurvivorship.includes('两条独立的理由')); assert.ok(TIAN_OUT_OF_RANGE.soWhat.includes('为了测出「天」而发明「人」')); }); test('★★★★ 穷举验证:汉的引擎【永远】说不出 4 或 5 ⇒ 它没有资格产生一个正残差', () => { // ★ 这一条是 TIAN_OUT_OF_RANGE 的前提①,而它【不许只靠这十格】—— // 十格是个样本,而这里要的是【值域】。⇒ 穷举。 // // ⚠⚠ 【本刀在这一行栽了一跤,标本留在这里,因为它正是这一族的错】: // 我写的是 `Object.keys(SUB_REGISTRY)` —— ✗ 而 SUB_REGISTRY 是【数组】, // Object.keys 给的是 ["0","1",…"15"](下标),不是 subs 的 id。 // ⇒ 于是【一个 sub 都没点上】⇒ 六轴全 40 ⇒ 全部 tier 0 ⇒ 穷举结果是 [0]。 // ★★ 而它【差一点通不过我自己那条断言,然后我可能会去改断言】—— // 即:一个【采样错了】的穷举,长得和一个【引擎变了】的穷举一模一样。 // ⇒ ★ 立一条口径:【穷举的 subs 全集,必须取【引擎自己读的那一份】(AXES.subs), // 不许从别处拿一份「看起来对」的。】而两份必须相等 —— 下面那条断言就是钉它的。 const SUBS = [...new Set(AXES.flatMap((a) => a.subs))]; // ★ 而 talents.js 那份与 dynasties.js 那份【必须是同一份当前注册表】—— // ⇒ 把完整集合钉成断言,防止六轴漏读新增副天赋。 assert.deepEqual( SUBS.slice().sort(), SUB_REGISTRY.map((x) => x.id).sort(), '★★ AXES 读的副天赋与 talents.js 的 SUB_REGISTRY 对不上 —— 六轴的输入漏了或多了' ); assert.equal(SUBS.length, 20, '★ 17 项基线 + 3 项擅长深化,AXES.subs 与 SUB_REGISTRY 仍逐一对齐'); const seen = new Set(); const WANGS = [0, 29, 30, 54, 55, 74, 75, 100]; // 结构证明已经钉死汉各通道 tier 恒为 3;这里覆盖空集、全集、单项和缺一项边界, // 避免词表扩容后继续做 2^N 的重复穷举。 const profiles = [[], SUBS, ...SUBS.map((id) => [id]), ...SUBS.map((id) => SUBS.filter((x) => x !== id))]; for (const subs of profiles) { for (const w of WANGS) seen.add(computeRank(profileFromSubs(subs, { wang: w }), 'han').rank.tier); } assert.deepEqual([...seen].sort((a, b) => a - b), [0, 3], '★★★ 汉的 tier 值域变了 —— 引擎变了'); assert.equal(Math.max(...seen), 3, '★★★★ 一个最高只会说「县令」的引擎,永远说不出「他该封侯」'); }); test('★★ 残差的形状:两个值,而它们量的是【我们的词汇表】,不是历史', () => { const board = residualBoard(); // −2 = 引擎认出了他的画像 ⇒ 过闸 ⇒ tier 3;−5 = 没认出 ⇒ 一条路都不开 ⇒ tier 0 for (const r of board.quebian) { if (r.residual === -2) assert.equal(r.engineTier, 3); if (r.residual === -5) assert.equal(r.engineTier, 0); } const minus5 = board.quebian.filter((r) => r.residual === -5).map((r) => r.id).sort(); assert.deepEqual(minus5, ['licai', 'liguang', 'nikuan', 'zhangtang']); assert.ok(RESIDUAL_SHAPE.answer.includes('没有一类算得准')); assert.ok(RESIDUAL_SHAPE.theTwoValues.includes('我们自己的词汇表')); // ★ 而 RESIDUAL_SHAPE 必须自己招出兒宽那一格的订正(subs 非空,但汉一项都看不见) assert.ok(RESIDUAL_SHAPE.theTwoValues.includes('warm, calm')); assert.ok(RESIDUAL_SHAPE.andTian.includes('两个常数相减')); }); // ── ★★★★ 李广那一格的原文 —— curl 双证,而它推翻了老板给的三个前提 ──────── test('★★★★ 「广不为后人…何也?」是【李广自己】的话,不是太史公的', () => { const y = LIGUANG_YUANWEN; assert.ok(y.liGuangAsks.quote.includes('廣嘗與望氣王朔燕語'), '★ 说话人必须留在引文里'); assert.ok(y.liGuangAsks.quote.includes('無尺寸之功以得封邑者,何也')); assert.ok(y.liGuangAsks.speaker.includes('不是太史公')); // ★★ 太史公曰整段里【没有】这个问题 assert.ok(!y.taiShiGongYue.quote.includes('何也'), '★★★ 太史公曰里出现了「何也」—— 回原文复核'); assert.ok(!y.taiShiGongYue.quote.includes('尺寸之功')); assert.ok(y.taiShiGongYue.quote.includes('桃李不言,下自成蹊')); assert.ok(y.taiShiGongYue.caveat.includes('他在【辩护】,不在【问】')); // ★★★ 而论赞里那句「口不能道辭」是 renwu.js 头号发现的第三个证人 assert.ok(y.taiShiGongYue.quote.includes('口不能道辭')); assert.ok(y.taiShiGongYue.caveat.includes('不进映射') || y.taiShiGongYue.caveat.includes('论赞不进 MAPPING')); }); test('★★★★ 「李廣難封」不出《史記》—— 而【《史記》有其事,无其辞】,两句都必须在', () => { const nf = LIGUANG_YUANWEN.nanFeng; assert.ok(nf.claim.includes('零命中')); assert.ok(nf.zeroHit.includes('逐字符'), '★ 铁律:凡零命中必须逐字符复核'); assert.ok(nf.zeroHit.includes('難經'), '★★ ctext 那两次命中是导航菜单里的书名 —— 必须交代,否则「零命中」是假的'); assert.ok(nf.realSource.includes('王勃')); assert.ok(nf.realQuote.includes('馮唐易老,李廣難封')); assert.equal(nf.realWitnesses.length, 2, '★ 一个出处的断言必须双证'); assert.ok(nf.variants.includes('大運不窮'), '★ 异文必须标,不许暗合'); // ★★★ 而「另一半」必须在:事实在原文里,只有措辞是唐人的 assert.ok(nf.caveat.includes('然廣不得爵邑,官不過九卿'), '★★★ 少了这一句,这就是一次【假订正】'); assert.ok(nf.caveat.includes('有其事,无其辞')); }); test('★★★★ 原文自己把残差解释掉了三次 —— 而武帝那次,把「天」当成了变量', () => { const y = LIGUANG_YUANWEN; // ① 李广自己:相与命 assert.ok(y.liGuangAsks.quote.includes('豈吾相不當侯邪?且固命也')); assert.ok(y.liGuangAsks.caveat.includes('李广本人就没做到')); // ② 王朔:报应 assert.ok(y.wangShuoAnswers.quote.includes('禍莫大於殺已降')); assert.ok(y.wangShuoAnswers.caveat.includes('两千年前')); // ③ ★★ 武帝:數奇,而它被当成一个【变量】用了 assert.ok(y.shuQi.quote.includes('陰受上誡')); assert.ok(y.shuQi.quote.includes('數奇')); assert.ok(y.shuQi.caveat.includes('它成了那个人下场的原因')); // ★★★ 两条限定语必须在 —— 缺一条这就是过度解读 assert.ok(y.shuQi.caveat.includes('是【本表串的】,不是原文说的')); assert.ok(y.shuQi.quote.includes('公孫敖'), '★★ 原文自己给的竞争解释(卫青给公孙敖让路)必须留在引文里'); assert.ok(y.shuQi.caveat.includes('两个都摆,不裁')); }); test('★ 李广那一格的原文:双证,且见证必须列出来', () => { const y = LIGUANG_YUANWEN; assert.equal(y.witnesses.length, 2); assert.ok(y.verified.includes('curl')); for (const k of ['liGuangAsks', 'wangShuoAnswers', 'shuQi', 'taiShiGongYue']) { assert.ok(y[k].quote.length > 0, `${k} 没有引文`); assert.ok(y[k].source.includes('史記'), `${k} 没有卷次`); assert.ok(y[k].caveat.length > 0, `${k} 没有限定语 —— 注的价值全在限定语上`); } }); test('★★★ FINDINGS ⓪/⑴/⑵:这一刀的三条判词必须在数据里,不是注释里', () => { const byId = Object.fromEntries(FINDINGS.map((f) => [f.id, f])); // ⓪ 李广不是最大的正残差,是最大的负残差 assert.ok(byId[0].title.includes('不是最大的正残差')); assert.ok(byId[0].finding.includes('它们连符号都相反')); assert.equal(byId[0].changed, false); // ⑴ 老板记的三处全错 assert.ok(byId[-1].title.includes('三处全错')); assert.ok(byId[-1].finding.includes('同一个坑的第四次')); assert.ok(byId[-1].finding.includes('假订正'), '★ 订正必须连着说「另一半」,否则它就是假订正'); // ⑵ 原文解释掉了三次 assert.ok(byId[-2].title.includes('解释掉了三次')); assert.ok(byId[-2].measured.shuqiIsNote.includes('A 有而 B 无'), '★ 见证学的判别器:ctext 只收正文'); }); test('★★★ RESIDUAL_KINDS:四档,各有各的「谁错了」—— 而天那一档必须写着「没有人错」', () => { assert.equal(Object.keys(RESIDUAL_KINDS).length, 4); assert.ok(RESIDUAL_KINDS.tian.whoIsWrong.includes('没有人错'), '★★ 天是世界的噪声,不是谁的错'); assert.ok(RESIDUAL_KINDS.quebian.whoIsWrong.includes('我们错了'), '★★ 负残差是我们的无知'); assert.ok(RESIDUAL_KINDS.unmeasurable.whoIsWrong.includes('尺子不够')); assert.equal(RESIDUAL_KINDS.tian.sign, '+'); assert.equal(RESIDUAL_KINDS.quebian.sign, '−'); assert.equal(RESIDUAL_KINDS.unmeasurable.sign, null, '★ 量不了没有符号 —— 它不是一个数'); assert.ok(RESIDUAL_KINDS.tian.means.includes('不许解释它')); }); test('★★★ NOT_DONE ⑤:「天 = 残差」的定义是对的,但不可用 —— 而 (b) 是制造天,不是测量天', () => { const n5 = NOT_DONE.find((x) => x.id === 5); assert.ok(n5, 'NOT_DONE ⑤ 没了'); assert.ok(n5.detail.includes('本刀认这个定义'), '★ 不许把老板的定义说成错的 —— 它是对的'); assert.ok(n5.detail.includes('那不是测量天,那是制造天')); assert.ok(n5.detail.includes('「人」这一半还没做完')); });