数字我 Digital Me:全量入库(大文件走 Git LFS)
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s

This commit is contained in:
2026-10-10 16:24:43 +08:00
commit 0593ff7e62
1893 changed files with 361723 additions and 0 deletions
+618
View File
@@ -0,0 +1,618 @@
// 汉·公卿年表的单测 —— 这不是「覆盖率」测试,这是【史学口径的可执行定义】。
//
// 这张表的用户是全世界最难骗的那批人:他们会去翻《汉书》。
// 所以下面每一组断言,守的都不是「代码跑不跑得起来」,是「我们有没有说谎」:
// · 引文里混进一个字的顏師古注 → 挂
// · 拿「五年遷」算出一个去职年份 → 挂
// · 给一个只剩一个字的人名安上姓 → 挂
// · 两个见证打架却硬给一个结论 → 挂
// · 把「东市」记到本卷账上 → 挂
//
// 跑:node --test test/gongqing.test.js
import test from 'node:test';
import assert from 'node:assert/strict';
import {
RULERS, GONGQING, COVERAGE, SEATS, EVENTS, BIAO,
CALENDAR_NOTE, SHIGU_NOTE, WITNESS_NOTE, PERSON_NOTE,
eraAt, yearOfEra, gregorianYearOf, courtAt, fateOf,
assertGongqingInvariants,
} from '../src/gongqing.js';
// ── 0. 不变量:数据不许违反自己的规矩 ──
test('assertGongqingInvariants 必须全绿 —— 规矩写在注释里没用,写成断言才算数', () => {
const bad = assertGongqingInvariants();
assert.deepEqual(bad, [], '不变量被破坏:\n' + bad.join('\n'));
});
// ── 1. 纯函数性:这是信誉的基础 ──
test('确定性:courtAt / fateOf 跑两次必须完全相同', () => {
for (const y of [-156, -154, -140, -119, -87]) {
assert.deepEqual(courtAt(y), courtAt(y), `courtAt(${y}) 两次不一致`);
}
for (const n of ['朝錯', '田蚡', '公孫弘', '張湯']) {
assert.deepEqual(fateOf(n), fateOf(n), `fateOf(${n}) 两次不一致`);
}
});
test('无输入 / 坏输入不许瞎编', () => {
assert.equal(eraAt(-9999), null);
assert.equal(eraAt('景帝三年'), null);
assert.equal(courtAt(-9999), null);
assert.equal(yearOfEra('乾隆', 3), null);
assert.equal(yearOfEra('孝景', 99), null, '景帝没有九十九年,不许算出来');
assert.equal(fateOf(''), null);
});
// ══════════════════════════════════════════════════════════════════════════
// 2. 纪年表 —— 「这正是我们缺的那张表」
// ══════════════════════════════════════════════════════════════════════════
test('纪年 → 公历:景帝三年 = 前154', () => {
assert.equal(yearOfEra('孝景', 3), -154);
const e = eraAt(-154);
assert.equal(e.label, '孝景三年');
assert.equal(e.rulerName, '汉景帝');
assert.equal(e.n, 3);
});
test('纪年 → 公历:武帝各年号首尾(与 ctext 该表自带的「公元前」栏交叉核对过)', () => {
assert.equal(yearOfEra('建元', 1), -140);
assert.equal(yearOfEra('元光', 1), -134);
assert.equal(yearOfEra('元朔', 1), -128);
assert.equal(yearOfEra('元狩', 1), -122);
assert.equal(yearOfEra('元鼎', 1), -116);
assert.equal(yearOfEra('元封', 1), -110);
assert.equal(yearOfEra('太初', 1), -104);
assert.equal(yearOfEra('天漢', 1), -100);
assert.equal(yearOfEra('太始', 1), -96);
assert.equal(yearOfEra('征和', 1), -92);
assert.equal(yearOfEra('後元', 1), -88);
assert.equal(yearOfEra('後元', 2), -87, '武帝崩于後元二年');
});
test('纪年表必须首尾相接、不重不漏 —— 中间漏一年,后面全错一格', () => {
// 往返:每一年换算回去必须还是同一年
for (let y = -179; y <= -87; y++) {
const e = eraAt(y);
assert.ok(e, `前${-y} 年没有纪年 —— 表断了`);
assert.equal(yearOfEra(e.eraKey, e.n), y, `前${-y} 往返不一致`);
}
});
test('★ 表原文【没有「前元」】—— 「景帝前元三年」是后人的叫法,不许复活', () => {
const jing = RULERS.find((r) => r.id === 'jing');
const first = jing.eras[0];
assert.equal(first.name, null, '景帝头一段【无年号】');
assert.equal(first.raw, '孝景元年');
assert.ok(/不许复活/.test(first.nameNote));
// ⚠ 只扫【数据字段】,不扫注 —— 「不许复活「前元」」这句注里必然带「前元」二字。
// 禁烟的牌子上必须写着「吸烟」,禁令的文本不是违禁品(与 decisions.js 的 SERIAL_NOTE 同理)。
const dataFields = RULERS.flatMap((r) => [r.name, r.tableName, ...r.eras.flatMap((e) => [e.key, e.raw, e.name])]);
assert.ok(!dataFields.filter(Boolean).some((s) => s.includes('前元')), 'RULERS 的数据字段里冒出了「前元」');
assert.equal(eraAt(-154).label, '孝景三年', '晁错死在孝景三年,不是「景帝前元三年」');
});
// ══════════════════════════════════════════════════════════════════════════
// 3. ★ 十月岁首 —— 「汉纪年与公历一一对应」不许复活
// ══════════════════════════════════════════════════════════════════════════
test('★ 太初改历前,十月/十一月/十二月的事,实际公历年要减一', () => {
// 建元二年「十月丞相嬰免」:year 是 -139,可那个十月实际在前 140 年
const e = GONGQING.find((x) => x.era === '孝武建元二年' && x.event === '免' && x.person === '竇嬰');
assert.ok(e, '找不到窦婴免相那一笔');
assert.equal(e.year, -139);
assert.equal(e.month, '十月');
const g = gregorianYearOf(e);
assert.equal(g.year, -140, '★ 岁首在十月:窦婴免相实际在前 140 年,不是前 139 年');
assert.equal(g.adjusted, true);
});
test('★ 同一格里十月排在三月前头 —— 这就是岁首在十月的活证据', () => {
const rows = GONGQING.filter((x) => x.era === '孝武建元二年' && x.seat === 'chengxiang');
const shiyue = rows.find((r) => r.month === '十月');
const sanyue = rows.find((r) => r.month === '三月');
assert.ok(shiyue && sanyue);
assert.ok(
GONGQING.indexOf(shiyue) < GONGQING.indexOf(sanyue),
'十月必须排在三月前 —— 表就是这么写的,因为年从十月起'
);
});
test('太初改历(前104)之后,十月不再修正', () => {
const after = { year: -103, month: '十月', era: '太初二年' };
assert.equal(gregorianYearOf(after).adjusted, false, '太初已改历,十月不再是岁首');
const before = { year: -105, month: '十月', era: '元封六年' };
assert.equal(gregorianYearOf(before).adjusted, true);
assert.equal(CALENDAR_NOTE.taichuYear, -104);
});
test('正月—九月的条目一律不修正', () => {
for (const m of ['正月', '二月', '三月', '五月', '六月', '九月', null]) {
assert.equal(gregorianYearOf({ year: -154, month: m }).adjusted, false, `${m} 不该被修正`);
}
});
test('★ year 的正负口径必须写死:-154 = 公元前154,不是天文学年号', () => {
assert.ok(/不是.*天文学年号/.test(CALENDAR_NOTE.yearSign.replace(/【|】/g, '')));
});
// ══════════════════════════════════════════════════════════════════════════
// 4. ★★ 样板年:孝景三年(前154)—— 「同僚图谱是决策点的生成器」
// ══════════════════════════════════════════════════════════════════════════
test('★★ courtAt(-154):那一年,丞相是陶青、太尉是周亚夫、御史大夫是晁错', () => {
const c = courtAt(-154);
assert.equal(c.era.label, '孝景三年');
assert.equal(c.ruler.name, '汉景帝');
// 丞相陶青 —— 他是【景帝二年】拜的,那一年他没动,所以景帝三年那一行里【没有他】。
// 这一条就是「记事链不是花名册」的证明:查表查不到,只有重放才拿得到。
assert.equal(c.chengxiang.holders.length, 1);
assert.equal(c.chengxiang.holders[0].person, '陶青');
assert.equal(c.chengxiang.holders[0].since, -155, '陶青是景帝二年八月丁未拜的');
// 太尉周亚夫 —— 空了二十三年的位子,七国反的这一年才重开
assert.equal(c.taiwei.holders[0].person, '周亞夫');
assert.equal(c.taiwei.holders[0].since, -154);
// ★★ 御史大夫晁错 —— 哪怕他正月壬子就要斩了,那一年他【在场】。
// 这正是 holders 的口径:给的是「那一年在过这个位子上的人」,不是「年底还剩谁」。
// 按年底算,景帝三年的御史大夫就成了「无」—— 那这个功能最想说的那句话就没了。
assert.equal(c.yushi.holders[0].person, '朝錯');
assert.equal(c.yushi.holders[0].since, -155);
assert.equal(c.yushi.holders[0].left.event, '要斬', '他就是在这一年没的');
assert.equal(c.yushi.holders[0].left.month, '正月');
});
test('★★ 而那一年正月壬子,晁错要斩 —— 而你在场', () => {
const f = fateOf('朝錯');
assert.equal(f.found, true);
assert.equal(f.end.word, '要斬');
assert.equal(f.end.year, -154);
assert.equal(f.end.era, '孝景三年');
assert.equal(f.end.month, '正月');
assert.equal(f.end.day, '壬子');
assert.equal(f.end.office, '御史大夫');
assert.equal(f.end.quote, '正月壬子,錯有罪要斬。');
assert.equal(f.end.yearReliable, true, '这一笔年份是硬的:表自己给了年月日');
});
test('★ 晁错三种写法都认得(朝錯/晁錯/鼂錯)—— 一个本子里就有三种', () => {
const a = fateOf('朝錯'), b = fateOf('晁錯'), c = fateOf('鼂錯');
for (const f of [a, b, c]) {
assert.equal(f.found, true);
assert.equal(f.end.word, '要斬');
}
assert.equal(a.person, '朝錯');
assert.ok(a.variants.includes('晁錯') && a.variants.includes('鼂錯'));
});
test('★★ 景帝三年那一行,同僚自己就把关口生出来了', () => {
const c = courtAt(-154);
const names = [
...c.chengxiang.holders, ...c.taiwei.holders, ...c.yushi.holders,
...c.jiangjun, ...c.others,
].map((x) => x.person).filter(Boolean);
// 削藩的、腰斩的、劝斩他的、领兵平叛的、后来做到丞相的 —— 全在这一年
for (const who of ['陶青', '周亞夫', '朝錯', '竇嬰', '袁盎', '衞綰']) {
assert.ok(names.includes(who), `景帝三年少了 ${who} —— 这一年的图谱不全,关口就生不出来`);
}
});
test('★ 故吴相袁盎 —— 吴反的那一年,他从反王的相变成朝廷的奉常', () => {
const e = GONGQING.find((x) => x.person === '袁盎');
assert.equal(e.year, -154);
assert.equal(e.office, '奉常');
assert.equal(e.from, '故吳相');
assert.equal(e.quote, '故吳相袁盎為奉常。');
});
test('★ 窦婴那一笔挂着卷四十九的旁证:表记谁升了,传记为什么升', () => {
const e = GONGQING.find((x) => x.person === '竇嬰' && x.event === '拜' && x.year === -154);
assert.equal(e.office, '大將軍');
assert.ok(e.corroboration, '缺旁证');
assert.ok(/卷四十九/.test(e.corroboration.source));
assert.ok(/獨急斬錯以謝吳/.test(e.corroboration.quote), '这一句才是关口的由来');
assert.ok(/旁证不是本表|属卷四十九/.test(e.corroboration.caveat), '旁证必须标明它不是本卷');
});
// ══════════════════════════════════════════════════════════════════════════
// 5. ★★ 史学铁律 —— 这几组挂了,这个功能就得下线
// ══════════════════════════════════════════════════════════════════════════
test('★★【东市不在这一卷】:本表关于晁错之死只有九个字', () => {
const e = GONGQING.find((x) => x.event === '要斬' && x.year === -154);
assert.equal(e.quote, '正月壬子,錯有罪要斬。');
assert.ok(!e.quote.includes('東'), '「东市」出卷四十九,不许记到本卷账上');
assert.ok(!e.quote.includes('朝衣'));
assert.ok(!e.quote.includes('七國'), '「七国反」也不在这一卷');
assert.ok(/卷四十九/.test(e.caveat), 'caveat 必须说清楚东市出自哪一卷');
});
test('★★【表作「要斬」不作「腰斬」】—— 照录原字,不许径改', () => {
const yao = GONGQING.filter((x) => x.event === '要斬');
assert.equal(yao.length, 2, '本表只有两个人拿到这两个字:晁错、刘屈氂');
for (const e of yao) {
assert.ok(e.quote.includes('要斬'), '照录原字');
assert.ok(!e.quote.includes('腰斬'), '不许把「要」改成「腰」');
}
assert.deepEqual(yao.map((e) => e.person), ['朝錯', '劉屈氂']);
assert.equal(yao[1].year, -90, '刘屈氂:征和三年');
});
test('★★【引文里一个字的后人注都不许有】—— 顏師古是唐人,班固是东汉人', () => {
const notes = ['師古曰', '應劭曰', '如淳曰', '晉灼曰', '臣瓚曰', '張晏曰', '文穎曰', '鄧展曰', '孟康曰'];
for (const e of GONGQING) {
for (const w of notes) {
assert.ok(!String(e.quote).includes(w), `${e.era}/${e.office} 的 quote 混进了「${w}」`);
if (e.corroboration) {
assert.ok(!String(e.corroboration.quote).includes(w), `${e.era} 的旁证 quote 混进了「${w}」`);
}
}
}
});
test('★★【「N年遷」不许算出年份】—— 实测三个样本,两种读法互斥', () => {
// 周亚夫:前154 拜太尉,「五年遷」,实际前150 迁丞相 → 含拜年才对
const ya = GONGQING.find((x) => x.person === '周亞夫' && x.office === '太尉' && x.event === '拜');
assert.equal(ya.year, -154);
assert.ok(ya.quote.includes('五年遷'));
const yaCheng = GONGQING.find((x) => x.person === '周亞夫' && x.office === '丞相' && x.event === '拜');
assert.equal(yaCheng.year, -150, '前154 算第 1 年 → 前150 是第 5 年(含拜年)');
// 陶青:前162 拜御史大夫,「七年遷」,实际前155 迁丞相 → 不含拜年才对
const tao = GONGQING.find((x) => x.person === '陶青' && x.office === '御史大夫');
assert.equal(tao.year, -162);
assert.ok(tao.quote.includes('七年遷'));
const taoCheng = GONGQING.find((x) => x.person === '陶青' && x.office === '丞相' && x.event === '拜');
assert.equal(taoCheng.year, -155, '前162 + 7 = 前155(不含拜年)');
// ★ 两条互斥 —— 所以一律不算
assert.notEqual(
-154 + 5, -150,
'若「N年」= 拜年 + N,周亚夫该在前149 迁 —— 可他在前150。这就是不能算的理由'
);
assert.equal(-162 + 7, -155, '而陶青偏偏就是拜年 + N。两种读法互斥。');
});
test('★★ 张汤:下场写在拜官句尾 → fateOf 给结局,但 year 必须是 null', () => {
const f = fateOf('張湯');
assert.equal(f.found, true);
assert.equal(f.end.word, '六年有罪自殺');
assert.equal(f.end.year, null, '★「六年」是那个不可计算的记法 —— 算出一个年份就是撒谎');
assert.equal(f.end.yearReliable, false);
assert.ok(/算不出来/.test(f.end.yearNote));
// 拜官那一笔的年份是硬的
const bai = f.entries.find((e) => e.event === '拜' && e.office === '御史大夫');
assert.equal(bai.year, -120, '元狩三年三月壬辰,廷尉张汤为御史大夫');
});
test('★★ 直不疑:两个见证打架(三年死/三年免)→ 不裁,也不给 fate', () => {
const f = fateOf('直不疑');
assert.equal(f.found, true);
assert.equal(f.end, null, '★ 两个见证给的是两回事,硬给一个结局就是撒谎');
assert.equal(f.unresolved.length, 1);
assert.equal(f.unresolved[0].field, 'fate');
assert.ok(/草庐/.test(f.unresolved[0].witnessA) && /ctext/.test(f.unresolved[0].witnessB));
// quote 必须截在两边一致的地方
const e = GONGQING.find((x) => x.person === '直不疑');
assert.equal(e.quote, '八月壬辰,衞尉直不疑為御史大夫。');
assert.ok(!e.quote.includes('三年'), '有分歧的四个字不许进 quote');
});
test('★★ 霍光:月份异文(三月/二月)→ month 必须是 null,两说并陈', () => {
const e = GONGQING.find((x) => x.person === '霍光');
assert.equal(e.year, -87);
assert.equal(e.month, null, '★ 两个见证一个三月一个二月 —— 不许替读者选');
assert.equal(e.day, '丁卯');
assert.equal(e.variantUnresolved.field, 'month');
assert.ok(/本项目的推算/.test(e.caveat), '推算必须标明是我们的推算,不是史料的裁断');
});
test('★★【姓名不具的一律 null】—— 顏師古自己说这是史之阙文', () => {
for (const e of GONGQING) {
if (e.name && e.name.length === 1 && !e.from) {
// 单字且没有「记事链」把他串起来的
if (['殷', '勝', '介'].includes(e.name)) {
assert.equal(e.person, null, `「${e.office}${e.name}」只剩一个字,不许替它猜姓`);
}
}
}
// 接晁错位子的那个人,表只留下一个字
const jie = GONGQING.find((x) => x.name === '介');
assert.equal(jie.office, '御史大夫');
assert.equal(jie.year, -153);
assert.equal(jie.person, null);
assert.ok(/姓名不具/.test(jie.caveat));
assert.ok(/闕文|阙文/.test(SHIGU_NOTE.quote + SHIGU_NOTE.why));
});
test('★★【同名不并人】:元光四年同一年两个「張敺」→ person 必须置 null', () => {
const e = GONGQING.find((x) => x.name === '張敺');
assert.equal(e.year, -131);
assert.equal(e.office, '御史大夫');
assert.equal(e.person, null, '★ 同一年另有「宣平侯張敺為太常」,封邑不同,显非一人');
assert.equal(e.personUncertain, true);
assert.ok(/宣平侯/.test(e.caveat));
assert.ok(/同名不并人/.test(PERSON_NOTE.homonym));
});
test('★ 记事链解出来的名字是可以给 person 的 —— 那不是猜', () => {
// 「六月丞相嘉薨」的「嘉」= 申屠嘉,由同表孝文後元二年那一笔解出
const e = GONGQING.find((x) => x.name === '嘉' && x.event === '薨');
assert.equal(e.person, '申屠嘉');
const seed = GONGQING.find((x) => x.person === '申屠嘉' && x.event === '拜');
assert.equal(seed.year, -162);
assert.equal(seed.seed, true);
assert.ok(/申屠嘉/.test(e.caveat), '必须说清楚这个名字是哪儿解出来的');
});
test('★★ 引文标点必须声明为本项目自断 —— 见证一致恰恰是不能照抄的理由', () => {
assert.ok(/标点为本项目自断/.test(WITNESS_NOTE.punctuation));
assert.ok(/受著作权保护/.test(WITNESS_NOTE.punctuation));
assert.ok(/两个见证的标点【完全一致】/.test(WITNESS_NOTE.punctuation));
});
test('★ ctext 的坏字判定必须有内证,不许「我觉得草庐对」', () => {
assert.ok(/简体/.test(WITNESS_NOTE.ctextFlaws), '判 ctext 之误的依据是【坏字是简体】这个指纹');
const xue = GONGQING.find((x) => x.person === '薛澤' && x.event === '拜');
assert.ok(/平【觉】侯|平觉侯/.test(xue.caveat));
assert.ok(xue.quote.includes('平棘侯'));
});
// ══════════════════════════════════════════════════════════════════════════
// 6. ★ 太尉:「太尉是谁(或:省)」—— 官制的开合就是战事的影子
// ══════════════════════════════════════════════════════════════════════════
test('★★ 景帝元年:太尉是【省】—— 这个官那时候根本不存在', () => {
const c = courtAt(-156);
assert.equal(c.taiwei.status, '省');
assert.equal(c.taiwei.holders.length, 0);
assert.equal(c.taiwei.abolished[0].office, '太尉');
assert.equal(c.chengxiang.holders[0].person, '申屠嘉', '而丞相是文帝朝拜的 —— 接续记事就是为这个');
assert.equal(c.yushi.holders[0].person, '陶青');
});
test('★★ 七国反的那一年,空了二十三年的太尉重开', () => {
assert.equal(courtAt(-155).taiwei.status, '省', '景帝二年还是省');
assert.equal(courtAt(-154).taiwei.holders[0].person, '周亞夫', '景帝三年重开');
assert.equal(courtAt(-151).taiwei.holders[0].person, '周亞夫', '中间几年他还在');
assert.equal(-154 - -177, 23, '文帝三年官省 → 景帝三年重开,二十三年');
});
test('★ 周亚夫迁丞相 → 太尉又省;田蚡开一年 → 又省', () => {
// ★ 景帝七年的丞相【是两个人】:陶青(六月乙巳免)和周亚夫(继任)。
// holders 给的是「那一年在过这个位子上的人」,所以两个都在 —— 这才是实情。
const c150 = courtAt(-150);
assert.deepEqual(c150.chengxiang.holders.map((h) => h.person), ['陶青', '周亞夫']);
assert.equal(c150.chengxiang.holders[0].left.event, '免');
assert.equal(c150.chengxiang.holders[1].left, null, '周亚夫这一年拜相,还没走');
assert.equal(
c150.taiwei.holders[0].person, '周亞夫',
'★ 他迁丞相的那一年,太尉这个位子上【也还是他】—— 年内先后不可知,两边都算他在'
);
assert.equal(courtAt(-149).taiwei.status, '省', '次年就省了');
assert.equal(courtAt(-140).taiwei.holders[0].person, '田蚡', '建元元年为田蚡一个人又开了一次');
assert.equal(courtAt(-139).taiwei.holders[0].person, '田蚡', '免于建元二年 —— 那一年他还在');
assert.equal(courtAt(-138).taiwei.status, '省', '此后太尉这个官就没再有过');
});
// ★★ 这一条是先写测试才逼出来的:原先把「官省」记在【栏】上,
// 于是卫青一拜大司马,太尉的「省」就被顶掉了 —— courtAt(前110) 会说「太尉栏:不载」,
// 可太尉明明是【裁了】,而那一栏坐着大司马。裁的是官,不是栏。
test('★★【官省是「那个官」裁了,不是「那一栏」空了】—— 太尉裁着,大司马照样坐在同一栏里', () => {
const later = GONGQING.filter((x) => x.office === '太尉' && x.event === '拜' && x.year > -139);
assert.equal(later.length, 0, '★ 太尉这个官,建元二年之后本表一笔都没有了');
const c = courtAt(-110);
assert.equal(c.taiwei.holders[0].person, '衞青');
assert.equal(c.taiwei.holders[0].office, '大司馬大將軍');
assert.equal(c.taiwei.status, 'held');
assert.equal(c.taiwei.abolished[0].office, '太尉', '★ 太尉照样是裁着的 —— 裁的是官,不是栏');
assert.equal(c.taiwei.abolished[0].since, -139);
});
test('★ 卫青死后(前105—前88),那一栏是真的空了', () => {
for (const y of [-105, -100, -95, -90, -88]) {
assert.equal(courtAt(y).taiwei.status, '省', `前${-y} 年那一栏该是空的:太尉裁着,大司马也没人`);
assert.equal(courtAt(y).taiwei.holders.length, 0);
}
assert.equal(courtAt(-87).taiwei.holders[0].person, '霍光', '到武帝断气那一年才又坐上人');
assert.equal(-87 - -139, 52, '建元二年太尉官省 → 後元二年霍光拜大司马大将军,五十二年');
});
test('★ 推断出来的年份必须标出来,不许冒充原文', () => {
const inferred = GONGQING.filter((x) => x.yearInferred);
assert.equal(inferred.length, 2, '灌婴、周亚夫那两条「官省」的年份是推断的');
for (const e of inferred) {
assert.equal(e.event, '官省');
assert.ok(/推断/.test(e.caveat), '推断必须说是推断');
}
// 建元二年那条「官省」原文自带年份 → 不许标 inferred
const tian = GONGQING.find((x) => x.event === '官省' && x.year === -139);
assert.ok(!tian.yearInferred, '这一条原文就写在建元二年那一格里,不是推断');
// courtAt 必须把 inferred 透出去 —— 推断出来的东西不许在下游变成事实
assert.equal(courtAt(-156).taiwei.abolished[0].inferred, true, '景帝元年太尉之省,年份是推断的');
assert.equal(courtAt(-138).taiwei.abolished[0].inferred, false, '建元二年那条原文自带年份');
});
test('★ 元狩四年:太尉那一栏头一回同时坐了两个人(卫青、霍去病)', () => {
const two = GONGQING.filter((x) => x.year === -119 && x.seat === 'taiwei');
assert.equal(two.length, 2);
assert.deepEqual(two.map((x) => x.person), ['衞青', '霍去病']);
assert.deepEqual(two.map((x) => x.office), ['大司馬大將軍', '大司馬票騎將軍']);
});
test('★ 大司马放在太尉栏是【原表的编排】,本文件不表态', () => {
const seat = SEATS.find((s) => s.id === 'taiwei');
assert.ok(/原表的编排/.test(seat.note));
assert.ok(/不是本文件对二者关系的判断/.test(seat.note));
});
// ══════════════════════════════════════════════════════════════════════════
// 7. ★ 覆盖范围:不许自吹
// ══════════════════════════════════════════════════════════════════════════
test('★★ 三公全覆盖 —— 前156—前87 每一年都得有丞相(或明说不载)', () => {
for (let y = -156; y <= -87; y++) {
const c = courtAt(y);
assert.ok(
c.chengxiang.holders.length > 0 || c.chengxiang.status !== 'held',
`前${-y} 年丞相栏是空的 —— 既没人也没说不载`
);
assert.equal(c.coverage.sangong, true, `前${-y} 年该在覆盖范围内`);
}
});
test('★★ 列将军【不许当名单】—— 表自己就阙:卫青拜大将军这一笔根本没有', () => {
assert.equal(COVERAGE.jiangjun.complete, false);
const bai = GONGQING.find((x) => x.person === '衞青' && x.office === '大將軍' && x.event === '拜');
assert.equal(bai, undefined, '★ 表里真的没有这一笔 —— 一个大将军凭空出现在他升迁的那一格里');
const sima = GONGQING.find((x) => x.person === '衞青' && x.event === '拜');
assert.equal(sima.year, -119);
assert.ok(sima.quote.includes('大將軍衞青'), '这一格管他叫大将军,可他是哪年当上的,表没说');
assert.ok(/表阙/.test(sima.caveat));
assert.equal(courtAt(-119).coverage.jiangjun, false);
});
test('★★ 九卿【不许当全名单】—— courtAt 必须自己说这话', () => {
assert.equal(COVERAGE.jiuqing.complete, false);
const c = courtAt(-120);
assert.equal(c.coverage.others, false);
assert.ok(/绝不许/.test(c.othersNote), 'courtAt 必须把「others 不是全名单」透给调用方');
assert.ok(/只抠了两处/.test(COVERAGE.jiuqing.note));
});
test('★ 覆盖范围外的年份,courtAt 必须自己承认', () => {
const c = courtAt(-170); // 文帝朝
assert.equal(c.coverage.inRange, false);
assert.equal(c.coverage.sangong, false);
assert.ok(/不在覆盖范围/.test(c.coverage.note));
assert.ok(/不许当数/.test(c.coverage.note));
});
test('★ 覆盖范围外的条目必须标 seed,不许混进来冒充', () => {
for (const e of GONGQING) {
const inRange = e.year >= COVERAGE.years.from && e.year <= COVERAGE.years.to;
if (!inRange) assert.equal(e.seed, true, `${e.era}/${e.office} 落在范围外却没标 seed`);
}
assert.equal(GONGQING.filter((x) => x.seed).length, 5, '接续记事:灌婴×3、申屠嘉、陶青');
});
test('★ 年内先后不可知,courtAt 必须说这话 —— 不许拿它排「谁先谁后」', () => {
const c = courtAt(-154);
assert.ok(/不给月份/.test(c.withinYearNote));
assert.ok(/不许拿 holders 的顺序排先后/.test(c.withinYearNote));
});
// ★★ 这一条守的是 courtAt 最要紧的那个前提:重放需要链是完整的。
// 链不完整的栏若也照着接,courtAt 会一本正经地编 —— 那比没有数据坏得多。
test('★★ 链不完整的栏【不许接】—— 否则 courtAt(前100) 会说「大将军是窦婴」', () => {
// 窦婴景帝三年拜大将军,表【从没记过】他何时去此职
const dou = GONGQING.find((x) => x.seat === 'jiangjun' && x.person === '竇嬰');
assert.equal(dou.year, -154);
const left = GONGQING.find((x) => x.seat === 'jiangjun' && x.person === '竇嬰' && x.event !== '拜');
assert.equal(left, undefined, '表真的没记他何时不当大将军了');
// 若照三公那样接下来,他会一直「是」大将军 —— 必须不接
assert.deepEqual(courtAt(-154).jiangjun.map((x) => x.person), ['竇嬰'], '那一年表记了他,报');
assert.deepEqual(courtAt(-153).jiangjun, [], '★ 次年表没记,就不报 —— 不许接');
assert.deepEqual(courtAt(-100).jiangjun, [], '★★ 接下来就成了「前100年大将军是窦婴」—— 那是编的');
// 九卿同理
assert.deepEqual(courtAt(-154).others.map((x) => x.person).filter(Boolean), ['袁盎', '劉通', '衞綰']);
assert.deepEqual(courtAt(-153).others, [], '★ 九卿只抠了两处,接出来的是假的');
assert.ok(/链不完整的栏,接出来的是编的/.test(courtAt(-154).coverage.carryForward));
});
// ══════════════════════════════════════════════════════════════════════════
// 8. ★ fateOf:「这个人后来怎么了」
// ══════════════════════════════════════════════════════════════════════════
test('★ 表给的字分得清清楚楚,不许合并成「离任」', () => {
const cases = [
['田蚡', '薨', -131], // 死在丞相任上
['公孫弘', '薨', -121], // 死在丞相任上
['李蔡', '自殺', -118], // 有罪自杀
['嚴青翟', '自殺', -115], // 有罪自杀
['趙周', '下獄死', -112], // 下狱死
['公孫賀', '下獄死', -91], // 下狱死
['劉屈氂', '要斬', -90], // 腰斩
['霍去病', '薨', -117],
['衞青', '薨', -106],
];
for (const [who, word, year] of cases) {
const f = fateOf(who);
assert.equal(f.found, true, `${who} 不在表里`);
assert.equal(f.end.word, word, `${who} 的下场该是「${word}」`);
assert.equal(f.end.year, year, `${who} 的年份不对`);
}
});
test('★★ 窦婴:免相之后表里一个字都没有 —— 不载 ≠ 善终', () => {
const f = fateOf('竇嬰');
assert.equal(f.found, true);
assert.equal(f.end.word, '免', '★ 表只记到「免」为止');
assert.equal(f.end.year, -139);
// 建元二年之后他再没进过这张表
const after = f.entries.filter((e) => e.year > -139);
assert.equal(after.length, 0, '★ 公卿年表只记公卿:你不在那个位子上,你的死不归它管');
const tian = GONGQING.find((x) => x.person === '田蚡' && x.event === '薨');
assert.ok(/窦婴怎么完的.*一个字都没有/.test(tian.caveat.replace(/【|】/g, '')));
});
test('★ 周亚夫:表只记到「免」—— 别替表往下写', () => {
const f = fateOf('周亞夫');
assert.equal(f.end.word, '免');
assert.equal(f.end.year, -147);
assert.ok(/别替表往下写/.test(f.entries.find((e) => e.event === '免').caveat));
});
test('★ 公孙弘:博士 → 左内史 → 御史大夫 → 丞相,一条完整的线', () => {
const f = fateOf('公孫弘');
const line = f.entries.filter((e) => e.event === '拜').map((e) => [e.year, e.office]);
assert.deepEqual(line, [[-130, '左內史'], [-126, '御史大夫'], [-124, '丞相']]);
assert.equal(f.entries.find((e) => e.event === '拜' && e.office === '左內史').from, '博士');
assert.equal(f.end.word, '薨', '死在丞相任上');
});
test('★ 表里没有的人,fateOf 必须说「表里没有」,不许编', () => {
const f = fateOf('司馬遷');
assert.equal(f.found, false);
assert.deepEqual(f.entries, []);
assert.ok(/不载 ≠ 没这个人/.test(f.note));
});
// ══════════════════════════════════════════════════════════════════════════
// 9. 结构:出处、见证
// ══════════════════════════════════════════════════════════════════════════
test('每一条都必须有引文和出处 —— 正文是推演,注是原文', () => {
for (const e of GONGQING) {
assert.ok(e.quote && e.quote.length > 0, `${e.era}/${e.office} 缺引文`);
assert.equal(e.source, BIAO, `${e.era}/${e.office} 的 source 不是本卷`);
assert.ok([1, 2].includes(e.witnesses));
}
});
test('只有一个见证的必须自己说出来', () => {
const single = GONGQING.filter((e) => e.witnesses === 1);
for (const e of single) {
assert.ok(e.caveat, `${e.era}/${e.office} 只有一个见证却没有 caveat`);
assert.ok(/见证|异文|一致/.test(e.caveat), `${e.era} 的 caveat 得交代见证情况`);
}
assert.equal(single.length, 1, '只有晁错起家那一格两个见证的用字不一致');
});
test('顏師古注是【注】,必须标明,且是单证 —— 不装作有两个见证', () => {
assert.ok(/是注不是正文/.test(SHIGU_NOTE.caveat));
assert.ok(/单证/.test(SHIGU_NOTE.caveat));
assert.ok(/唐人/.test(SHIGU_NOTE.caveat));
});
test('event 只许用表自己的字', () => {
const words = new Set(EVENTS.map((e) => e.word));
for (const e of GONGQING) assert.ok(words.has(e.event), `${e.era} 用了表外的词「${e.event}」`);
for (const bad of ['离任', '任命', '去世', '解雇']) {
assert.ok(!words.has(bad), `「${bad}」是现代词,不许进 EVENTS`);
}
});
test('GONGQING 必须按年升序 —— courtAt 靠它重放', () => {
for (let i = 1; i < GONGQING.length; i++) {
assert.ok(GONGQING[i].year >= GONGQING[i - 1].year, `第 ${i} 条乱序`);
}
});