这一份记的是「改了什么、为什么这么改、哪条断言把它钉住」——按缺陷逐条排列, 包括每轮变异测试的结果。README 的「测试环境」一节给的是结论,那一节是过程。
- The package's own tests (
node --test "tests/**/*.test.mjs"from this directory) cover the hexagram table, the 错卦/综卦/互卦 derivations, the ganzhi anchors, the twelve offices, the 建除 cycle, the nine-day period, the coin rules, the response timing table, each method's change cadence, the element-to-topic matching, and store round-trips. The classic texts are covered entry by entry: all 384 爻辞 are checked against the hexagram diagrams, all 384 小象传 against their 爻题, and all 64 彖传 against the hexagram table. The twelve 辟卦 are checked against both the month branches and the hexagram table, and the four derivations against their own rules, and the casting timing against the typing budget, and the lookup default plus its explicit full-text return. The 京房 layer is checked on nine more counts: the palace order against the transmitted table, 归魂's and 游魂's flipped lines, the eight 纳支歌诀 clauses, branches following the trigram's polarity, the 六亲 mapping, 世应 pairing without overflow, the reading and diagram call sites, both lookup levels, and 主客 always straddling the two trigrams. 用神 is checked on four more counts: the nine topics against their sources, the line-picking order, 婚恋 refusing to pick for the querent, and 用神不上卦 refusing to invent. 伏神 is checked on five more: both worked examples reproduced character for character, all 64 hexagrams scanned so every missing 六亲 resolves to exactly one 伏神 from its own palace, the four 飞伏 names, the emergence conditions naming what they cannot check, the right-hand panel reporting the line it hides under, and the MCP response carrying the same thing as a field. 旬空, 月破 and 入墓 are checked on fourteen more: the 旬空 歌诀 against the algorithm, both worked examples from the text recovered from their day pillars, the twelve months of 月破, the five elements' 墓, the seasonal void, false-void rescue and true-void grounds, the emergence path actually reaching 「终不得出」 on void alone, the 休囚无气 clause, 空 破 墓 暗 日破 冲散 reaching the diagram and the right-hand panel, a 月破 line counting as 真空 even when it is not 旬空, that sentence no longer telling the reader to wait for a clash, and the 逢月破 ground not being said twice once the mark already names it. 六神 is checked on four more: the 歌诀 verbatim, all thirty-two cells of the six-row table, both 乾为天 examples, and the rule that a god must not move the verdict — the same hexagram cast across twenty-eight days has to hold its verdict while the god under its first line changes hands. 化爻 is checked on nine more: the two quoted directions of 回头生 and 回头克, all twenty ordered element pairs landing in five classes of five, 《卜筮正宗》's five 回头克 cases checked against the 生克 table, the other three relations carrying no verdict, the 歌诀's sixteen pairs stored verbatim and each checked same-element and reverse, the quote that confines a changed line to its own moving line, the changed line kept still so it cannot be rescued as 发动, the reading and the structured field agreeing, and MCP carrying it. The removed 绝 mark is checked too — a test asserts no 纳支 can land on its own 绝 branch, so nobody adds the label back thinking it was forgotten. The 元神 / 忌神 / 仇神 circle is checked on eight more: the book's 金 example reproduced exactly, all five elements following 「余仿此」 with the three positions never colliding, the 仇神 proved to 反生忌神 and not 生用神, the circle withheld when the 用神 went to the 伏神 or when two candidates stand, each of the three reported with its line, its 动/暗动/静/动而逢日冲 and its 旺衰, 「勿以仇神即仇人也」 kept, 回头克 read four ways — 凶 on the 用神, 不作凶论 on a 忌神 or 仇神, unjudged on a 元神, and not borrowed when there is no circle — and MCP carrying it. 暗动 · 日破 · 冲散 is checked on fifteen more: the chapter's 坤之师 example reproduced line by line and pinned as 日破 so the definition and the example cannot drift apart unnoticed, a clashed line dropping out of both still-line lists once it is moving, all sixty-four hexagrams over twelve months and twelve days confirming no line ever lands in two of the three, 暗动 and 日破 never sharing a reading, the 用神 section reporting 暗动 as its own state rather than folding it into 动 or 静, 元神 暗动 coming out 喜 and 忌神 暗动 coming out 忌, 仇神 and off-circle 暗动 left unjudged, no verdict attached when the 用神 is not settled, the 「用神休囚」 premise called out when the 用神 is 旺相, the chapter's own 风水涣 to 坎为水 example reproduced (丑月丁酉, top line 卯木 moving and clashed by 酉), 旺相 and 元神 and 用神 cases each placing the 冲散 exactly, the sentence refused the right to pronounce 凶, 冲散 proven independent of the month's build the way the test could actually fail, the 用神 section giving 「动而逢日冲」 its own tier instead of folding it into 动, and the diagram and MCP both carrying it. The two MCP header tests sweep all sixty-four hexagrams rather than pinning one cast, because the tool runs on the real clock and a fixed cast would quietly stop exercising the branch it names. 旺衰 is checked against all eight non-seasonal months of 《四时旺相章》 when the module loads, so transposing 囚 and 死 throws instead of silently reporting. 六冲 · 六合 is checked on nine more: the ten and the eight pinned name for name, the pairing offsets pinned to 初四、二五、三六 so the naive inner/outer pairing cannot creep back, the pairing table itself read from 纳甲, the 「one pair proves three」 fact held across all sixty-four, no hexagram counted as both, each of the four reportable ways placed on a real reading, 爻与爻冲 kept below 六冲卦 and never opening a section on its own, the 用神-conditional half applied at 旺 and at 囚 and declined when the 用神 is not settled, the 近病/久病 rule quoted without a side picked, the 官讼 clause joined only when the matter really is 官讼是非, and the diagram plus MCP carrying it. The margin arcs are checked too: that they are drawn only after the diagram is on the page (measuring row heights while the node is still detached yields an empty picture), that they take their width from the gutter rather than out of the hexagram, that the arc's reach still fits inside that gutter, that they are muted and never red, that they do not intercept clicks, and that each one names its two branches. The 八宫名单 is checked too, on both sides. Ten checks on the data: the eight slots sit in generation order and their names match the received ordering palace by palace (each slot's name and key must be the same hexagram); the 世 walks up one line at a time from 一世 to 五世 while the flipped lines grow by one each step; the 游魂 slot does not flip the fourth line; the 归魂 slot flips only the fifth; every slot's 世 and 应 sit three apart; all sixty-four hexagrams find their own slot and agree on 世 and 应 with the hexagram's own reading; exactly sixteen hexagrams are 游魂 or 归魂 (that slot carries two marks when it is your own hexagram, which is what the client's "red wins over bold" rule is there for); and the whole roster including each slot's flipped lines is frozen, since the eight hexagrams of one palace read the same array. The roster is also checked not to be a second copied table — it must be the one that came out of the same derivation as the palace and generation themselves. Eight checks on the page: the row is really inserted rather than a function that is never called, it sits ahead of the four derivations, the heading names the palace and its element, the caption explains where 游 and 归 come from, the small-hexagram styles are not scoped back under .derive (doing that would drop the red flipped lines from the eight slots), the derivation diagram's own arrows and row spacing stay scoped under .derive, the red self-slot rule is written after the bold 游归 rule at the same specificity, and the eight slots mark flipped lines but not the 世 (the hexagram to the left already boxes it in red). 爻之合 is checked too. Nine checks: the 六合 table pairs the twelve branches without repeat or gap and the pairing is bidirectional; a day branch equal to the month branch counts once rather than twice; 合起 takes only still lines and 合绊 only moving ones, the two never overlapping; 合好 requires both lines moving (a still-moving pair does not count, and exactly twenty of the sixty-four have a harmonizing pair outside 初四二五三六 — 雷火丰's first 卯 against its top 戌 gives the one-moving/one-still contrast); 化扶 requires a moving line whose transformed line combines back; the combined-line positions are exactly the union of the four paths; the reading names all four without deciding 吉凶 (both closing lines of the chapter are quoted verbatim); the hexagram gets a muted 「合」 mark, and among the mark rules that take 朱砂 onlypoandtombmay appear; and MCP carries acombinefield plus a 【逢合】 header line. 爻之刑 is checked too. Six checks: the base text's six and the fate-reading set of eight are not merged (卯刑午 stands, not 卯刑子, and 未辰相刑 is not split into 未刑丑 and 戌刑未, since merging both sets would make 「how many lines are punished」 meaningless); punishment has a direction (卯刑午 holds while 午刑卯 does not); self-punishing 辰 is always 0 across the sixty-four, fixed by the 纳甲 (辰 only sits in the inner lines and a hexagram has one lower trigram), while 午, 酉 and 亥 all come out and a self-punishing pair is reported only once; the book's case reproduces step by step (寅月申日, 风火家人 changing to 离卦, the month branch 寅 punishing the fifth line's 巳 and the day branch 申 punished by that same 巳, both on one line); the reading does not decide 吉凶 from punishment and checks 「用神休囚」 and 「又兼他爻犯之」 one by one; the hexagram gets a muted 「刑」 mark and MCP carries apunishfield plus a 【犯刑】 header line. A load-time check sweeps all sixty-four hexagrams across twelve day branches, twelve month branches and all sixty-four motion patterns, verifying what makes each of the four paths valid rather than merely whether it fired. The client tests read the source, since there is no DOM in the test runner. 212 passing.
- The type scale and the page layout were raised together, twice, and checked page by page at a 1524x1304 viewport: casting, reading (the hexagram rows, the four-hexagram derivation, the twelve-辟卦 growth ring, the eight-palace list), library, almanac (twelve hours, solar terms, zodiac), and history with a 40-character question saved into it. Nothing wrapped or overflowed, and the browser console stayed clean.
- The hexagram drawings are pinned by a test on two counts, both of which were once true and
read as a grey smudge: the yin lines may not be painted in
--border-strong(two tenths of black, used so that yin would recede), and no line drawing may fall back to a 4-5px hairline. Yang and yin are told apart by shape — one whole bar against two broken halves — so brightness has nothing to add, and the main hexagram on the reading page is held to a heavier minimum than the thumbnails it sits beside. - The twelve-hour grid is pinned by a test: its column count must divide 12 and the time and
office lines must not wrap, because an
auto-fittrack silently squeezed each cell until03:00 - 05:00broke across two lines and left that row of cards at two different heights. - Three more layout defects are pinned by tests, all found by looking at the running page rather than by reasoning about it. The eight-palace roster aligned its items to the top, and the current hexagram alone carried a border and padding for its seal frame, so that one cell sat five pixels lower than the other seven; the frame is an outline now, which draws without taking up room. On the twelve-辟卦 ring the bars reach further out than the labels were hung, so on the two sectors carrying all six lines the bars fell across the characters — the labels moved out and the canvas grew from 200 to 232 units square to make room for them. The ring's breathing animation dipped the current sector to 0.55 opacity, flickering the very bars it was pointing at out of legibility twice every 2.6 seconds; it now bottoms out at 0.8.
- The reading page is two columns with the right one far longer than the left, so scrolling left a
large blank behind. The title and the four sections are now one pinned top bar — a translucent
blurred ground with a rule under it — and on a wide viewport with enough height the left column
is pinned and offset by the top bar's own height. That height was a number written into
:root, and measuring showed it was the 1340px value: narrow the content area to 700px and the title, the subtitle and the ganzhi pills stop fitting on one line, the top bar grows to about 195px, and the left column still offsets by 132px — which pushed the 天风姤 heading out from under the bar. The height is now measured by aResizeObserverand written back;--topbar-his only the first-paint fallback, and the two places no longer carry a number each. - The four-hexagram derivation and the eight-palace roster now lay themselves out from their own
width rather than the viewport's. Both were flex-wrap: narrow the content area and the four steps
folded into 3+1, the eight cells into 7+1, and the orphan sat centred on its own as though it had
dropped out of the list. Each block is now a named container (
container-type: inline-size) and its column count comes from@container— four or two for the derivation, eight or four for the roster, every count dividing the total so that no row is left one short. Both sit in the right-hand column, whose width the left column and the page margin decide rather than the viewport; a viewport query would happily pair a wide screen with a narrow column and pack them unevenly. A test asserts that no@mediarule touches either block, which is what keeps the column count from drifting back onto the viewport. - Inside the derivation the arrow and the hexagram are one cell now, not two siblings. The
mutual-hexagram note runs to two lines and the other three to one, and the arrow was a centred
flex column, so that one sat half a line below the other three and the four were not on a line. The
note is a centred flex box held at two lines tall, the current hexagram's lone cell spans the whole
row so it centres instead of sitting in the first column, and the line break comes from the
\nin the data (white-space: pre-line) rather than from wherevermax-widthhappened to fall. That width was 88px, and the longest note segment is seven CJK characters — 91px at the note's 13px — so that note wrapped to three lines and pushed its arrow off the line with the others. A test measures every hard-broken segment against the note's width, so a box narrower than the text goes red. - The page margin is one value now.
.apppadded 32px either side, spending 64px of width on a 1149px viewport, and that is what pushed the derivation and the roster into wrapping. It is--app-pad: clamp(14px, 2.1vw, 32px);.appuses it and.topbaruses its negation, because two places carrying a number each drift apart the moment the margin moves. The top bar's margin and padding, the gap between the reading page's two columns, and the left column's width are each asserted to follow that one value. - The left column was briefly narrowed to
clamp(232px, 21vw, 366px)to give the right column more room, and that was wrong. A hexagram row is34px + 42px + 1fr + 32px + autoplus four 12px seams, so 156px is gone to the fixed parts before the 纳甲 cell — ganzhi, relative, 世 and 应, void and broken, every one of themnowrap— gets its 140-odd px to live in. Under 300px the 纳甲 pushes through its track into the right column, and the line drawing is squeezed flat at the same time, so yin and yang stop being readable at a glance. It isclamp(300px, 26vw, 366px)now, with the floor asserted against the width the rows actually need.minmax(clamp(...), 366px)was tried first and is a no-op: with a fixed maximum the track sits at 366px for as long as the content is wide enough, so the lower bound never comes into play. - The six tests added for this round were each mutation-tested: twenty-seven mutations broken on
purpose across the two container queries, the four column counts, the cell wrapper, the note box,
the margin's single source, the left column's floor, and the negative case that no viewport query
reaches either block. Each was checked to turn the matching assertion red, and all 27 were caught;
the suite reports 212 passing after the script restored the files. Two traps turned up while
writing the script, and one of them was a real gap in a test. The script had been matching each
mutation's expected assertion against the test name, but those expectations are assertion
messages, so a run in which every single mutation behaved correctly reported 26 of 27 unpinned;
it now reads the failure detail instead. And the narrow-screen assertion read
grid-template-columns: 1fr, which1fr 1fralso matches — the test was vouching for the two-column case while claiming to check the one-column case. The trailing semicolon is what separates them now. Restoring the files only infinallyis a third trap, already noted above: mutation N then runs against the file mutation 1 broke, which reads as a test that never pinned anything when it was contamination. Every mutation here restores from a clean baseline first. - A review of the whole package turned up three defects, all in the history store and the cast id,
all now fixed and pinned. The id was a second-resolution timestamp hashed together with the
hexagram order and the moving lines. Number casting depends only on the two numbers, so casting
the same pair twice inside one second produced byte-identical ids — and the 起卦 button is never
disabled while the casting animation holds for
CASTING_HOLD_MS, so an ordinary double-click sends two requests. Both readings get saved, andremove(id)filters by id, so deleting one deleted the other as well: measured, two entries with the same id, one delete, zero left. The id seed carries a process-local counter now, and the test pins the consequence rather than a literal string — two readings of the same hexagram in the same second must differ, and deleting one must leave the other. - On Windows, renaming a file over an existing one fails outright with
EPERMorEACCESwhen another process holds the target open without delete sharing — a virus scanner or the search indexer, which is not rare. Nothing is half-written; the save simply fails. The rename now retries with backoff onEPERM/EACCES/EBUSYup to four times (about 400ms in total) and throws everything else immediately, since retrying a full disk or a read-only mount only holds the request. The decision is a pure function so it can be tested without provoking a real lock, and the call site is pinned separately — the predicate alone stays green if someone deletes the loop that uses it. - A
readings.jsonthat cannot be parsed used to throw out of every history endpoint, so a truncated write, a hand edit or a sync conflict left the history permanently unopenable with no way to recover. It is now renamed aside toreadings.json.corrupt-<timestamp>— moved, not deleted, so a hand-written note can still be recovered — and the history reads as empty. Separately,ReadingStore.update()had no route, no caller and no test; the only field it produced wasupdatedAt, whichtoSummarycopied out to become a field that was alwaysundefined. Both are gone rather than left as a branch that can never run and a key that implies a note-editing feature the package does not have.GET /history/:idstays: its store method is covered by a test, and it is the natural shape of the resource. - The fifteen assertions added for these three were each mutation-tested, and all 15 were caught
with the suite reporting 212 passing after the script restored the files. Three of the first
run reported unpinned and all three were the script's fault rather than a loose test: one
expectation named an assertion the mutation tripped after an earlier one had already fired, one
mutation deleted the call that an earlier assertion already covered, and one threw out of the
test before reaching the assertion that named the contract — the test now catches the rejection
and folds the cause into the message, so a failure reports the contract that broke rather than a
raw JSON parse error. The suite's own wording is also worth repeating:
assert.matchandassert.equalwithout a message throw Node's default text, so any keyword-based "did this mutation pin anything" check can never match. Two of those assertions carry an explicit message for that reason. - The 起卦 button had no in-flight guard, and that is the other half of the id defect above. The
cast beat holds for
CASTING_HOLD_MS— over four seconds — and throughout it every method button and the 起卦 button stayed clickable, so an ordinary double-click sent two cast requests, two casting logs typed over each other, and whichever answered last won. The onlydisabledin that panel belonged totoss-finish, and that one encodes a different rule — six tosses required — not "a cast is in flight". All three casting paths (daily/time, numbers, coins) go throughcast(), so that is where the gate now lives: acastingflag set synchronously before the firstawait, and the triggers locked for the duration. - The restore is the part that is easy to get wrong, so it is pinned explicitly. Unlock puts every
control back to the value it had rather than clearing
disabledacross the board:toss-finishis meant to be disabled whenever fewer than six coins have been thrown, and a blanket re-enable would leave a "成卦解卦" button that looks available and does nothing. The unlock sits infinally, because a single failed request must not leave the whole casting panel permanently dead. Andcast()now returns whether the reading was actually cast: the numbers path clears its two inputs only on success, since clearing them after a skipped or failed cast throws away what the user just typed. - Verified on the running page rather than by reading: double-clicking 时间起卦 produced exactly
one
POST /api/divination/castin the network log, and after the cast finishedtoss-finishstill carrieddisabled=""while the method buttons carried none. The disabled state during the four-second beat was not observed directly — the round trip between tool calls is longer than that window — but the assignment sits in the same synchronous block as thecastingflag that the double-click test proves took effect. The twelve assertions covering this were each mutation-tested and all 12 were caught, the suite reporting 212 passing after the script restored the files. Two failed the first run for reasons in the script rather than in the test: one mutation was an empty change that altered no behaviour, and another removed the wholefinallyblock, so it tripped the earlier "there is no finally" assertion rather than the one it aimed at. - The two coin-toss buttons used to contradict each other.
renderToss()computed which one should be disabled from how many tosses had landed, and then the tail oftossOnce()hard-wrote an unconditional re-enable on top of that, wiping the "six tosses is enough, disable 掷钱" it had just computed. Past six tosses both buttons were therefore live at once: a seventh toss could be thrown — a line that fits into neither a six-line hexagram nor anything the user can take back, so the reading shifts — and the six tosses could still be cast. Neither button meant anything. Availability now lives in one place,syncTossButtons(), derived only fromstate.tosses.length, and the in-flight tail calls it to recompute rather than blanket-enabling. Same lesson as the cast lock above: unlocking has to be computed from state, never by clearingdisabledwholesale. - Two related holes are closed in the same pass. 重来 is held while a toss is in flight — resetting halfway through would drop the line already on its way into a list that was just emptied, leaving a line in the hexagram with no visible origin. And the "six tosses, then 重来" path runs no in-flight tail at all, so its button state has to come from the redraw itself rather than from whatever the previous tail happened to leave behind.
- This part of the suite runs the logic instead of matching source text:
syncTossButtons,lockToss,tossOnceandrenderTossare lifted out verbatim and driven against a fake DOM and a fake fetch through seven paths — idle, one to five tosses, the sixth, a seventh, 重来, in-flight, and a failed request. The defect lived in who writesdisabledlast, which source text cannot show. All thirty-six assertions were mutation-tested and all 15 mutations were caught, the suite reporting 212 passing after the script restored the files. Seven failed the first run for reasons in the script: the expect named a later assertion while the test went red on an earlier one carrying the same meaning — relaxing "成卦解卦 is always enabled" to> 6, for instance, trips the idle-state assertion first. One of them was a genuinely loose test: it measured "did a failed toss record a line?" by counting rows in the hexagram log, but a failed request never reachesrenderToss, so the log is empty no matter what — it now measuresstate.tosses.lengthdirectly. - Verified on the running page as well: with a throwaway runtime up, six clicks on 掷钱 left
#toss-btncarryingdisabled=""and#toss-finishenabled, and 重来 put both back exactly at the start state (one clickable, one disabled). - The casting beat used to play only the engine's own 取数 steps — two for the coins, three for
numbers — so the speech ran out before the hexagram was even formed. It now follows with the four
lines that come after, in order of importance: the upper trigram, lower trigram and resulting
hexagram, the moving lines and the changed hexagram, the palace with its 世 and 应, and the body
and use with their elements. All four methods produce all four, and every word comes from a field
the engine has already computed. When the budget runs short it is the body-and-use line that goes;
the other three may never be squeezed out. That budget had a hole in it once: the closing line's
characters were never counted, so on a wordier cast the real duration pushed past the budget and
the last line was cut off by the page change. The closing line reserves its share now, and the hold
takes its length from the constant
CASTING_HOLD_MS(the 4s typing budget plus 400ms) instead of a second number at the call site. The four methods measure 3904-4000ms of typing against a 4400ms hold. The log is held at 320px, which fits the 11 lines the beat can reach (10 under the line cap plus the closing one) — each is nowrap at a fixed line height, so a pixel short of that is a missing pixel. - The fourteen assertions added for this round were each mutation-tested: six on the top bar
(pinned, blurred ground, rule, stacking, cancelled padding, wrapping the sections), five on the
left column (pinned, offset following the bar, viewport-bounded, height measured back, not a dead
number), four on the beat (all four lines present, the three important ones never squeezed, the
closing line kept, the fixed height fitting 11 lines), and two on the timings (not through the
budget, hold taken from the constant). Each implementation was broken on purpose to confirm the
matching test really went red; all 22 were caught, and the suite reports 212 passing after the
script restored the files. One trap turned up while writing the script: restoring only in
finallymeans mutation N runs against the file mutation 1 already broke, so anchors go missing and the red counts climb — which reads as "the test never pinned it" when it was contamination. - The package's own tests (
node --test "tests/**/*.test.mjs"from this directory) cover the hexagram table, the 错卦/综卦/互卦 derivations, the ganzhi anchors, the twelve offices, the 建除 cycle, the nine-day period, the coin rules, the response timing table, each method's change cadence, the element-to-topic matching, and store round-trips. The classic texts are covered entry by entry: all 384 爻辞 are checked against the hexagram diagrams, all 384 小象传 against their 爻题, and all 64 彖传 against the hexagram table. The twelve 辟卦 are checked against both the month branches and the hexagram table, and the four derivations against their own rules, and the casting timing against the typing budget, and the lookup default plus its explicit full-text return. The 京房 layer is checked on nine more counts: the palace order against the transmitted table, 归魂's and 游魂's flipped lines, the eight 纳支歌诀 clauses, branches following the trigram's polarity, the 六亲 mapping, 世应 pairing without overflow, the reading and diagram call sites, both lookup levels, and 主客 always straddling the two trigrams. 用神 is checked on four more counts: the nine topics against their sources, the line-picking order, 婚恋 refusing to pick for the querent, and 用神不上卦 refusing to invent. 伏神 is checked on five more: both worked examples reproduced character for character, all 64 hexagrams scanned so every missing 六亲 resolves to exactly one 伏神 from its own palace, the four 飞伏 names, the emergence conditions naming what they cannot check, the right-hand panel reporting the line it hides under, and the MCP response carrying the same thing as a field. 旬空, 月破 and 入墓 are checked on fourteen more: the 旬空 歌诀 against the algorithm, both worked examples from the text recovered from their day pillars, the twelve months of 月破, the five elements' 墓, the seasonal void, false-void rescue and true-void grounds, the emergence path actually reaching 「终不得出」 on void alone, the 休囚无气 clause, 空 破 墓 暗 日破 冲散 reaching the diagram and the right-hand panel, a 月破 line counting as 真空 even when it is not 旬空, that sentence no longer telling the reader to wait for a clash, and the 逢月破 ground not being said twice once the mark already names it. 六神 is checked on four more: the 歌诀 verbatim, all thirty-two cells of the six-row table, both 乾为天 examples, and the rule that a god must not move the verdict — the same hexagram cast across twenty-eight days has to hold its verdict while the god under its first line changes hands. 化爻 is checked on nine more: the two quoted directions of 回头生 and 回头克, all twenty ordered element pairs landing in five classes of five, 《卜筮正宗》's five 回头克 cases checked against the 生克 table, the other three relations carrying no verdict, the 歌诀's sixteen pairs stored verbatim and each checked same-element and reverse, the quote that confines a changed line to its own moving line, the changed line kept still so it cannot be rescued as 发动, the reading and the structured field agreeing, and MCP carrying it. The removed 绝 mark is checked too — a test asserts no 纳支 can land on its own 绝 branch, so nobody adds the label back thinking it was forgotten. The 元神 / 忌神 / 仇神 circle is checked on eight more: the book's 金 example reproduced exactly, all five elements following 「余仿此」 with the three positions never colliding, the 仇神 proved to 反生忌神 and not 生用神, the circle withheld when the 用神 went to the 伏神 or when two candidates stand, each of the three reported with its line, its 动/暗动/静/动而逢日冲 and its 旺衰, 「勿以仇神即仇人也」 kept, 回头克 read four ways — 凶 on the 用神, 不作凶论 on a 忌神 or 仇神, unjudged on a 元神, and not borrowed when there is no circle — and MCP carrying it. 暗动 · 日破 · 冲散 is checked on fifteen more: the chapter's 坤之师 example reproduced line by line and pinned as 日破 so the definition and the example cannot drift apart unnoticed, a clashed line dropping out of both still-line lists once it is moving, all sixty-four hexagrams over twelve months and twelve days confirming no line ever lands in two of the three, 暗动 and 日破 never sharing a reading, the 用神 section reporting 暗动 as its own state rather than folding it into 动 or 静, 元神 暗动 coming out 喜 and 忌神 暗动 coming out 忌, 仇神 and off-circle 暗动 left unjudged, no verdict attached when the 用神 is not settled, the 「用神休囚」 premise called out when the 用神 is 旺相, the chapter's own 风水涣 to 坎为水 example reproduced (丑月丁酉, top line 卯木 moving and clashed by 酉), 旺相 and 元神 and 用神 cases each placing the 冲散 exactly, the sentence refused the right to pronounce 凶, 冲散 proven independent of the month's build the way the test could actually fail, the 用神 section giving 「动而逢日冲」 its own tier instead of folding it into 动, and the diagram and MCP both carrying it. The two MCP header tests sweep all sixty-four hexagrams rather than pinning one cast, because the tool runs on the real clock and a fixed cast would quietly stop exercising the branch it names. 旺衰 is checked against all eight non-seasonal months of 《四时旺相章》 when the module loads, so transposing 囚 and 死 throws instead of silently reporting. 六冲 · 六合 is checked on nine more: the ten and the eight pinned name for name, the pairing offsets pinned to 初四、二五、三六 so the naive inner/outer pairing cannot creep back, the pairing table itself read from 纳甲, the 「one pair proves three」 fact held across all sixty-four, no hexagram counted as both, each of the four reportable ways placed on a real reading, 爻与爻冲 kept below 六冲卦 and never opening a section on its own, the 用神-conditional half applied at 旺 and at 囚 and declined when the 用神 is not settled, the 近病/久病 rule quoted without a side picked, the 官讼 clause joined only when the matter really is 官讼是非, and the diagram plus MCP carrying it. The margin arcs are checked too: that they are drawn only after the diagram is on the page (measuring row heights while the node is still detached yields an empty picture), that they take their width from the gutter rather than out of the hexagram, that the arc's reach still fits inside that gutter, that they are muted and never red, that they do not intercept clicks, and that each one names its two branches. The 八宫名单 is checked too, on both sides. Ten checks on the data: the eight slots sit in generation order and their names match the received ordering palace by palace (each slot's name and key must be the same hexagram); the 世 walks up one line at a time from 一世 to 五世 while the flipped lines grow by one each step; the 游魂 slot does not flip the fourth line; the 归魂 slot flips only the fifth; every slot's 世 and 应 sit three apart; all sixty-four hexagrams find their own slot and agree on 世 and 应 with the hexagram's own reading; exactly sixteen hexagrams are 游魂 or 归魂 (that slot carries two marks when it is your own hexagram, which is what the client's "red wins over bold" rule is there for); and the whole roster including each slot's flipped lines is frozen, since the eight hexagrams of one palace read the same array. The roster is also checked not to be a second copied table — it must be the one that came out of the same derivation as the palace and generation themselves. Eight checks on the page: the row is really inserted rather than a function that is never called, it sits ahead of the four derivations, the heading names the palace and its element, the caption explains where 游 and 归 come from, the small-hexagram styles are not scoped back under .derive (doing that would drop the red flipped lines from the eight slots), the derivation diagram's own arrows and row spacing stay scoped under .derive, the red self-slot rule is written after the bold 游归 rule at the same specificity, and the eight slots mark flipped lines but not the 世 (the hexagram to the left already boxes it in red). 爻之合 is checked too. Nine checks: the 六合 table pairs the twelve branches without repeat or gap and the pairing is bidirectional; a day branch equal to the month branch counts once rather than twice; 合起 takes only still lines and 合绊 only moving ones, the two never overlapping; 合好 requires both lines moving (a still-moving pair does not count, and exactly twenty of the sixty-four have a harmonizing pair outside 初四二五三六 — 雷火丰's first 卯 against its top 戌 gives the one-moving/one-still contrast); 化扶 requires a moving line whose transformed line combines back; the combined-line positions are exactly the union of the four paths; the reading names all four without deciding 吉凶 (both closing lines of the chapter are quoted verbatim); the hexagram gets a muted 「合」 mark, and among the mark rules that take 朱砂 onlypoandtombmay appear; and MCP carries acombinefield plus a 【逢合】 header line. 爻之刑 is checked too. Six checks: the base text's six and the fate-reading set of eight are not merged (卯刑午 stands, not 卯刑子, and 未辰相刑 is not split into 未刑丑 and 戌刑未, since merging both sets would make 「how many lines are punished」 meaningless); punishment has a direction (卯刑午 holds while 午刑卯 does not); self-punishing 辰 is always 0 across the sixty-four, fixed by the 纳甲 (辰 only sits in the inner lines and a hexagram has one lower trigram), while 午, 酉 and 亥 all come out and a self-punishing pair is reported only once; the book's case reproduces step by step (寅月申日, 风火家人 changing to 离卦, the month branch 寅 punishing the fifth line's 巳 and the day branch 申 punished by that same 巳, both on one line); the reading does not decide 吉凶 from punishment and checks 「用神休囚」 and 「又兼他爻犯之」 one by one; the hexagram gets a muted 「刑」 mark and MCP carries apunishfield plus a 【犯刑】 header line. A load-time check sweeps all sixty-four hexagrams across twelve day branches, twelve month branches and all sixty-four motion patterns, verifying what makes each of the four paths valid rather than merely whether it fired. The client tests read the source, since there is no DOM in the test runner. 212 passing. - This round read the verdict block on the real page field by field, one cast per question across
seven of them. Five named their matter — 事业, 财运, 疾病, 房产 and 出行 came back as 「这份前程」,
「这笔进项」, 「这桩病症」, 「这处房产或这纸契」 and 「这趟行程或这件失物」. Two left the sentence off
by design: 「明天的会议会顺利吗」 is simply outside the nine classes, and 「他对我还有没有真心」 is a
real gap — the 感情 keyword list does not catch that phrasing, so the line disappears rather than
being filled with something that happens to fit. Adding a word for it means weighing phrasings like
「公司对我是不是真心的」, so it is recorded here rather than quietly widening the table. The seven
runs covered 大凶, 大吉, 吉 and 平; the caution appears on every non-平 reading and is absent on 平,
as coded. In the DOM the block really does precede both columns and the right column no longer
carries a second copy of the summary. The toss progress is a
role="group"named 「摇卦进度」 holding arole="status"「已摇 0 / 6 爻」, with the bar width and the counter pinned across 0–6. - One environment note worth keeping: the in-app Browser on this machine does not deliver clicks.
clickreturnssuccess/dispatched: trueand the page does not react — the section tabs do not switch, the input keeps its placeholder — and everyref-based click reportsSTALE_ELEMENT_REF. The page's own JavaScript is alive (settingdocument.titletakes effect at once), and timers in a background tab really are throttled (setIntervalgoes tens of seconds without a callback). The way through is to let the page drive itself: a second same-origin proxy in front of the same server injects a driver script into the real page, clicks are fired by that script, and waiting uses theloadevent of a deliberately delayed image as the timer, since events are not throttled. Results are written into a<pre>on the page and the Browser only reads. That path did produce a result page from a real click — the check that stayed blocked in the previous round. Casting a second time in the same tab sentPOST /castand never saw it return; hitting the real server and the proxy directly both cast repeatedly in milliseconds, so the stall is the client animation's throttled timers, not the server or the proxy. - Every new assertion this round went through mutation testing: the block's position and the order of its seven parts, 宜/忌 and the caution each rendered exactly once, no duplicate summary in the right column, no arrow in the fact table, the short word taken only from after the colon; one distinct 取象 sentence for each of the nine classes with none repeated, none when the class is unrecognised, strength following the total score; and the progress bar's width and counter across 0–6. Each was broken in turn to confirm the matching test actually goes red — all 17 pinned, and 214 pass with 0 fail after the script restores the baseline.
- This round moved two decisions out of static analysis and into the model.
divination_castgained atopicenum that the Agent fills in after reading the user's own words; the page still uses the keyword table, because on a page there is nobody to read the question for you. Both paths were checked: for 「他对我还有没有真心」 the keyword table catches nothing (detectTopicreturns null and the topic sentence is left off entirely), whiletopic: loveputs the class, the topic sentence, the 用神 and the 应期 all where they belong; and for 「这工作该不该跳」, which the keywords would have filed under 事业功名, an explicitwealthwins. A value outside the nine is rejected outright (INVALID_ARGUMENTS, with "omit it if you are unsure") rather than ignored — silently ignoring it leaves the Agent believing it landed. Across five topics the 吉凶 was checked item by item:label,scoreand the 体用 relation do not move. - The plain block is that same apparatus with the terminology taken out, not a second opinion. 体卦 becomes 「你」, 用卦 becomes 「那件事」, and 旺相休囚死 becomes how much force you have right now — the three most abstract things in the method. None of 「体卦 / 用卦 / 旺衰 / 类神 / 月令」 may appear anywhere in it, checked one by one in the tests. Writing that assertion is what caught 「月令」 and then 「旺衰」 slipping into my own copy; it took two passes to get clean. Where the 生克 layer and the total disagree (体克用 is 小吉 on 生克 alone, but a 体卦 on death ground drags the total to 平), the difference is spelled out rather than left for the reader to suspect a broken calculation. The sample response in the README was generated by running the code, not written by hand.
- Every new assertion this round went through mutation testing — all 26 pinned, and 217 pass with 0
fail after the script restores the baseline. The first run left eight unpinned, and six of those were
the tests being too soft rather than the mutations being clever: the 旺衰 check only asked whether a
term had leaked and never whether it had been translated (pasting the raw 旺 back in stayed
green); the blank-line check pointed at the wrong function; the ordering check passed with the other
line deleted, because
indexOfon a missing string returns -1 and -1 is still less than -1; and two places read.topic.keydirectly, which throws a TypeError when the topic is null — and a TypeError carries no message, so a keyword-based "is it pinned" check can never match it. Optional chaining fixed those two. The other two were faults in the mutations themselves: one left the file a syntax error, so the run reported a SyntaxError instead of any assertion, and one edited the main block's filter rather thanplainBlock, which has nothing to do with what it claimed to break. - The coins path was broken end to end over MCP, and the user is the one who reported it.
divination_castwithmethod: coinsalways failed with 「六次掷钱结果必须是 6 到 9 之间的整数」:tossCoins()performs one toss and returns a{ sum, coins }object, whilecastByCoins()wants the six results as an array, so the object's.lengthisundefinedand not one call got through. The page route (server.mjs) collects six tosses separately and composes them, so only the MCP side was affected — the user found it by casting through the engine directly, bypassing MCP. The fix is one line. The same read turned up thatcastByCoinsdoes not rejectNaNorundefined(a non-number compares false against anything), so a hand-made request quietly composed an all-zero 坤卦 that looked like a hexagram while nothing had been tossed;Number.isIntegercloses that too. - Why earlier rounds missed it: the engine-level
castByCoinshas dozens of tests that all feed it an array, and the MCP end-to-end coverage only ever exercisednumbers—time,dailyandcoinswere never called at all. No method had end-to-end coverage. All four are exercised now: the hexagram name and order land in 1–64, the verdict is one of the five, the plain block is at the end, and all four must pass the Agent's topic through (dropping it on one still yields a complete reading, which is invisible from outside); coins additionally checks that the 起卦依据 line really lists six tosses, and that twelve consecutive tosses do not all produce the same hexagram. - 「Time casting uses the present 时辰」 now has evidence behind it too: the 「月 · 日」 line in 起卦依据
is checked against today's date. Comparing the 月令旺衰 instead does not work — that comes from
buildReading'snow, so passing 1970 tocastByTimestill yields the current month's element and both sides agree. The midnight boundary is covered by accepting either the date before or after the call. - The SKILL gained a step 「起卦前先把这件事问清楚」: casting the moment the user asks produces an interpretation that will not attach to their actual situation — abstract and thin, which is exactly the failure being avoided. That section, the nine topic keys and 「relay the plain block at the end」 are all asserted: it is the Agent's entry point, the repository check only verifies the file exists, and one tidy-up edit can remove any of it silently.
- All 14 mutations this round are pinned, and 219 tests pass with 0 fail after the script restores the
baseline. The first run left five unpinned: two were real gaps (no method was checked for passing the
topic through, and 「present time」 had nothing to check against), two were wrong expect keywords on my
side, and one was an equivalent mutation —
Array.isArrayis redundant next to the length check, so removing it changes no behaviour at all and the knife had to be replaced.