1082 lines
76 KiB
JavaScript
1082 lines
76 KiB
JavaScript
// 开考 —— 成绩单的断言。
|
||
//
|
||
// ★★★ 这个文件的立场,先说死,因为它决定了每一条断言的【方向】:
|
||
//
|
||
// 本文件【不断言引擎是对的】。它断言的是【成绩单没有说谎】。
|
||
// ⇒ 所以下面凡是「引擎答了什么」的断言,钉的都是【难看的那个数】:
|
||
// 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('「人」这一半还没做完'));
|
||
});
|