Put the subject under test on the table
It started with a single relationship chain. I pressed 爸爸 (father) and then 爸爸 again, and the screen answered 爷爷, my father's father. I expected one more press to change the answer; it took three presses in a row before 奶奶 finally appeared. The page has no text field at all, 0 input elements and 0 textarea elements, so the only thing you can feed it is its 22 buttons, and what comes out is decided entirely by one relationship definition table plus one if-chain. I handled it the way I handle a subject under test: the template itself is 15,121 bytes and 13,713 UTF-16 code units spread over 418 line breaks, and I cut the function bodies out verbatim and ran them on a DOM stub. The instrumented build and the real page pipeline were then compared chain by chain over the 100 two-step chains and the 1,000 three-step chains, and the mismatch count was 0. Every figure below rests on that check.
36 rule sites, and the decision order is just the line order
The lookup function lookupTerm occupies lines 261 through 391, which is 131 lines and 3,569 code units. Inside that block the keyword return appears 38 times, and 2 of them sit in a chain.some callback and are therefore not rules, so the remaining 36 returns are 36 term sites; the same block also holds 47 if statements, 2 else branches and 14 ternaries. The order is exactly the source order: the single-tier branch on line 272 fires first and the unconditional fallback on line 388 fires last. Order is what matters here, because once a chain is caught by an earlier site, a later site written far more carefully is never reached at all. I replaced the matching line with an empty statement and watched where the chain landed instead, and here are the 5 pairs I picked.
| Chain | Line hit | Result | After deleting that line |
|---|---|---|---|
father,mother | 386 | 长辈(女性) elder generation, female | 388 晚辈(女性) younger generation, female |
father,older_brother,wife | 290 | 姑姑 father's sister | 318 伯母 father's brother's wife |
mother,older_brother,wife | 294 | 姨妈 mother's elder sister | 319 舅妈 mother's brother's wife |
father,father,son | 287 | 伯伯/叔叔 father's brother | 388 晚辈(男性) younger generation, male |
father,son,son | 388 | 晚辈(男性) younger generation, male | no return is left at the end of the function |
Full enumeration: how many chains fall back and how many stay ambiguous
Take the Cartesian product of the 10 buttons grouped by chain length. With multi-dimensional combinations the row count is a product rather than a sum, which the older note listing every combination already works through. Forward direction, male perspective. The 100 two-step chains produce only 28 distinct terms, and the 34 fallback, 15 ambiguous and 51 named chains are spread over 19 words; the 1,000 three-step chains produce 30 words, where the fallback count climbs to 384 and the ambiguous and named counts are 165 and 451; the 10,000 four-step chains produce 34 words, 4,032 of them fallback and 1,368 ambiguous. Steps 1 through 4 together come to 11,110 chains. Fallback here means the 6 words 兄弟 (brother), 姐妹 (sister), 长辈(男性) (elder generation, male), 长辈(女性) (elder generation, female), 晚辈(男性) (younger generation, male) and 晚辈(女性) (younger generation, female), while ambiguous means the 6 strings that carry a slash.
Figure 1: the fallback line grows faster than the word list does
isOlder and reverseRel: age order is thrown away in two places
isOlder lives on lines 406 to 412. It scans backward from the tail of the chain: a hit on older_ returns true, a hit on younger_ returns false, and when the chain contains no sibling tier at all it takes line 411 and returns false. That single default decides which word the peer generation gets. Among the 100 two-step chains the 8 peer words collapse to 表弟 4 and 表妹 4, younger male cousin and younger female cousin, and all 8 of those rows come from the default false: only 0 valid chains actually carry younger_, and the 4 words for an older brother or an older sister score 0 as well. The 1,000 three-step chains are the first ones where the comparison has anything to say, with 5 rows resolving to 堂弟, 5 to 堂哥, 1 to 堂妹 and 1 to 堂姐, and all 48 rows that come out as a younger brother or a younger sister belong to the group whose chain really does contain younger_. The default group accounts for 0 of those 12 rows. There are 216 chains that hold no sibling tier at all, and every one of them runs through line 411.
Figure 2: reverse the chain once and age order is gone
The other place is reverseRel, whose table body runs from line 239 to line 249. Under M all 4 sibling tiers, 哥哥 (older brother), 姐姐 (older sister), 弟弟 (younger brother) and 妹妹 (younger sister), map to younger_brother, and under F they all map to younger_sister, so age order is smoothed away during the reversal. Passing male changes nothing either: that value never equals the capital F, so the code always takes the F side and the two tables end up identical across all 10 tiers. The collapse is easy to see, because [father,older_brother] and [father,younger_brother] both reverse into [younger_sister,son]. Among the 23 words that the reversed three-step enumeration produces, 伯伯, 嫂子, 姐夫, 堂哥, 表哥, 堂姐 and 表姐 each appear 0 times.
5 of the 6 shortcut examples throw and the chain area prints undefined
The page is the kinship calculator. Its shortcut row is 6 buttons on one line, labelled 爷爷的儿子, 姑姑的儿子, 舅舅的女儿, 叔叔的妻子, 外婆 and 姑姑, which is 4 chains of 3 tiers and 2 chains of 2 tiers. Apart from the 外婆 entry, whose source text is 'mother,mother', the other 5 token strings carry brother or sister, and neither of those two is among the 10 keys defined in relDefs. Click one and calculate() throws TypeError: Cannot read properties of undefined (reading 'side') while reading def.side on line 105 of the script.
After the throw the two outputs are not in the same state. The result area is never rewritten: if you computed something earlier, the term simply stays at its previous value. I pressed until 姑姑 showed up, then fed it a chain carrying brother, and the screen still read 姑姑. The chain area, by contrast, had already been rewritten, because addRel pushes before it renders: relLabels finds nothing for brother, so the tail of innerHTML receives the literal undefined, that is 9 code units, and kcChain keeps the unknown token exactly as typed, measured as ["father","father","brother"]. Repeating that same click with 6 unfamiliar tokens threw the identical error 6/6. Hand-editing the same entry into the legal chain 'father,father,older_brother' produces 爷爷. How you take a chain apart and count the people over dinner is the subject of the scenario note the meal where nobody could name anyone, and this article is only about that table.
FAQ
Q: if a fallback word comes back, did I build the chain wrong?
Not necessarily. 34 of the 100 two-step chains, 384 of the 1,000 three-step chains and 4,032 of the 10,000 four-step chains run through the fallback, and a perfectly legal chain lands there just the same, because gen only takes 3 values and the rules look at only 7 fields of state.
Q: how do I reproduce these counts?
Cut relDefs, calculate, lookupTerm, singleTerm, isOlder and reverseRel out of the <script> block of the template exactly as written, run them on a DOM stub that implements only getElementById, textContent and innerHTML, and then enumerate the 10 tiers length by length. Against the real pipeline the instrumented build showed 0 disagreements over the 100 two-step and 1,000 three-step chains, which is why this table of 36 sites was trusted as a sound input.