A compatibility engine can be deterministic and still be dishonest. The obvious failure is a wrong calculation. The subtler failure is unjustified certainty: turning a blank birth time into noon, allowing a nickname to change a result, or comparing an Hour Branch that was never calculated.
This tutorial describes a test strategy for a two-chart pipeline. The domain example is BaZi, a traditional symbolic framework for reflection. It is not scientifically validated as a prediction method. The fixture below is fictional software test data, not a customer story. Its public evidence repository is brand-owned by Bazi Clarity, so it is implementation evidence—not independent validation or social proof.
The engineering goal is narrow: given two input records, preserve what is known, expose what is unknown, and make every comparison reproducible.
Define the integrity contract before the formula
Start with invariants, not an attractive compatibility percentage. A useful contract for this pipeline is:
- calculate Person A and Person B independently;
- use birth date, optional time, and resolved place data for each chart;
- never infer an Hour Pillar from a missing time;
- keep names outside every numeric and symbolic calculation;
- compare only stems, branches, and element data that exist;
- return observations and reflection prompts, not a verdict.
These rules define which outputs are allowed. They also produce strong negative tests. It is easy to assert that a result contains a Day Master. It is more valuable to assert that it contains no score, no assumed hour, and no relationship guarantee.
Calculate two charts before comparing either one
The safest architecture is a composition of three boundaries:
const leftChart = calculateNatal(normalize(leftInput));
const rightChart = calculateNatal(normalize(rightInput));
const comparison = compareKnownFacts(leftChart, rightChart);
calculateNatal should know nothing about the other person. compareKnownFacts should receive calculated chart objects, not raw form fields. That separation prevents Person A's city, time setting, or display name from leaking into Person B's chart.
A metamorphic test makes this visible: change only the left record and assert that the serialized right chart remains byte-for-byte equal. Then swap the input order and assert that each natal chart still matches its original owner. If either test fails, the comparison layer is contaminating chart construction.
Make unknown time an explicit state
An empty string is too easy to coerce into a convenient default. Represent time precision in the input contract:
const known = { time: "09:30", timePrecision: "exact" };
const unknown = { time: null, timePrecision: "unknown" };
An unknown birth time must produce no Hour Pillar. A compact serialized boundary is { label: "Hour", unknown: true }. It should survive calculation, caching, rendering, export, and later comparison.
The comparison code can filter unavailable pillars before building cross-chart pairs:
const available = (pillars) => pillars.filter((p) => !p.unknown);
for (const left of available(leftChart.pillars)) {
for (const right of available(rightChart.pillars)) {
inspectBranchPair(left, right);
}
}
Test both directions: left time unknown and right time unknown. Also test that an explicitly supplied 12:00 differs from unknown time. Noon may be a real user value; it must never double as a missing-data sentinel.
Prove that names cannot change the result
Names are display metadata, not calculation inputs or compatibility score ingredients. This is best protected with an invariance test rather than a code comment.
Create two requests with identical birth facts and different labels—full name, nickname, Unicode characters, and an empty label. Remove display-only fields from the response and assert deep equality across all variants. Run the same test on both sides of the pair.
Do not hash a whole form object if it contains a name. Build a canonical calculation payload from an allowlist of relevant fields. This keeps cache identity aligned with the actual model and prevents a renamed person from generating a supposedly different chart.
The public model also omits a compatibility percentage. That is a separate contract: assert that score is absent, rather than merely hiding it in the interface.
Treat birthplace as a resolution pipeline
A city label alone is not enough. It should resolve to an IANA time zone plus latitude and longitude. For a supplied clock time, the engine applies the historical UTC offset, calculates the disclosed true solar time correction, and reports a DST flag from offset comparisons. That flag is not a complete classification of every historical civil-time change.
Those values have boundaries. An IANA time zone represents civil-time history, not longitude. Coordinates support the meridian correction, but they do not reveal a missing clock time. The equation-of-time adjustment is date-dependent, and different BaZi schools may choose different solar-time conventions. Version and disclose the chosen profile instead of presenting it as universal.
Tests should include dates on both sides of a DST transition, places with the same zone but different longitudes, and times close to a two-hour branch boundary. A nearby-city change may leave the final pillar unchanged; the test should still prove that the intermediate resolution used the selected coordinates.
Compare only facts that survived calculation
Once both charts exist, the comparison layer can describe a limited set of relationships: the two Day Master elements, each chart's Five Element snapshot, and combinations, clashes, or harms among known branches.
The fictional regression pair uses a known time for Shanghai and an unknown time for New York. The right chart therefore contains Year, Month, and Day Pillars plus an unknown Hour marker. Known branches can still participate in comparisons. The missing right Hour Branch cannot.
That distinction matters when reading an interaction list. An interaction between the known left Hour Branch and the known right Year Branch is allowed. An interaction involving the right Hour position would be fabricated. Assert every returned interaction against the set of available pillar labels for its own side.
Element summaries should also remain separate. Combining two distributions into one relationship grade destroys provenance. Preserve the left and right raw contributions, seasonal weights, rounded percentages, strongest element, and weakest element as two inspectable objects.
Build one fixture that tells a complete story
A useful integration fixture stores the inputs, expected natal charts, expected comparison, method version, and source hashes together. It should be large enough to cross the important boundaries without pretending to be a representative human case.
For the mixed-time fixture, assert at least these facts:
assert.equal(result.leftChart.timeKnown, true);
assert.equal(result.rightChart.timeKnown, false);
assert.deepEqual(result.rightChart.pillars[3], {
label: "Hour",
unknown: true,
});
assert.equal("score" in result.comparison, false);
assert.match(result.comparison.confidence.notice, /provided data/i);
Then validate interaction provenance. Each referenced branch must equal the branch stored at the referenced Year, Month, Day, or known Hour position. This catches an easy snapshot-testing blind spot: a list can look stable while silently containing a value derived from a fallback hour.
Hash evidence, but understand what the hash proves
Canonical JSON plus SHA-256 can make silent fixture changes detectable. Sort object keys recursively, serialize the value, and compare its digest with a checked-in manifest. Recorded source hashes are provenance references, not independently reproducible unless the matching source and build procedure are public.
This shows that the package matches its checked-in manifest. Detecting changes relative to a reviewed release also requires an independently retained digest. It does not prove that a traditional interpretation is scientifically true, that every time-zone record is perfect, or that the software covers every BaZi school. Integrity and validity are different claims.
Run the static verifier in a clean checkout. It should recalculate package hashes, confirm the unknown-hour object, reject a score field, and scan for prohibited verdict language. It checks package consistency; it does not rerun the production engine or validate recorded source hashes.
Add negative vectors for input boundaries
Happy-path snapshots are not enough. Reject a city without a time zone, an invalid date, or a location without valid longitude. Treat ambiguous civil time according to an explicit policy. The current engine deliberately downgrades an "exact" flag with empty time to the explicit unknown-time state; test that behavior so it never becomes an invented hour. Rejecting the mismatch would be a versioned contract change.
Property-style tests add useful pressure. Changing names must leave the model unchanged. Repeating identical inputs must produce identical canonical output. Removing a known time may remove Hour-dependent findings but must not add any. No branch interaction may reference an unknown pillar. These relations remain valuable even when the expected Chinese calendar values differ under another disclosed school profile.
Keep interpretation outside the evidence claim
The deterministic layer can prove which inputs and rules produced which chart facts. The text layer may turn those facts into neutral prompts about support, boundaries, or repairing tension. It must not turn software reproducibility into a soulmate label, clinical judgment, or guaranteed outcome.
That boundary belongs in tests and interface copy. A paid or longer explanation can add context, but it cannot create missing birth data. Real-world compatibility still depends on behavior, consent, communication, safety, and circumstances that no natal calculation measures.
Review the published evidence package
The compatibility calculation evidence guide contains the fictional mixed-time vector, manifest, and verifier described here. You can also try the free two-chart comparison without supplying an email; birth time remains optional for either chart.
The durable lesson applies beyond BaZi: missing data is a fact about your data. Model it, preserve it, and test that no later layer upgrades uncertainty into certainty.
Disclosure: AI assistance was used to draft and edit this tutorial. Technical claims and links were checked against the brand-owned evidence package and its static consistency verifier before publication.
Top comments (0)