← Back to Engineering Blog

Conflict Resolution in Divination: Reconciling Astrological Lineages with Database Schemas

How we modeled varying historical traditions into a unified JSON structure without losing nuance.

When building software, we often seek a single source of truth. We normalize databases, enforce strict schemas, and write unit tests to ensure that A always equals A. But what happens when your domain involves centuries of historical texts, and the "truth" depends entirely on which 16th-century polymath you ask?

This was the exact challenge we faced while building SAGE, a hybrid geomantic oracle platform that unifies Western Geomancy and Indian Ramal traditions. We quickly discovered that historical mystics were terrible at standardizing their APIs.

In this post, we’ll explore how to model conflicting historical opinions into a clean, unified JSON structure without losing the nuance of individual traditions.

1. The Problem: When Sages Disagree

In geomancy, there are 16 foundational figures. Each figure has specific attributes: an element (Fire, Earth, Air, Water), a ruling planet, and a zodiac sign.

If you consult the classical texts of Gerard of Cremona, the figure Puer (Boy) is heavily associated with Fire and Mars. However, if you look at later Renaissance variations or specific Ramal traditions, elemental weights and planetary dignities might shift.

If we hardcode these associations in a rigid relational database table like this:

CREATE TABLE figures (
    id VARCHAR PRIMARY KEY,
    name VARCHAR,
    element VARCHAR,
    planet VARCHAR
);

We immediately run into a problem. If a user requests a reading using the traditional Western lineage, Puer might be Fire. If they use a different ruleset, it might behave differently. Forcing a single strict schema inherently erases historical nuance and alienates practitioners of specific lineages.

2. Modeling Lineages as Configurable JSON Constants

Instead of treating the attributes of a figure as absolute truths, we treat them as contextual configurations. We moved away from rigid table schemas and embraced flexible JSON document structures to model varying opinions.

We define a base registry of figures, but extract the attributes into lineage-specific configuration objects:

{
  "figures": {
    "puer": {
      "binary_signature": "1101",
      "lineages": {
        "agrippa_western": {
          "element": "fire",
          "planet": "mars",
          "zodiac": "aries"
        },
        "ramal_traditional": {
          "element": "air",
          "planet": "mars",
          "zodiac": "scorpio"
        }
      }
    }
  }
}

By decoupling the figure's core identity (its binary signature, 1101) from its astrological attributes, we allow the software to swap "lenses" dynamically based on the user's selected tradition.

3. Building a Fallback Lookup Registry

Storing every possible attribute for every figure across every obscure lineage is unmaintainable. Often, lineages agree on 90% of the data and only diverge on a few edge cases.

To solve this, we implemented a Fallback Lookup Registry using a Chain of Responsibility pattern.

When the SAGE engine evaluates a figure, it doesn't query the figure directly. It queries the registry with a specific context:

class FigureRegistry:
    def __init__(self, base_rules, overrides):
        self.base_rules = base_rules
        self.overrides = overrides

    def get_attribute(self, figure_id, attribute, lineage):
        # 1. Check if the specific lineage has an override
        if lineage in self.overrides and figure_id in self.overrides[lineage]:
            if attribute in self.overrides[lineage][figure_id]:
                return self.overrides[lineage][figure_id][attribute]
        
        # 2. Fall back to the accepted classical baseline
        return self.base_rules[figure_id].get(attribute)

# Usage
element = registry.get_attribute("puer", "element", lineage="ramal_traditional")

This pattern ensures that we only need to store the deltas (the conflicts) for new traditions, rather than duplicating the entire dataset. It keeps our JSON payloads lightweight and our domain logic incredibly clean.

4. Unified Database Collections for Custom Rulesets

SAGE runs on a NoSQL database (Firestore). When generating readings, we need to preserve the exact ruleset used at the time of the reading so that historical logs remain accurate even if we update the base JSON constants later.

We achieve this by flattening the resolved attributes at runtime and attaching a "ruleset snapshot" to the database document of the reading itself:

{
  "reading_id": "rdg_892347",
  "user_id": "usr_123",
  "tradition": "western_agrippa",
  "cast_figures": ["puer", "via"],
  "resolved_schema_version": "v1.2.0",
  "snapshots": {
    "puer": {
      "element": "fire",
      "planet": "mars"
    }
  }
}

By saving the resolved snapshot directly into the single unified collection, our frontend doesn't need to perform complex historical joins. The reading document is entirely self-contained. Whether it's a Ramal reading or a Western reading, the UI components simply render the data present in the snapshot.

💡 The Result

By modeling these conflicts through dynamic JSON configurations, fallback registries, and runtime snapshots, SAGE manages to respect the deep, conflicting nuances of historical texts without compromising on the stability of a modern software architecture.

Conclusion

Software engineering often forces us to view the world in binary terms. But human history, spirituality, and divination are messy. People disagree. Lineages fork.

In the end, the code doesn't force the sages to agree; it just gives them a well-structured room to argue in.

Ready to Experience the Oracle?

The engineering behind SAGE exists for one purpose: to give you the most precise, reliable geomantic reading possible. Try it for free.

✧ Get 11 Free Energy Tokens ✧