fengshui.engine audit

Errata v1

Defects we found in our own engine and product, published with impact, fix, and how we caught them.

Engine version 0.1.0 · all entries below are fixed; regression tests cover each one · last updated 2026-10-10.

Why publish this? A calculation tool's trustworthiness is not "we never had bugs" — every nontrivial engine has them. It is whether the bugs were findable (independent cross-checks, not self-review), found, fixed with a regression test, and disclosed. This page is the disclosure. Entries are ordered by severity.

E-01 · Day pillar flipped at 08:00 instead of midnight severity: critical

What: the original API took only Beijing wall-clock time (UTC+8 hardcoded in 8 places across 4 files). When the interface was extended to arbitrary time zones, the day-pillar flip was found tied to 08:00 UTC — Beijing midnight — so a local-civil-day pillar was computed against the wrong day boundary for anyone outside UTC+8: charts could show the previous day's pillar.

Impact: wrong day and hour pillars for all non-China time zones (15-hour worst case when local time was fed as Beijing time).

Fix: reworked to four_pillars_local: year/month pillars from absolute Lichun/jie instants, day pillar from the local civil day, hour from local time; UTC+8 hardcoding removed.

Found by: the time-zone rework itself — notably not by the (then complete) HKO lunar cross-check, because the lunar calendar and the day pillar are independent code paths. A dedicated regression test (day_pillar_flips_at_local_midnight_not_08_00) now pins it.

E-02 · Product could never boot in production severity: critical

What: the production config validator derived configuration keys from environment-variable names by string manipulation (STRIPE_SECRET_KEY → stripesecretKey), and all six derivations were wrong. The validator therefore concluded all six required variables were missing — even when all six were set — so production startup always threw.

Impact: the shipped product could not start in production at all. Every test was green, because the tests asserted the error message mentioned the variable names — which it did, for the wrong reason.

Fix: explicit mapping table (ENV_TO_CFG); the test rewritten to assert that a fully-configured production boot succeeds.

Found by: pre-launch rehearsal. The lesson — "a test that passes for the wrong reason" — is recorded in our engineering log and motivated the negative-injection test policy (every gate must be shown to fail on injected defects).

E-03 · Leap-month assembly: whole-month misplacement severity: critical

What: the leap-month assembly logic could assign a leap month to the wrong year: for the 1985 Spring Festival it produced lunar 1985-01-21 instead of 1985-02-20 (a full month early), and it computed 2033 as 12 months with no leap month instead of 13 months with leap month 11 (閏十一月) — the famous 2033 polemic case.

Impact: lunar dates off by a month in affected years.

Fix: rewrote the per-year leap assembly against GB/T 33661-2017 with the new-moon chain from the corrected E-05 anchor; fixed after two rounds of self-correction. Full 1901–2100 machine cross-check against the Hong Kong Observatory (2,474 month starts, 73/73 leap months) now covers this path.

E-04 · New-moon anchor was the mean new moon severity: major

What: implementing Meeus chapter 49, the anchor was taken from memory as "JDE 2443192.94102" for the Feb 1977 new moon. Measured 0.29 days off. The remembered value is the mean new moon for k=−283, not the true one; the correct k=0 corrected anchor is JDE 2451550.260. Worse: trusting the wrong anchor would have led to "correcting" a correct coefficient table and breaking it.

Impact: potential systematic half-day error in all new moons (and thus lunar months) if the anchor had been "fixed" the wrong way.

Fix: anchor taken from an independent implementation and pinned by a test that additionally asserts the periodic-correction term exceeds 0.05 days — so the class of error cannot silently return. Two real coefficient transcription errors were found and fixed in the same pass (Meeus 49.6: 0.0000047 → 0.0000074; the −0.009173·T² term applies to A1 only, not all 14 terms).

Found by: cross-validation of the anchor itself — the lesson recorded: anchors must also be cross-validated.

E-05 · Beijing time converted twice severity: major

What: the helper beijing_from_jd_ut already added the 8-hour offset; call sites subtracted another 8 hours, shifting every displayed lunar date 8 hours early — making a correct new-moon computation look like an off-by-one-day error, which initially masked E-04.

Impact: lunar dates displayed one day early for instants within 8 hours of midnight boundaries.

Fix: corrected at 3 call sites with explanatory comments; caught because the double-shift made the (correct) computation look wrong, triggering the investigation that found E-04.

E-06 · Subscription state never advanced severity: major (revenue)

What: current_period_end was never written to the database — a PostgreSQL type error surfaced only under the real database, while all 308 in-memory test assertions stayed green. In the same audit: the in-memory store lacked the "re-open on recurrence" behavior of the SQL store, and the append-only audit chain silently dropped 38 of 40 concurrent appends while remaining internally consistent.

Impact: entitlements could expire or persist incorrectly; audit trail incomplete under concurrency.

Fix: store-contract parity test (the same 47-step script drives both stores and diffs normalized results) plus a 40-way concurrent-append test on real PostgreSQL 16 with append-only triggers.

Why public: it is the concrete proof of our claim that green tests are a property of the test environment, not of the code — this layer ran for the first time and found three bugs the same day.

E-07 · Password lockout kept billing severity: major (users)

What: a user who forgot their password was permanently locked out while their subscription kept charging — no recovery path existed.

Fix: full password-reset flow with account-existence non-disclosure (identical responses and deliberately equalized timing via a dummy scrypt run), reset links bound to configuration domain only, single-use expiring tokens, and separate rate limits.

E-08 · Past-due grace period was unbounded by default severity: moderate (revenue)

What: the grace window for failed payments (status past_due) was effectively bounded only by a Stripe dashboard setting that could be changed without code review and would trip no test. Worse, the naive implementation would reset the grace timer on every dunning webhook — making "never expires" the actual behavior while every branch looked individually correct.

Fix: a server-enforced 14-day upper bound that does not reset on repeated past_due notifications; the dashboard setting demoted to a second line of defense.

Discovered-since-release (not defects)

Found something wrong? That is the point of this page. Every claim here is reproducible from the repository's acceptance harness.