Files
digital-me-history/digital-me-backend/test/ta.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

308 lines
20 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.
// ta.js —— 塌。
//
// ★ 本文件的性质,照 caocao.test.js / qian.test.js 的口径:
// 【规矩写在注释里没用,写成断言才算数。】
// ⚠ 而本表是【常量表】,所以断言分两种,别混:
// ① 【体例断言】:本表自己的规矩(股别、卷次、见证数、推演不许安卷次…)—— assertTa()
// ② ★★ 【抄件断言】:本表抄的 dynasties.js 的数据,必须与真数据一致 —— assertSnapshot()
// ⇒ 它的方向是【本表过时了】,✗ 不是「引擎错了」。见 ta.js WHY_NO_IMPORT。
import test from 'node:test';
import assert from 'node:assert/strict';
import {
LING,
THE_VERB_SURVIVED,
THIRD_KIND,
THE_JOKE,
GATE_VS_CEILING,
ONLY_HAN,
ZERO_HITS,
NEW_TRAPS,
HAN_CHANNELS_SNAPSHOT,
WITNESS_NOTE,
lingOf,
zhengwenLing,
peizhuLing,
qiuxianStrata,
yearCoverage,
whatTheyChanged,
assertTa,
assertSnapshot,
} from '../src/ta.js';
import { getDynasty } from '../src/dynasties.js';
import { assertNoGameMechanics } from '../src/decisions.js';
// ══════════════════════════════════════════════════════════════════════════
// 体例
// ══════════════════════════════════════════════════════════════════════════
test('ta:体例自检 —— assertTa() 零违规', () => {
const bad = assertTa();
assert.deepEqual(bad, [], '体例违规:\n' + bad.join('\n'));
});
test('★★ ta:抄件必须与 dynasties.js 的真数据一致 —— 分叉 = 本表过时了,不是引擎错了', () => {
const bad = assertSnapshot(getDynasty('han'));
assert.deepEqual(
bad,
[],
'★ HAN_CHANNELS_SNAPSHOT 与引擎分叉了。\n' +
'⚠ 断言的方向是【本表过时了】—— 去 ta.js 把抄件更新,并【重读所有依赖那个数的话】。\n' +
bad.join('\n')
);
});
test('★ ta:抄件不是 import —— 本表必须是叶子(不 import dynasties.js / caocao.js)', async () => {
const fs = await import('node:fs');
const src = fs.readFileSync(new URL('../src/ta.js', import.meta.url), 'utf8');
// ★ 见 WHY_NO_IMPORT ③:本表也不 import caocao.js —— 两把刀必须【独立地】撞到同一个地方。
assert.equal(/^\s*import\s/m.test(src), false, '★ ta.js 必须是纯常量表,不 import 任何东西(见 WHY_NO_IMPORT)');
});
test('ta:不许有手游机制的字面 —— 数据会走到用户眼前', () => {
// ⚠ lao.js 的教训:为了禁它而提到它,扫描器照样判红 —— 禁令写注释里,数据里一个字都不许有。
const bad = assertNoGameMechanics({
LING, THE_VERB_SURVIVED, THIRD_KIND, THE_JOKE, GATE_VS_CEILING, ONLY_HAN, ZERO_HITS, NEW_TRAPS,
});
assert.deepEqual(bad, [], bad.join('\n'));
});
// ══════════════════════════════════════════════════════════════════════════
// ★★★ 老板问②:求贤令在正文还是裴注 —— 算出来,不靠注释说
// ══════════════════════════════════════════════════════════════════════════
test('★★★ ta:三道求贤令 —— 两道在正文(210/214),一道在裴注(217)', () => {
const s = qiuxianStrata();
assert.deepEqual(
s.rows.map((r) => [r.year, r.kind]),
[[210, 'zhengwen'], [214, 'zhengwen'], [217, 'peizhu']]
);
// ★ 走得最远的那一道,落在注里 —— 与 caocao.js 同形(免/復 只在注里),第二例。
assert.equal(lingOf('qiuxian-217').kind, 'peizhu');
});
test('★★ ta:正文 3 道,裴注 2 道 —— 五道令,一道不多一道不少', () => {
assert.equal(LING.length, 5);
assert.equal(zhengwenLing().length, 3);
assert.equal(peizhuLing().length, 2);
assert.equal(zhengwenLing().length + peizhuLing().length, LING.length, '★ 每一道令都必须有股别');
});
// ══════════════════════════════════════════════════════════════════════════
// ★★★ 老板要的那个数据形状 —— 两栏是空的,而空得有理由
// ══════════════════════════════════════════════════════════════════════════
test('★★★ ta:「天花板变化」全表为 0 —— 这是结论,不是漏填', () => {
const w = whatTheyChanged();
assert.equal(w.changedCeiling, 0, '★★ 五道令一道都没改天花板 —— 改天花板的那一刀是 caocao.js 的 208');
// ★★★ 这个 4 是【断言按住的】,不是手数的:我在注释里数成了 3(把「三道求贤令」
// 和「动闸的令」混成了一个集合),而【庚申令也动闸】(「軍吏……堪任郡國之選」)。
// ⇒ caocao.js 的口径第三次复现:【手数的数一律要有断言看着,否则它一定会漂。】
assert.equal(w.changedGate, 4, '★ 庚申令 / 210 / 214 / 217 四道动的是【闸】');
assert.deepEqual(w.gateIds, ['gengshen-noyear', 'qiuxian-210', 'yiwei-214', 'qiuxian-217']);
// ★ 只剩己酉令不动闸 —— 它是【军法】,不是取士。
assert.equal(lingOf('jiyou-203').gateChange, null);
// ★ 而 assertTa 里有一条断言钉着:ceilingChange 必须恒为 null。
for (const l of LING) assert.equal(l.ceilingChange, null);
});
test('★★★ ta:「起讫年」—— 五道令里,最要紧的那一道【原文没给年】', () => {
const y = yearCoverage();
assert.equal(y.total, 5);
assert.equal(y.undated, 1);
// ★ 没年的那道,正是「治平尚德行,有事賞功能」那一道。
assert.deepEqual(y.undatedIds, ['gengshen-noyear']);
const g = lingOf('gengshen-noyear');
assert.equal(g.year, null);
assert.equal(g.yearKind, 'none');
// ⚠ 不许有人「顺手」把它安成 203(裴注把它排在建安八年己酉令之侧 —— 那是裴松之的编次,不是令的日期)
assert.ok(/裴松之的編次|裴松之的编次/.test(g.yearNote), '★ 必须说明:203 是裴注的位置,不是令的日期');
});
test('★★ ta:有年的四道,全部是「年次是原文的,公元是本表换的」—— 没有一道原文直写年号', () => {
const dated = LING.filter((l) => l.year !== null);
assert.equal(dated.length, 4);
for (const l of dated) {
assert.equal(l.yearKind, 'tuiyan-from-yuanwen', `${l.id}:公元是本表换的,必须标出来`);
assert.ok(l.yearNote, `${l.id}:必须有 yearNote`);
}
});
// ══════════════════════════════════════════════════════════════════════════
// ★★★★★ 头号结论:舉没有停,它的宾语被换掉了
// ══════════════════════════════════════════════════════════════════════════
test('★★★★★ ta:THE_VERB_SURVIVED —— 同一个「舉」字,从「舉孝廉」走到「不仁不孝……其各舉所知」', () => {
const c = THE_VERB_SURVIVED.chain;
assert.equal(c.length, 4);
// ★ 每一步的原文里都必须真的有那个动词 —— 不是我在注释里说它有。
for (const s of c) assert.ok(s.quote.includes('舉'), `这一步的原文里没有「舉」:${s.quote.slice(0, 20)}`);
// ★★ 头尾两步:一个被舉为「孝廉」,一个下令舉「不仁不孝」的人。
assert.ok(c[0].quote.includes('舉孝廉爲郎'));
assert.ok(c[3].quote.includes('不仁不孝'));
assert.ok(c[3].quote.includes('其各舉所知'));
// ★★★ 而最后那一步在【注】里。
assert.equal(c[3].kind, 'peizhu');
assert.equal(c[0].kind, 'zhengwen');
});
test('★★★ ta:「初,公舉种孝廉」—— 他自己成了举主,而这一句在正文', () => {
const c = THE_VERB_SURVIVED.chain[1];
assert.equal(c.kind, 'zhengwen');
assert.ok(c.quote.includes('初,公舉种孝廉'));
assert.ok(c.quote.includes('唯其才也'), '★ 他舉的人叛了他,而他说「唯其才也」,放了他');
// ⚠ 「初」= 追叙 ⇒ 舉的年份原文没给。这一条必须说出来,否则会有人把它读成 199。
assert.ok(/追叙/.test(c.when), '★ 「初」是追叙 —— 199 是生禽种那一年,不是他舉种那一年');
});
// ══════════════════════════════════════════════════════════════════════════
// ★★★★★ 老板的红线 —— 写成断言,不写成祈祷
// ══════════════════════════════════════════════════════════════════════════
test('★★★★★ ta:红线 —— 「天花板塌了 ≠ 你就上得去」,第三档必须自己说出「新尺子上照样有零分」', () => {
const third = THIRD_KIND.kinds.find((k) => k.id === 'reclassified');
assert.ok(third, '★ 缺 reclassified 那一档 —— 那正是本刀的头号结论');
assert.ok(/零分/.test(third.theRedLine), '★ 必须说:一个不会打仗的孝廉,在新尺子上是零分');
assert.ok(/另一張卷子|另一张卷子/.test(third.theRedLine), '★ 必须说:那不是更容易的卷子,是另一张卷子');
assert.ok(/落榜/.test(third.theRedLine), '★ 必须说:他是被重新量了一遍然后落榜了 —— 不是「他上得去」');
});
test('★★★ ta:老板的二分是两档,本表是三档 —— 而第三档是引擎装不下的那一档', () => {
assert.equal(THIRD_KIND.kinds.length, 3);
assert.deepEqual(THIRD_KIND.kinds.map((k) => k.id), ['zero-score', 'no-ruler', 'reclassified']);
// ★ 前两档引擎有(no-ruler 就是 dynasties.js 的 KEMU_ABSENCE.kind);第三档它装不下。
const r = THIRD_KIND.kinds.find((k) => k.id === 'reclassified');
assert.ok(/沒有|没有/.test(r.inEngine), '★ 第三档:引擎没有它,而且是【类型】上没有位置');
});
test('★★★ ta:红线 —— 本表只有汉,一条别朝的都不许有', () => {
for (const l of LING) assert.equal(l.dynastyId, 'han', `★ ${l.id}:不许给别的六朝也编「塌」`);
assert.ok(/本刀沒查那六朝|本刀没查那六朝/.test(ONLY_HAN.why), '★ 必须说「我没查」,不许说「它没有」');
});
// ══════════════════════════════════════════════════════════════════════════
// ★★ 零命中 —— 铁律:零命中比单证更危险
// ══════════════════════════════════════════════════════════════════════════
test('★★ ta:每一条零命中都必须逐字符复核过,并且自己说出【它不说明什么】', () => {
assert.ok(ZERO_HITS.entries.length >= 4);
for (const z of ZERO_HITS.entries) {
assert.ok(z.charCheck, `「${z.word}」:没有逐字符复核`);
assert.ok(z.whatItDoesNotSay, `「${z.word}」:没有说它不说明什么 —— ★ 别反过来说本表无据`);
// ★ 「察舉」那一条尤其:它必须自己说出「舉」字在本卷里其实很多 —— 否则它读起来像「察举没了」。
if (z.word === '察舉') {
assert.ok(/舉」三本【正文各 16/.test(z.charCheck), '★ 必须给出「舉」的单字计数 —— 证明不是检索坏了');
assert.ok(/論文的話|论文的话/.test(z.whatItDoesNotSay), '★ 「察举名存实亡」是论文不是史料 —— 老板点名禁了');
}
}
});
// ══════════════════════════════════════════════════════════════════════════
// ★★ 本轮的三个新坑 —— 教训比结论值钱,所以它们也要被断言看着
// ══════════════════════════════════════════════════════════════════════════
test('★★ ta:NEW_TRAPS —— 四个坑,每个都要说清【怎么发现的】与【怎么修】', () => {
assert.equal(NEW_TRAPS.length, 4);
for (const t of NEW_TRAPS) {
assert.ok(t.what && t.why && t.fix, `坑 ${t.id}:what/why/fix 缺一不可`);
assert.ok('didItBite' in t, `坑 ${t.id}:必须说它有没有咬到人 —— ★ 「没咬到」是运气,不是方法`);
}
});
test('★★★★★ ta:第四个坑 —— 「没有哪个见证是那个可靠的见证」', () => {
const t = NEW_TRAPS.find((x) => x.id === 4);
assert.ok(t, '★ 缺第四个坑 —— 那是本刀最值钱的一条方法论');
// ★ 前五处异文错的都是 S,第六处错的是 T ⇒ 谁开始信任一本,谁就只剩两个见证。
assert.ok(/沒有哪個見證是|没有哪个见证是/.test(t.why));
assert.ok(/只剩兩個見證|只剩两个见证/.test(t.why));
// ⚠ 而它咬到了本表自己的数据(「既」→「旣」)—— 必须承认。
assert.ok(/咬到了/.test(t.didItBite), '★ 它咬到了我自己的 quote —— 不许写成「没咬到」');
});
test('★★★ ta:对照量 —— 三本的「太祖」必须是同一个数(它是切干净了的证据)', () => {
// ★ 切考證之前 S 是 70,W/T 是 62。⇒ 这个 62 就是「S 的第三股已经切掉了」的证据。
assert.ok(/正文 62 \/ 裴注 80/.test(WITNESS_NOTE.control), '★ 对照量必须写死在数据里,不是注释里');
// ⚠ 而它必须自陈它的局限:它对坑①敏感,对坑②(简体 remap)不敏感。
assert.ok(/差點把警報讀成了噪聲|差点把警报读成了噪声/.test(WITNESS_NOTE.control));
});
// ══════════════════════════════════════════════════════════════════════════
// ★★★★ 两把刀合龙:gate 与 ceiling
// ══════════════════════════════════════════════════════════════════════════
test('★★★★ ta:GATE_VS_CEILING —— caocao.js 证了 ceiling,本刀证了 gate,两个都不是常数', () => {
assert.equal(GATE_VS_CEILING.ceiling.provedBy.startsWith('caocao.js'), true);
assert.ok(GATE_VS_CEILING.gate.provedBy.includes('本刀'));
// ★ ceiling 那一条是 caocao.js 的,本表只是引它 —— 引文必须是原文。
assert.ok(GATE_VS_CEILING.ceiling.quote.includes('漢罷三公官'));
assert.ok(GATE_VS_CEILING.gate.quote.includes('上之人不求之耳'));
});
test('★★★ ta:210 年那道令,逐字驳的就是引擎 xiaolian 的 gateNote —— 而两句都对', () => {
const l = lingOf('qiuxian-210');
assert.equal(l.kind, 'zhengwen');
assert.ok(l.quote.includes('上之人不求之耳'));
assert.ok(l.quote.includes('唯才是舉'));
assert.ok(l.quote.includes('若必廉士而後可用,則齊桓其何以霸世'), '★ 老板点名的那一句');
// ★★ 而引擎的 gate 里那一维(wang = 乡论)就在抄件里 —— 它必须真的在。
const x = HAN_CHANNELS_SNAPSHOT.channels.find((c) => c.id === 'xiaolian');
assert.equal(x.gate.wang, 30, '★ 210 年那道令驳的就是这一维');
});
test('★★★ ta:庚申令 —— 「治平尚德行,有事賞功能」= 原文说【标准是时势的函数】,而它在注里', () => {
const g = lingOf('gengshen-noyear');
assert.equal(g.kind, 'peizhu', '★ 它是裴注引王沈《魏書》');
assert.ok(g.quote.includes('治平尚德行,有事賞功能'));
assert.ok(g.quote.includes('郡國之選'), '★ 它吵的正是【谁能被察举】');
// ⚠⚠ 而它必须自己拆自己:最替本刀说话的那句话,出自最偏袒曹操的那个史源。
assert.ok(/回護|回护/.test(g.finding), '★ 王沈《魏書》於曹操多回护 —— 必须说出来');
assert.ok(/他在辯解|他在辩解/.test(g.finding), '★ 这是他在替自己的做法辩护 —— 必须说出来');
});
// ══════════════════════════════════════════════════════════════════════════
// ★★★★★ 216 —— 举主与传主,在塌掉的天花板两边
// ══════════════════════════════════════════════════════════════════════════
test('★★★★★ ta:THE_JOKE —— 「孤今日可復作尉否?」「昔舉大王時,適可作尉耳。」', () => {
assert.equal(THE_JOKE.kind, 'peizhu');
assert.ok(THE_JOKE.quote.includes('孤今日可復作尉否'));
assert.ok(THE_JOKE.quote.includes('昔舉大王時,適可作尉耳'));
assert.ok(THE_JOKE.quote.includes('王大笑'));
assert.equal(THE_JOKE.witnesses.length, 3);
});
test('★★★ ta:裴松之【自己在那条注里做了两证核对】—— 而本表照抄他的判法,不自己裁', () => {
// ★ 他判了两件事,判得不一样:右丞「疑此不然」;舉北部尉「如此則爲有徵」。
assert.ok(THE_JOKE.quote.includes('疑此不然'));
assert.ok(THE_JOKE.quote.includes('如此則爲有徵'));
assert.ok(/一千六百年/.test(THE_JOKE.peiSongzhiDidOurJob), '★ 他做的正是本项目在做的事');
});
test('★★★ ta:那条注顺手证明 —— 曹操第一格的【举主】,原文给了两个人,裴松之只考了一个', () => {
assert.ok(THE_JOKE.quote.includes('梁鵠以公爲北部尉'));
assert.ok(THE_JOKE.quote.includes('司馬建公所舉'));
// ★ 而本表不替裴松之裁「梁鵠」那一说。
assert.ok(/本表【也不替他裁】/.test(THE_JOKE.twoJuzhu));
});
// ══════════════════════════════════════════════════════════════════════════
// 函数:不炸
// ══════════════════════════════════════════════════════════════════════════
test('ta:纯函数 —— 非法输入不炸,查不到返回 null(不许猜)', () => {
assert.equal(lingOf('没有这道令'), null);
assert.equal(lingOf(null), null);
assert.equal(lingOf(undefined), null);
assert.deepEqual(assertSnapshot(null), ['没拿到 dynasties.js 的汉 —— 无法验证 HAN_CHANNELS_SNAPSHOT']);
assert.ok(Array.isArray(assertSnapshot({ era: 'x', span: 'y', channels: [] })));
});
test('ta:常量表必须是冻的 —— 证词不许被人改', () => {
assert.equal(Object.isFrozen(LING), true);
assert.equal(Object.isFrozen(HAN_CHANNELS_SNAPSHOT), true);
assert.equal(Object.isFrozen(THE_VERB_SURVIVED), true);
for (const l of LING) assert.equal(Object.isFrozen(l), true, `${l.id} 没冻`);
});