Files
digital-me-history/digital-me-backend/test/kaoyan.test.js
T
admin 0593ff7e62
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s
数字我 Digital Me:全量入库(大文件走 Git LFS)
2026-10-10 16:24:43 +08:00

1082 lines
76 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 开考 —— 成绩单的断言。
//
// ★★★ 这个文件的立场,先说死,因为它决定了每一条断言的【方向】:
//
// 本文件【不断言引擎是对的】。它断言的是【成绩单没有说谎】。
// ⇒ 所以下面凡是「引擎答了什么」的断言,钉的都是【难看的那个数】:
// 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<tier>
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('「人」这一半还没做完'));
});