// 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} 没冻`); });