Astro Affinity Brief

Astro affinity — what was built, how it was tested, what it may claim

Audience: an analyst reviewing the method and the evidence. No code knowledge assumed. Date: 2026-09-22. Every figure below was measured, not estimated; where something is an estimate or a judgement it says so.


1. What it is

A scoring engine that answers one question: do two astrological factors express the same underlying archetypal pattern?

It decomposes any chart factor into "genes" — the archetypal functions, qualities and processes it is made of — and scores two factors by how much of that material they share.

The point is that it sees past the form to the content:

Moon Opposition Plutoan aspect between two planets
Moon in 8th Housea placement naming neither Pluto nor an aspect

These score 100% affinity, because the 8th house is ruled by Pluto, so both say the Moon meeting Pluto — once by angle, once by rulership. No text matching could find that.

What the score does NOT claim

It is an archetypal match, not interchangeable meaning. Venus square Saturn and Venus trine Saturn have the same underlying Venus–Saturn ingredients, and they do not mean the same thing. The aspect angle describes how those ingredients relate; affinity measures only which ingredients are present. The two score 100% against each other for that reason. The angle is a separate dimension that belongs alongside this number, never folded into it.

This distinction was the single most-repeated correction from external review, and the wording was changed at the source because of it.


2. Two operations, one engine

Outward — "who else has this?" Scored against the full 22,693-component corpus at an 85% cut.

What is validated here is narrower than "finding similar people". The evidence supports "we can identify shared archetypal patterns between two charts". It does not yet support "we can identify astrologically similar people" — that additionally requires knowing how COMMON a shared pattern is, and no population frequency has been measured. One natal aspect in six is generational (§4). Until rarity weighting exists, two strangers can share a factor and it can mean nothing. Treat person-matching as designed and costed, not validated.

Inward — "what else in my own chart is like this?" Scored against the ~740 factors of one person's chart at a 70% cut.

The cut differs because the cut is a property of the candidate pool, not of the engine. 85 was calibrated against 22,000 corpus components where a loose threshold admits thousands of strangers. A chart is 740 factors that are already all about this one person, so the same number means something different. Using 85 inside a chart leaves one query in eight returning nothing at all.


3. Evidence

3.1 Calibration — where 85 came from

134,061 scored pairs from 20 queries stratified across aspects, houses, signs, figures and all six chart layers. Each candidate was labelled by whether it genuinely carries the query's signature (function-set containment either way).

cutpoolgenuinely shares the signatureshares nothing
85%1,08796%4%
80%2,16186%14%
75%4,04871%22%
70%7,37650%41%

The decisive column is the last one. At 70, two in five matches shared no archetypal function with the query at all. Per-route precision at 85 is 94–99% for aspects, houses and signs.

3.2 Calibration — where 70 came from, for the intra-chart case

403 real query factors swept across four real charts.

cutavg poolempty poolsstructural match rate
851.950 / 40397%
7010.118 / 40394%

The structural match rate barely moves while the pool grows five-fold.

"Structural match rate" is not quality. It means the returned factor shared a body or the whole signature with the query — a structural test the engine can run on itself, not a human judgement. Nobody should quote 94% as "94% of recommendations were judged good". See §6.4.

3.3 Discrimination — a fixed probe set

21 probes: pairs that ARE the same pattern reached by different routes, pairs that are NOT, and cross-layer pairs. Tracked through every change.

at the startfinal
same-signature pairs56–100 (spread 44)93–100 (spread 7)
separation margin2137

Layer ordering also behaves: same-layer 92–93% > same-family 84–86% > cross-family 77–81%.

3.4 Coverage — 20 item types

Natal aspects with planets, angles, nodes and asteroids; bare sign and bare house placements; angle-in-sign; a dignified placement; transit aspect, house and sign; progressed aspect and sign; draconic aspect and house; synastry aspect and house; composite aspect; chart figure. 20 of 20 returned the astrologically correct relatives.

3.5 Live validation

Four real charts pulled from the production chart API (Buenos Aires 1974, Tokyo 1991, Cairo 1966, Vancouver 2001) across four layers each — 16 API calls, 718–750 factors per person. A random factor from each was scored against that person's own chart. Every returned row was astrologically defensible; the failures found were about pool size, not correctness, and led to the 70 cut above.

3.6 Independent review

Three external audits, two of them on stratified random samples the engine's author did not choose — specifically to remove selection bias. The third audit found no systemic scoring flaw and recommended stopping.

Substantive changes the audits caused:

  1. Repeated functions were being collapsed. "Sun in Leo" — the Sun in the Sun's own sign — reduced to a single "Self" and so matched anything Sun-flavoured at 100%. Reinforcement now survives: the signature reads Self ×2 and the pool fell from 46 to 1, the survivor being the Sun in its own house.
  2. A sign's traditional co-ruler was being treated as a full function, which contradicted the engine's own flagship example.
  3. A missing defining function cost almost nothing. "Neptune in Pisces" scored 99% against "Jupiter in Pisces" while carrying no Jupiter. It now scores 70 and falls below the cut.
  4. Same-ruler-different-domain was overstated. "Vertex in 3rd" and "Vertex in 6th" have identical genes (both houses are Mercury-ruled) and scored 99%, though the life areas differ completely. Now 91 — while a genuine reciprocal inversion (Venus in Mercury's house against Mercury in Venus's house) correctly keeps its 100.
  5. Nine chart points decomposed to nothing at all — the Ascendant, Midheaven, Descendant, IC, Vertex, Part of Fortune, Eris, Pholus and Earth had no archetypal function, so anything built on them matched everything. This also explains why aliases like "True Node" behaved oddly: the decomposer never recognised them.

4. Known limits — measured, and stated so they are not rediscovered


5. Cost

Engine build (all 22,693 components decomposed)1.1 s, 73 MB, once at boot
Admit one person's 740-factor chart23 ms
Answer one query against that chart18 ms
Full pairwise matrix of a chart, two-stage68 ms
Person-match index~119 keys/person, ~2 KB; ~917 MB across 467,188 users

Nothing is stored for the intra-chart read. The person-match index is the only storage, and it does not exist yet.


6. What an analyst might usefully challenge

  1. Is "archetypal match" the right unit at all? The engine asserts that a planet's function (Moon = Feeling, Pluto = Power) is the atom of meaning, and that a sign or house contributes its ruler's function. That is a defensible reading of the tradition, not the only one.
  2. The angle is excluded from the score. A square and a trine between the same two planets are 100% affine. Defensible for "same pattern", wrong for "same meaning" — and the product language has to carry that distinction every time it shows a number.
  3. The function words for nine points are proposals, not tradition — Ascendant = Persona, MC = Vocation, Eris = Discord, Earth = Embodiment and so on. They are one table and easily changed, but they now materially affect scores.
  4. Human agreement has not yet been measured. The 94% figure in §3.2 is a structural match rate — the share of returned matches that shared a body or the whole signature with the query, a test the engine applies to itself. It is not a quality judgement and it is not evidence that anybody found the results good. No astrologer has rated a sample blind. That is the strongest validation still missing, and §7 is the instrument for closing it.
  5. The generational problem has no measured fix yet. We know one aspect in six is shared with a whole cohort; we have not measured the actual population frequency of any key, because the index does not exist.

7. The blind validation set — built, not yet run

§6.4 names the missing validation: no astrologer has judged a sample blind. The instrument now exists so that can be done.

Sampling. Drawn from real engine output across four score bands — 95–100, 85–94, 75–84, 60–74 — 50 per band. Within each band half the pairs are cross-route: one side an aspect and the other a placement. That was a deliberate correction to the first draft, where the sample mirrored the corpus and 76% of pairs were aspect-to-aspect. Two aspects sharing a planet pair look alike to anybody; the claim that actually needs a human is the cross-route one, where the engine says a placement and an aspect are the same pattern. No query contributes more than two pairs. Reproducible from seed 8812.

Report eight cells, never one number. Four bands × two route types. Collapsing them hides the only two failures worth distinguishing.

score bandsame-route "yes"cross-route "yes"
95–100
85–94
75–84
60–74

The reading is committed in advance, before any judgement is seen:

With more than one judge, inter-rater agreement bounds the whole exercise: a construct astrologers disagree about cannot be measured more finely than they agree, so compute that first and treat it as the ceiling on any precision claim.

node scripts/symbolism/_blind-score.js <completed-sheet.md> produces exactly that table from a filled-in sheet. It was written before any ratings existed, so the analysis cannot be shaped by the result.


8. Reproducing any of it

node scripts/symbolism/relate.js --affinity "Moon in 8th House" --cat "Planets in Houses"
node scripts/symbolism/relate.js --affinity "Venus Square Saturn" --cat "Planet Aspects" --cut 70
node scripts/symbolism/relate.js --match    "Moon Opposition Pluto" --cat "Planet Aspects"

The engine is scripts/symbolism/affinity.js and scripts/symbolism/relevance-core.js; the reasoning behind every threshold is in the code comments and the commit messages, with the measurement that produced it. Design and API notes: FEATURE-52 in app_issues, with the HD/I Ching audit in FEATURE-82.


9. Human Design / Gene Keys — audit of the proposed plan

Added 2026-10-05. Asked because nothing HD-flavoured appears as a facet or a connection, and because an external plan proposed building a three-level HD layer. Every figure below was measured against the live database on 2026-10-04.

9.1 What exists

substratecomponentsstatus
I Ching Hexagrams64present, linked from astro
I Ching Lines384present, nothing links to them from astro
Centers9present, linked from astro
Circuitry3present, no incoming links at all
Gene Keys0does not exist — no gene key, manifestor, projector, reflector or incarnation-cross content anywhere

9.2 Four assumptions in the plan that are wrong

1. There is no five-category Matrix layer. The plan keyed everything to Purpose / Harmony / Love / Abundance / Evolution. The product uses 27 facets — Path & Career, Ambition & Drive, Vision & Awareness, Creativity & Imagination, Intimacy & Romance, Nurturing & Fulfillment, Growth & Wealth and twenty more — held in 22,159 authored rows and in the Flutter report model. Re-cutting to five would orphan all of them. The plan's worked example also named Creative Expression and Love & Romance; neither is a real facet.

2. "Virtues" does not exist, and its proposed replacements are not linkable. Principles (320 components), Characteristics (211) and Keywords (160) exist as component categories but have zero rows targeting them. Nothing in the corpus points at them, so they cannot be rendered under a facet without an authoring project first.

3. Three of the named link sources are empty from astro.

I Ching Lines    126 target rows — all 126 from I Ching Hexagrams, 0 from astro
Decans           681 target rows — Sabian 440, Tarot Minor 104, hexagrams 101, signs 36, 0 from astro
Circuitry          0 target rows

A UI section for any of the three renders blank for every user.

4. Gates are near-absent on aspects. Hexagram links reach placements but not aspects: Planets in Signs 418 and Planets in Houses 283, against Transit Aspects 210 (of 5,399 components) and Synastry Aspects 213 (of 3,143). Aspects are the bulk of a chart, so a gate appears on roughly one aspect in twenty. Authoring 64 gate signatures fixes gate identity; it does not fix this. Aspect-to-gate links are a separate generation job, and the plan collapsed the two.

9.3 What the audit found already built

Facet attribution is effectively complete — every astro category sits at 99–100%:

Planets in Signs      359/359   100%     Transit Aspects      5333/5399   99%
Planets in Houses     384/387    99%     Progressed Aspects   1114/1130   99%
Planet Aspects       2684/2703   99%     Draconic Aspects     2853/2860  100%
Synastry Aspects     3125/3143   99%     Composite Aspects    1807/1808  100%

with all eight in-house and in-sign categories at 100%. "Which of my factors drove Path & Career" is therefore a query against rows that already exist, not an authoring project.

And the transit path already computes it. The report builder derives a score per facet, a harmonious-versus-challenging tone count, and a capped list of contributing events, from a facet field the chart engine returns on every transit and forecast row. That explanation layer exists today and is consumed only by the AI report payload — the user never sees it.

Correspondence links are real, counted as rows whose source is an astro category:

Tarot Major Arcana   13,780 / 17,372      Chakras            14,183 / 17,621
Tarot Minor Arcana    9,998 / 12,839      Centers            10,642 / 13,235
Tarot Court Cards        436 /  1,233     I Ching Hexagrams   1,397 /  3,478

Tarot Minor Arcana is well covered, which happens to support the plan's own worked example. Court Cards are thin.

But affinity is structurally blind to all of it. The layer-family gate drops Hexagrams, Lines, Centers and Circuitry because none of them belongs to a chart layer. In the affinity scope Hexagram 1 decomposes to Individual, Power, Will, Fixed, Water — genes borrowed from the astro components that link to it, not an authored identity — and Hexagram 1 Line 5 decomposes to nothing, so all 384 lines are zeroed by the no-signature guard.

9.4 The resulting rule

Use HD and I Ching as a surfaced correspondence layer where it already exists. Do not make it a scoring or affinity layer until gates have authored signatures and aspect coverage.

Build now: the 27-facet explanation layer, with a Symbolic Links block — never "Gate Themes", since gates are present on about 5% of aspects and must not name the section — rendering only the sections that have rows. Label honestly: I Ching Gate, Energy Center, Archetypal Gate, Symbolic Center, never "Human Design Analysis", "Your Gene Keys" or "Bodygraph", because the corpus does not support the claim.

Do not build: Gene Keys, Lines, Circuitry, Decans, HD affinity, HD relationship mechanics.

Filed as FEATURE-82, with S-a (explanation layer) and S-b (labelling) as the shippable half, and S-c (64 gate signatures) / S-d (aspect-to-gate links) as the later, separate unlocks.


10. Human Design type — computable today, and already validated once

Asked 2026-10-05: could we do something similar to Human Design's Projector / Generator labels? Nothing similar is needed. The real type is computable from what we already hold.

10.1 The bodygraph was in the corpus

Measured 2026-10-05; nothing was authored for this.

whatwherestate
64 gates with a centre and channel partnersI Ching Hexagramscomplete
gates per centreThroat 11, Sacral 9, Root 9, G 8, Spleen 7, Solar Plexus 7, Ajna 6, Heart 4, Head 3exactly the real HD distribution
channels36 distinct, all reciprocal0 integrity problems; real HD has 36
gate to signagrees with the canonical rave mandala64/64
the 9 centresCenterscomplete, with descriptions

Only the arithmetic was missing, and that is now scripts/symbolism/human-design.js.

10.2 The two things that were not in the database

The wheel offset. The stored sign is sign-resolution, so it pins the wheel's ORDER but not its OFFSET. Used the published constant — gate 41 opens at 02°00′00″ Aquarius, so a gate boundary falls at −1.75° and gate 25 spans 28°15′ Pisces to 03°52′30″ Aries. The wheel mode re-derives all 64 signs from that constant and checks them against the database, so a wrong offset cannot pass silently. Independent spot check: a Sun at 19°00′ Scorpio resolves to gate 43 line 1, and published gate tables open gate 43 at 18°52′30″ Scorpio.

The Design set. A bodygraph has two activation sets — Personality (birth) and Design (the moment the Sun was 88° of arc earlier, about 89 days before birth). Design is half the activations, so it is not optional. Newton iteration on the Sun's 0.9856°/day converges in two engine calls; observed deltas 0.30° then 0.008°. Total cost: 3 chart calls per person, once, static for life.

10.3 Validation against the population split

Nothing in the implementation was tuned toward this: the wheel, the offset and the channel table were all fixed before any type was computed. 120 real births, Personality + Design, 0 failures.

typeourspublisheddelta
Generator36%37%−1
Manifesting Generator35%33%+2
Projector18%20%−2
Manifestor11%9%+2
Reflector0%1%−1

Average 4.7 defined centres of 9 and 19.5 activated gates of 26. Every type lands within two points.

10.4 The two defects the validation caught, which nothing else would have

This table was run three times, and each run found a fault invisible in a finished type label.

13% Reflectors against a published 1%. The chart engine returns the north node as True Node, so reading it by its Human Design name dropped one activation per set; and Chiron was included, though it is not one of the 13 activating bodies.

A −9 on Manifesting Generators that the second run could not explain. It had a cause: the chart engine returns Earth at 0.00° on every chart — 214 of 214, one distinct value across the whole sample. Earth is always exactly opposite the Sun, so the correct value is Sun + 180; trusting the engine put every person's Earth in gate 25 and corrupted 2 of the 26 activations. Earth is now derived and never read, and the maximum delta fell from 9 points to 2. An earlier draft of this section attributed that −9 to sample composition and to the published split being soft. That was wrong and is withdrawn.

The second fault is not confined to Human Design. 23 components are named Earth … — 12 Planets in Houses, 6 Synastry Aspects, 5 Transit Aspects — and they took 172 views in the 9-day window. Every one of those readings is selected from a longitude that is zero for everybody, so the house shown is the house of 0° Aries. Filed separately as ISSUE-571; the fix belongs in the engine, because the app reads it directly.

10.5 What this does and does not license

Computed and defensible: type, inner authority, profile lines, defined and open centres, defined channels, activated gates — plus, for the first time, a per-person route into the 384 I Ching Lines components, which §9 showed have zero incoming links from astro.

Not supported: a commercial Human Design product. No gate has authored content of its own (a gate's affinity-scope genes are borrowed from the astro components that link to it), and Gene Keys is not in the corpus at all. Ship the computed structure under honest labels, not a "Human Design Analysis".

Filed as FEATURE-83.

10.6 The other three loops, measured

The individual type is one of four loops a Human Design product normally has. The other three were checked rather than assumed, and none of them needs new data.

Relationships. The four standard HD connection types — electromagnetic, companionship, dominance, compromise — compute from the same 36-channel table, with nothing added. 20 bodygraphs, all 190 pairs scored:

pairs with ZERO connections   0
connections per pair          min 4   median 11   max 18   mean 10.8
mean per pair                 electromagnetic 3.28   dominance 3.59
                              compromise 3.45        companionship 0.43

No pair comes back empty, which is the opposite of the intra-chart affinity problem in §3.2. Companionship is the rare one, so it carries the most signal. 126,454 of 420,093 accounts (30.1%) already hold a second person with full birth data, and there are 1,005,915 HD-computable people in the database in total.

Timing. gateOf() takes any longitude, so the transit positions the app already computes are gates for free — no extra call, no extra data.

Depth. Covered in §9 and §10.5: the 384 line documents exist, are QA-passed, and have never been viewed.

So of the four loops, three are arithmetic over data already held. The costed one is the individual bodygraph, at 3 engine calls per person, once.

10.7 What the bodygraph should be keyed to

The cost in §10.2 is 3 engine calls per bodygraph. What that multiplies by is a design decision, and it was measured rather than assumed.

Key it to the birth identity, not the account. Across all HD-computable person rows:

person rows                 1,026,089
unique birth identities       902,913     (year/month/day/time/timezone/lat/lng)
repeats                          12.0%
engine calls, per row         3,078,267
engine calls, per identity    2,708,739
avoided                         369,528

The repeats are shared between accounts, not within them. An early reading of this — that the duplicates were one account re-saving the same person — was wrong. 1/2/1980 12:00 PM appears in 1,414 separate accounts, 14/6/1946 10:54 AM in 307, the 4/7/1776 USA chart in 205; only 11,153 duplicate rows sit inside a single account. So a shared cache earns its keep through genuine cross-account reuse of public figures and defaults. (1/2/1980 12:00 PM at a noon time across 1,414 accounts looks like a placeholder rather than a person, and is worth a separate look.)

And it must be invalidated when birth data is repaired. users.data.big3 is the existing precedent for a per-person cached chart derivative, and the stale-cache classifier in services/chartMath.js exists precisely because timezone and location get corrected after the fact. A bodygraph cached without the same invalidation will outlive the chart it was computed from.

10.8 Why the population split is not enough, measured

§10.3 is a distribution test. It has caught three real faults, but it is blind to any error that moves everybody the same way — and the room for one of those was measured rather than assumed.

The corpus agreeing 64/64 does not pin the wheel. The stored gate→sign table scores 64/64 for every offset between 0.940° and 2.810° — a window 1.870° wide, a third of a gate and almost exactly two line-widths. The published value in use, 1.75°, sits inside it with 0.81° below and 1.06° above. Across 2,354 real activations, moving the offset from one edge of that window to the other:

changes the GATE for    776 of 2,354 activations   32.97%
changes the LINE for  1,578 of 2,354               67.03%

So a third of everyone's gates could be wrong while every structural check in this repo still passes and the population split stays exactly where it is, because a uniform error does not move a distribution. Only a chart whose answer is published outside this codebase can catch it.

The gate. The test file holds 29 passing checks: gate and line boundaries rather than midpoints (midpoints survive a wrong offset), the full 384-cell grid, the type and authority truth table including motor-to-throat reachability through an intermediate centre, the four connection types, and regressions locking the three faults already found. The published-chart section is deliberately empty and fails under --ship. The default run stays green, because the repo's runner auto-discovers every test and a permanently red suite would destroy the signal for everyone else; the gate is on shipping, not on the suite.

Nothing Human Design may ship until --ship passes.