This is the fifteenth post in the LocalPapa Notes dev-log series.
After the chart engine went live, a gap became obvious: the user sees twelve palace positions filled with star names — then what? For most people, "武曲 in the Life Palace, 廉貞 in the Wealth Palace" is a sequence of unfamiliar names. Without interpretation, a chart is just a pretty grid.
Three interpretation paths — only one keeps the promise
There are three engineering paths for chart interpretation:
A. Local table lookup: pre-encode interpretation text in JS, render directly in the browser. Nothing leaves the device.
B. Server-side AI call: send the chart to a backend, call an LLM, return the interpretation. Highest quality, but the user's birth data and chart travel to our servers.
C. Local computation + one-click to the user's own AI: calculate the chart locally, provide a "copy chart summary" button so the user can paste it into ChatGPT themselves. The user decides whether to upload.
Which to pick? LocalPapa's position is "100% browser, no upload" — B is out immediately. C is an interesting hybrid, but it pushes interpretation quality off our hands to a third-party AI. We chose A: local lookup, interpretation fully on-device. Close the tab, the birth date disappears, no exceptions.
Combinatorial lookup: 168 cases collapsed to 40 entries
The core of chart interpretation is "what does this star in this palace mean." The instinctive approach: write a paragraph for each of 14 major stars × 12 palaces = 168 combinations. Problems with that:
- Handcrafting 168 text entries almost guarantees uneven quality and format drift.
- Maintenance hell: change one star's description, revisit 12 entries spread across 12 palaces.
- OKF sync: the knowledge-base
.md files need to stay in sync too — 168 places instead of 14.
The combinatorial approach instead:
- Star essence (14 entries): each star's core personality — 武曲 is "wealth, fortitude, drive"; 廉貞 is "career ambition, rule-following, complex emotions"…
- Palace domain (12 entries): each palace's area of governance — Life Palace is "self, personality, outward image"; Wealth Palace is "financial outlook, earning style, spending tendencies"…
- Combination: at render time, dynamically assemble "star essence + palace domain" into a sentence.
That's 14 + 12 = 26 entries for baseline interpretations. Add 14 individual Life Palace deep-reads (the Life Palace is the most important palace, worth its own detailed entry) and 4 Four-Transformation effect descriptions — total 44 entries, 75% fewer than 168, yet more consistent quality and far easier OKF sync.
The trade-off: combinatorial text sacrifices precision for uncommon combinations — a hand-crafted "武曲 in Life Palace" would be more precise than what a generic combination produces. For MVP, "broad coverage, consistent quality, maintainable" outweighs "maximum precision for select combinations." This trade-off is noted on the page ("general tendencies, not precise fortune-telling").
Single source of truth, dual output
Where do the interpretation texts live? Same rule as OKF: build_ziwei_okf.py holds them, the generator outputs both:
tools/ziwei-interpret.js (window.ZiweiInterpret): browser-side interpretation data, same-origin JS, no fetch, offline-cacheable via Service Worker. The page's renderInterpret(chart, lang) reads it to assemble text.
- OKF star pages (
okf/ziwei/stars/*.md): each star's OKF page gains a "general tendencies" section — an AI agent reading the knowledge base gets the same interpretation knowledge.
One data source, two outlets. Edit once, both sync.
The language-code trap
After launch, switching to Chinese and viewing interpretations showed only star names — the text was gone. English worked fine.
Root cause: ziwei-interpret.js uses zh and en as its language keys; but the page's language code is zh-TW and en. Querying with zh-TW against a key named zh finds nothing.
Fix: one line at the renderInterpret entry point — lang.startsWith('zh') ? 'zh' : 'en'. One line, problem gone. But the bug's nature is worth noting: it wasn't a logic error — it was an interface contract mismatch. The data-holder (the generator) used one naming convention; the caller (the page) used another; both were internally consistent, just incompatible at the boundary. This kind of bug is silent, hard to reproduce, and especially common in multilingual systems.
Three Directions/Four Alignments: the engine already computed it
The interpretation MVP read only the "single palace" — the Life Palace's star tendencies. The advanced layer is Three Directions/Four Alignments (三方四正): a core Zi Wei technique that says you can't read the Life Palace in isolation. You look at it together with the Wealth Palace (first of the triple), Career Palace (second of the triple), and Migration Palace (the opposite palace) — these four together give a full cross-section of a person's life pattern.
How to implement? The engine already computed all twelve palace positions and each palace's major stars during chart calculation. Three Directions/Four Alignments needs no new computation — it's just looking up palaces by name: byName('Life'), byName('Wealth'), byName('Career'), byName('Migration'), each returning a star list, then feeding into the existing starEssence assembly logic.
The new code footprint is tiny: engine unchanged, interpretation logic unchanged, just four palace-name lookups and a render function. The descriptive text lives in build_ziwei_okf.py's SANFANG structure, generator dual-output:
ziwei-interpret.js's sanfang section (page lookup)
okf/ziwei/sanfang.md (knowledge base)
Empty palaces (no major star) display a "borrow from opposite palace" prompt, using the Migration Palace's star as a stand-in for an empty Life Palace — traditional practice, and clean graceful degradation.
Verification: took a 1990 birth date, confirmed Life=武曲/天相, Wealth=廉貞/天府, Career=紫微, Migration=破軍 against reference texts.
A reusable deep-reading pattern
Once Three Directions/Four Alignments landed, an unexpected payoff emerged: the "generator holds → dual output → page lookups → empty-palace degradation" pattern was fully validated and could be transplanted to other services.
Da Mo Yi Zhang Jing's Six Paths root nature and four-pillar interaction later both reuse this exact pattern. Every subsequent "add an interpretation layer to a divination service" operation became pattern application, not fresh design — add a generator data section, re-run, add a render function. Done.
What we learned
- Interpretation architecture defines the privacy boundary. Choosing path A isn't just a technical decision — it's a value judgment about what stays on-device and what doesn't.
- Combinatorial beats exhaustive. 26 entries covering the base interpretations for 168 combinations, traded for maintainability and OKF sync.
- Unify language codes at interface boundaries. Data using
zh, page using zh-TW — this silent mismatch is a chronic disease in multilingual systems.
- "The engine already computed it" is the right first question. Don't write a new algorithm for Three Directions/Four Alignments when the palace data is already in the chart object.
- Once a deep-reading pattern is validated, package it for reuse. First time is expensive; every subsequent service is template application.
The same question, applied to depth
The question this post answers: how deep can chart interpretation go without sending the user's birth data off their device?
Deeper than you'd expect. Combinatorial essence-domain reading, Three Directions/Four Alignments cross-palace analysis, a full bilingual interpretation section — all computed in your browser, gone when you close the tab, no server logs, no database. This isn't a feature limitation statement. It's an architecture choice on display.
Next post, a sharp turn — what does "prediction" mean in a lottery prediction tool? Time-seeded LCG, deterministic entertainment, and why "prediction" is an interesting lie in this context. Want to follow the series? Bookmark LocalPapa Notes.