-
-
Notifications
You must be signed in to change notification settings - Fork 2
TRIPLICITY_BACKEND_STANDARD
The Moira triplicity backend is a sovereign datum-provider subsystem. Its definitions, layer boundaries, invariants, failure doctrine, and determinism rules are stated here and are frozen until explicitly superseded by a revision to this document.
This document reflects current implementation truth as of Phase 11 (75 passing
tests in tests/unit/test_triplicity.py). It does not describe aspirational
future capabilities.
A triplicity in Moira is:
A grouping of exactly three zodiac signs that share the same classical element (fire, earth, air, or water) and are governed by the same set of three planetary rulers under a given doctrinal authority.
| Element | Signs |
|---|---|
| Fire | Aries, Leo, Sagittarius |
| Earth | Taurus, Virgo, Capricorn |
| Air | Gemini, Libra, Aquarius |
| Water | Cancer, Scorpio, Pisces |
Each triplicity has exactly three rulers: a day ruler, a night ruler, and a participating (mixed-sect) ruler.
A triplicity doctrine in Moira is:
A named authority that prescribes the precise ruler assignments for each triplicity group. Different editions or traditions may assign different day/night/participating rulers; each is a distinct doctrine.
Currently only one doctrine is implemented:
| Member | Authority |
|---|---|
DOROTHEAN_PINGREE_1976 |
Dorotheus of Sidon, Carmen Astrologicum, ed. Pingree (Leipzig, 1976) |
The active ruler for a sign in a given sect context is:
day_rulerwhenis_day_chart is True;night_rulerotherwise.
The active ruler is the planet that holds full triplicity dignity for that sign under that sect context.
The participating ruler is:
The third ruler in the triplicity triple — neither the day ruler nor the night ruler. Its contribution to any scoring computation is governed explicitly by
ParticipatingRulerPolicyat each call site.
The participating ruler's dignitary weight is contested across doctrinal sources. Moira preserves the ambiguity explicitly rather than silently embedding a choice.
A triplicity score in Moira is:
The integer contribution awarded to one planet for occupying one sign, computed as:
primary_score(default 3) if the planet is the active rulerparticipating_score(default 1) if the planet is the participating ruler andParticipatingRulerPolicy.AWARD_REDUCEDis in effect0otherwise, including whenParticipatingRulerPolicy.IGNOREis used
Score values are bounded to {0, participating_score, primary_score}.
A TriplicityAssignment in Moira is:
An immutable, fully resolved record of one sign's triplicity assignment under one doctrine and one sect context. It carries day_ruler, night_ruler, participating_ruler, active_ruler, and the canonical 3-element signs tuple.
moira/triplicity.py is a utility / datum-provider module. It does
not orchestrate computations or call other Moira engines.
The internal structure follows the same phase model as the houses backend, but because this is a utility module, only four phases are applicable. The full phase table is:
Phase 1 — Truth preservation (TriplicityDoctrine, doctrine enum; explicit table binding)
Phase 2 — Classification (TriplicityElement, ParticipatingRulerPolicy)
Phase 3 — Inspectability (TriplicityAssignment.__post_init__, @property helpers)
Phase 4 — Policy (ParticipatingRulerPolicy; explicit call-site declaration)
Phase 5 — (N/A: no position logic)
Phase 6 — (N/A: no proximity computation)
Phase 7 — (N/A: no angularity)
Phase 8 — (N/A: no system comparison)
Phase 9 — (N/A: no chart-wide distribution)
Phase 10 — Subsystem hardening (invariant register, failure-behavior freeze)
Phase 11 — Architecture freeze (this document)
Phase 12 — Public API exposure (moira/__init__.py)
triplicity.py:
-
may read
moira.constants.SIGNSfor sign validation - may not call any Moira planet, chart, or dignity engine
- may not emit astronomical computations (positions, angles, times)
-
may not access chart context except through explicit
is_day_chartparameter -
may not mutate a
TriplicityAssignmentafter construction
| Member | Table variable | Triplicity group coverage |
|---|---|---|
DOROTHEAN_PINGREE_1976 |
_TRIPLICITY_RULERS_DOROTHEAN_PINGREE_1976 |
All 12 signs (4 triplicity groups) |
The _TABLES dict is the registry that maps TriplicityDoctrine members to
their ruler tables. A KeyError on _TABLES lookup is the authorised signal
for an unrecognised doctrine argument.
| Group | Day ruler | Night ruler | Participating ruler |
|---|---|---|---|
| Fire (Aries, Leo, Sagittarius) | Sun | Jupiter | Saturn |
| Earth (Taurus, Virgo, Capricorn) | Venus | Moon | Mars |
| Air (Gemini, Libra, Aquarius) | Saturn | Mercury | Jupiter |
| Water (Cancer, Scorpio, Pisces) | Mars | Venus | Moon |
Water triplicity scholarly commitment: The assignment of Mars as the
water-triplicity day ruler follows Pingree's 1976 critical edition of
Dorotheus. Some later redactions assign Mars only to mixed-sect contexts;
Moira preserves Pingree's edition as the canonical source for
DOROTHEAN_PINGREE_1976. See module docstring for extended provenance note.
| Concern | Delegated to | Convention |
|---|---|---|
| Sign name list | moira.constants.SIGNS |
Ordered list of 12 tropical sign name strings |
No other delegation. All other logic (tables, sign grouping, element lookup,
scoring) is self-contained within moira/triplicity.py.
All public names are declared in moira/triplicity.py and re-exported from
moira/__init__.py.
| Name | Members | Inherits |
|---|---|---|
TriplicityDoctrine |
DOROTHEAN_PINGREE_1976 |
StrEnum |
TriplicityElement |
FIRE, EARTH, AIR, WATER
|
StrEnum |
ParticipatingRulerPolicy |
IGNORE, AWARD_REDUCED
|
StrEnum |
| Vessel | Fields | Guards in __post_init__
|
|---|---|---|
TriplicityAssignment |
sign, doctrine, is_day_chart, day_ruler, night_ruler, participating_ruler, active_ruler, signs
|
5 (see §7) |
| Property | Returns | Derivation |
|---|---|---|
element |
TriplicityElement |
_SIGNS_TO_ELEMENT[frozenset(signs)] |
inactive_ruler |
str |
night_ruler if is_day_chart else day_ruler
|
has_participating_overlap |
bool |
participating_ruler == active_ruler |
| Function | Signature | Returns |
|---|---|---|
triplicity_assignment_for |
(sign, *, is_day_chart, doctrine=…) -> TriplicityAssignment |
Fully resolved assignment record |
triplicity_score |
(planet, sign, *, is_day_chart, doctrine=…, participating_policy=…, primary_score=3, participating_score=1) -> int |
Integer score ∈ {0, 1, 3} by default |
| Name | Purpose |
|---|---|
_TRIPLICITY_RULERS_DOROTHEAN_PINGREE_1976 |
Ruler table for DOROTHEAN_PINGREE_1976 doctrine |
_DOROTHEAN_PINGREE_SIGN_GROUPS |
Precomputed triple → signs tuple map |
_TABLES |
Registry: TriplicityDoctrine → ruler table
|
_SIGN_GROUPS |
Registry: TriplicityDoctrine → sign groups map
|
_SIGNS_TO_ELEMENT |
frozenset(signs) → TriplicityElement lookup |
| Condition | Behaviour |
|---|---|
sign not present in table for the given doctrine |
ValueError("Sign <sign> has no triplicity entry in doctrine <doctrine>") |
doctrine key not in _TABLES (raw string or unknown member) |
KeyError |
is_day_chart is not bool
|
TypeError("is_day_chart must be bool, …") via __post_init__
|
Any other __post_init__ guard violation |
ValueError or TypeError (see §7) |
| No partial output on failure | Guaranteed — construction is atomic |
| Condition | Behaviour |
|---|---|
sign not present in table |
Returns 0 silently — never raises |
planet not in table for the given sign |
Returns 0 silently — never raises |
doctrine key not in _TABLES
|
KeyError |
Invalid or empty string for planet
|
Returns 0 silently |
| Correct inputs | Returns exactly one of: primary_score, participating_score, or 0
|
The asymmetry between triplicity_assignment_for (raises on bad sign) and
triplicity_score (returns 0 on bad sign) is intentional. Assignment lookup
is an explicit resolution step where a missing sign is a programming error.
Score lookup is called from scoring loops where absence of rulership is the
expected answer for most planet/sign combinations.
Construction of TriplicityAssignment directly (outside triplicity_assignment_for)
is permitted for testing synthetic assignments. All five __post_init__ guards
are always enforced:
| Guard | Violation raises |
|---|---|
sign not in SIGNS |
ValueError |
doctrine not isinstance TriplicityDoctrine |
ValueError |
not isinstance(is_day_chart, bool) |
TypeError |
active_ruler != (day_ruler if is_day_chart else night_ruler) |
ValueError |
len(signs) != 3 |
ValueError |
sign not in signs |
ValueError |
| # | Invariant | Violation raises |
|---|---|---|
| T1 | sign in SIGNS |
ValueError |
| T2 | isinstance(doctrine, TriplicityDoctrine) |
ValueError |
| T3 | isinstance(is_day_chart, bool) |
TypeError |
| T4 | active_ruler == (day_ruler if is_day_chart else night_ruler) |
ValueError |
| T5 | len(signs) == 3 |
ValueError |
| T6 | sign in signs |
ValueError |
| # | Invariant |
|---|---|
| F1 | result.sign == sign |
| F2 | result.doctrine == doctrine |
| F3 | result.is_day_chart == is_day_chart |
| F4 |
result.active_ruler == result.day_ruler when is_day_chart is True
|
| F5 |
result.active_ruler == result.night_ruler when is_day_chart is False
|
| F6 | len(result.signs) == 3 |
| F7 | result.sign in result.signs |
| F8 | All signs in result.signs share the same day_ruler, night_ruler, and participating_ruler
|
| # | Invariant |
|---|---|
| S1 | Return value ∈ {0, participating_score, primary_score} |
| S2 | Return value is non-negative |
| S3 | At most one planet in CLASSIC_7 scores primary_score for any given sign/sect combination |
| S4 | Return is deterministic: same inputs always yield same output |
| # | Invariant |
|---|---|
| CL1 |
triplicity_score(a.active_ruler, sign, is_day_chart=is_day, participating_policy=IGNORE) == primary_score for all signs and sects |
| CL2 |
triplicity_score(a.inactive_ruler, sign, is_day_chart=is_day, participating_policy=IGNORE) == 0 for all signs and sects |
| CL3 |
triplicity_score(a.participating_ruler, sign, is_day_chart=is_day, participating_policy=AWARD_REDUCED) == participating_score when not a.has_participating_overlap
|
| CL4 |
triplicity_score(a.participating_ruler, sign, is_day_chart=is_day, participating_policy=IGNORE) == 0 when not a.has_participating_overlap
|
| Context | Rule |
|---|---|
TriplicityAssignment.signs tuple |
Order is determined by insertion order of _TRIPLICITY_RULERS_DOROTHEAN_PINGREE_1976 (Python dict, guaranteed insertion-ordered since Python 3.7) |
triplicity_assignment_for on equal input |
Identical output — no state, no randomness |
triplicity_score on equal input |
Identical output — no state, no randomness |
_SIGNS_TO_ELEMENT lookup |
Order-independent (keyed by frozenset) |
| Excluded concern | Notes |
|---|---|
| Planetary position computation | Belongs to moira.planets
|
| Chart assembly and sect determination | Belongs to higher-level engines |
| Almuten figuris or dignity totalling | Belongs to moira.dignities
|
| Relational intelligence between signs | Belongs to moira.aspects or moira.dignities
|
| Multi-doctrine lookup or comparison | Deferred — single doctrine is the current scope |
| Topical house triplicity rulers | Outside scope of this module |
| Astrological interpretation | Never in this backend |
Authoritative runtime: project .venv (Python 3.14, Windows).
All validation commands must be run as:
.venv\Scripts\python.exe -m pytest <target>
No test may be marked passing unless it passes in .venv with no
modifications to the test file.
| File | Phase(s) | Tests | Focus |
|---|---|---|---|
tests/unit/test_triplicity.py |
1–4, 10 | 75 | Full public surface contract |
Test class breakdown:
| Class | Count | Coverage |
|---|---|---|
TestTriplicityDoctrine |
3 | Enum is StrEnum; exactly one value; correct value string |
TestTriplicityElement |
3 | Four members; correct value strings; StrEnum identity |
TestParticipatingRulerPolicy |
3 | Two members; correct value strings; StrEnum identity |
TestTriplicityAssignmentFields |
9 | Field preservation, active_ruler sect logic, signs tuple, frozen identity |
TestTriplicityAssignmentGuards |
5 | All five __post_init__ paths |
TestTriplicityAssignmentProperties |
12 |
element, inactive_ruler, has_participating_overlap across all signs/sects |
TestTriplicityAssignmentFor |
8 | All 12 signs, all 4 element groups, sect switch, sibling-sign agreement |
TestTriplicityScore |
14 | Primary/participating/zero paths, both policies, custom weights, exhaustive non-negativity |
TestHardening |
18 | Determinism, cross-layer invariants CL1–CL4, failure contracts, misuse resistance |
| Total | 75 |
These values are hand-checked against Pingree 1976 and must not change without a doctrine revision.
| Sign | is_day_chart | active_ruler | inactive_ruler | participating_ruler | element |
|---|---|---|---|---|---|
| Aries | True | Sun | Jupiter | Saturn | fire |
| Leo | False | Jupiter | Sun | Saturn | fire |
| Sagittarius | True | Sun | Jupiter | Saturn | fire |
| Taurus | True | Venus | Moon | Mars | earth |
| Virgo | False | Moon | Venus | Mars | earth |
| Capricorn | True | Venus | Moon | Mars | earth |
| Gemini | True | Saturn | Mercury | Jupiter | air |
| Libra | False | Mercury | Saturn | Jupiter | air |
| Aquarius | True | Saturn | Mercury | Jupiter | air |
| Cancer | True | Mars | Venus | Moon | water |
| Scorpio | False | Venus | Mars | Moon | water |
| Pisces | True | Mars | Venus | Moon | water |
The assignment Cancer/Scorpio/Pisces → day: Mars, night: Venus, participating: Moon
is the authoritative DOROTHEAN_PINGREE_1976 value. Any change to this triple
requires a new doctrine member and must not alter the existing table entry.
| Planet | Sign | is_day_chart | Policy | Expected score |
|---|---|---|---|---|
| Sun | Aries | True | IGNORE | 3 |
| Sun | Aries | False | IGNORE | 0 |
| Jupiter | Aries | False | IGNORE | 3 |
| Jupiter | Aries | True | IGNORE | 0 |
| Saturn | Aries | True | AWARD_REDUCED | 1 |
| Saturn | Aries | True | IGNORE | 0 |
| Moon | Aries | True | AWARD_REDUCED | 0 |
| Mars | Cancer | True | IGNORE | 3 |
| Venus | Cancer | False | IGNORE | 3 |
| Mars | Cancer | False | IGNORE | 0 |
| Moon | Cancer | True | AWARD_REDUCED | 1 |
| Sun | Ophiuchus | True | AWARD_REDUCED | 0 |
| Uranus | Aries | True | AWARD_REDUCED | 0 |
Invariants CL1–CL4 are exercised exhaustively in TestHardening against
all 12 signs × 2 sect contexts (= 24 cases each). No pathological case
was found in the DOROTHEAN_PINGREE_1976 table: has_participating_overlap
is False for all 24 combinations.
| Limitation | Notes |
|---|---|
| Single doctrine | Only DOROTHEAN_PINGREE_1976 is implemented. The _TABLES registry is extensible. |
| No chart-position awareness | This module receives is_day_chart as a caller-declared parameter; it does not compute sect from planetary positions. |
signs tuple order is insertion-order |
The tuple order reflects _TRIPLICITY_RULERS_DOROTHEAN_PINGREE_1976 insertion order, not any canonical doctrinal ordering of signs within a triplicity group. |
| Relational intelligence belongs in dignity layer | Whether a planet's triplicity rulership contributes to its total essential dignity score is governed by moira.dignities, not this module. |
| No validation against external oracle | No machine-readable external authority for Dorothean triplicity tables has been identified; spot checks in §12.1 are hand-verified against Pingree 1976 directly. |