Composite Method midpoint@1 — Specification (W-05)
Status: Implemented v1 convention · Task: W-05 · Updated: 2026-10-25 · Implementation: W-06 package.
A composite chart is a relationship chart: one derived chart built from two admitted reference charts. Two construction families exist and are not interchangeable:
- Midpoint (space) methods average the two charts’ ecliptic longitudes. No new astronomy is needed.
- Time/place methods (Davison) compute an averaged instant and location, then ask Composer for a real chart at that point.
midpoint@1 is the first family only. The second is a separate, unsupported variant
(§10).
1. Admitted variant
| Field | Value |
|---|---|
| Technique | composite |
| Version | 1 |
| Variant | midpoint |
| Rulebook entry | { "technique": "composite", "version": "1", "variant": "midpoint", "hash": "<payload hash>" } |
Rule payload (validated by validateCompositeRules):
{
"method": "midpoint",
"tieBreak": "forward-from-a",
"angles": "midpoint",
"houses": "midpoint-cusps"
}
The payload fixes the conventions. Bodies come from the profile; the aspect/orb policy is the
profile’s aspectPolicy with the motion override of §5. No field is optional: an
unrecognised value is INVALID_RULES, never a default.
2. Inputs and compatibility
| Input | Source | Use |
|---|---|---|
| Chart A | Admitted snapshot, host-supplied | Source points, angles, cusps |
| Chart B | Admitted snapshot, host-supplied | Source points, angles, cusps |
| Profile | Engine | Body set, zodiac, house system, aspect policy |
Injected GeometryPort |
WesternFeatureContext.upstream.geometry |
Optional contact evaluation |
Charts are admitted, not composed here: the engine (or a future chart-pair operation) supplies
both snapshots. This feature never calls ComposerPort.compose.
Required equality between A and B:
| Field | Rule | Failure |
|---|---|---|
context.zodiac (and ayanamsa when sidereal) |
Must be identical | PROFILE_INCOMPATIBLE |
context.positionOrigin |
Must be identical | PROFILE_INCOMPATIBLE |
context.houseSystem, when cusps are used |
Must be identical and non-null on both charts | FACTOR_UNAVAILABLE (houses) |
Different instant, houseObserver and positionObserver are legitimate — two people are born
at different times and places — and never fail. Different frame values do not matter either: the
composite carries no house-counting reference. Only the cusp ring constrains the two charts, and it
needs equal houseSystem values, not equal frames.
3. Construction
3.1 Angle helpers
All longitudes are degrees in [0, 360).
wrap360(x) = ((x % 360) + 360) % 360
wrap180(x) = r > 180 ? r - 360 : r, where r = wrap360(x) // range (-180, 180]
wrap180 is closed at +180 and open at -180. That single choice makes the opposite-point
tie-break in §3.2 deterministic.
3.2 Body midpoint
For a body present in both charts with longitudes a (chart A) and b (chart B):
d = wrap180(b - a)
m = wrap360(a + d / 2)
d is the signed shorter arc from A to B; m is its midpoint. The antipode wrap360(m + 180) is
never chosen, never used as a “house-friendly” alternative, and never selected to keep a planet in a
sign. At d = 180 exactly (opposite points) the tie is resolved forward from A:
a = 0, b = 180 -> m = 90
a = 270, b = 90 -> m = 0 (270 + 90 = 360)
This tie-break is the tieBreak: "forward-from-a" value; the other convention is a different
variant.
Bodies present in only one chart are omitted and reported as missing-bodies, in profile body
order. A body is never interpolated, borrowed from the other chart or filled from a previous request.
3.3 Derived angles
| Point | Rule |
|---|---|
| Ascendant | midpoint(ascA, ascB) by §3.2 |
| Midheaven | midpoint(mcA, mcB) by §3.2 |
All other angles (vertex, armc, co-ascendants, …) |
Absent — not derived by this method |
armc is equatorial right ascension, not a zodiacal longitude, and is excluded outright: averaging
it would produce a number that is neither an ARMC nor a longitude.
If either chart has no angles (angles === null), the composite angles are null with
missing-angles; the bodies still compose.
3.4 Derived house ring
When houses: "midpoint-cusps" and both charts carry cusps:
cusp_k = midpoint(cuspA_k, cuspB_k), k = 1..12
Properties that must be asserted in fixtures:
- the ring is derived, not a Binder house result; it must not be presented as a valid cusp set for the named house system;
- cusp 1 equals the derived ascendant whenever both source charts satisfy
cusp 1 === ascendant; - the ring is not sorted, rotated or normalised into monotone order. A midpoint ring may be non-monotone; that is a property of the method, not an error to repair;
- body-to-house placement is not computed here. A consumer may pass the composite
GeometryChart(derived cusps included) to the shared overlay port; Western code adds no placement algorithm.
With houses: "none" the ring is absent. With midpoint-cusps and a missing source ring the ring
is absent with FACTOR_UNAVAILABLE (houses) and the bodies still compose.
3.5 Speeds
Every composite point has speedLongitude: null. A composite chart is not a moving chart, and this
method defines no derivative; inventing one would fabricate applying/separating state. Contacts are
therefore evaluated with motion: "unknown".
4. Result
| Field | Meaning |
|---|---|
method |
{ id: 'midpoint', version: '1', tieBreak: 'forward-from-a' } |
sources |
{ a: SnapshotIdentity, b: SnapshotIdentity }, in request order |
bodies[] |
{ body, longitude, origin: DerivedPointOrigin } in profile order |
angles |
{ ascendant, midheaven } or null, each derived |
houses |
{ system, cusps, derived: true } or null |
contacts? |
Same-chart contacts of the composite chart (§5), when geometry is connected |
missing |
missing-bodies, missing-angles, missing-houses, missing-aspects |
origin for every point is:
{ "kind": "derived", "method": "composite.midpoint", "version": "1",
"sources": ["<A snapshot hash>", "<B snapshot hash>"] }
The result retains both source hashes and the method identity so the chart is replayable without re-reading either source.
5. Contacts
Composite contacts are same-chart contacts of the derived chart, not cross-chart contacts of A and B. Re-labelling A-to-B synastry contacts as composite aspects is a defect.
When geometry is connected:
policy = { ...profile.aspectPolicy, motion: 'unknown' }
contacts = geometry.evaluateChartAspects(compositeGeometry, policy)
compositeGeometryis aGeometryChartwithpointsbuilt from §3.2,origin.kind = 'derived'andcuspsfrom §3.4 when available.- The evaluated policy is recorded verbatim, including
motion: 'unknown'. - If geometry is absent,
contactsis omitted withmissing-aspects; the chart itself is still a successful result.
6. Composer facts and ports
| Need | Provided by | Absent behaviour |
|---|---|---|
| Two admitted snapshots (bodies, angles, cusps) | Host / engine chart-pair input | INSUFFICIENT_INPUT |
| Zodiac/ayanamsa equality | Snapshot contexts | PROFILE_INCOMPATIBLE |
GeometryPort |
WesternFeatureContext.upstream.geometry |
missing-aspects only |
ComposerPort |
Not required for midpoint |
— |
Declared requirements: snapshot: 'none', upstream: ['positions'],
optionalUpstream: ['geometry'], dependencies: [].
positions is required even though no astronomy is performed: without observed source longitudes
there is nothing to average. The gate is inspected for provenance, not for a new read. The cusp ring
comes from the supplied pair, not from the engine’s request snapshot, so composite does not
raise the snapshot requirement to houses.
The pair is routed through WesternEngine.operation; WesternEngine.compose can acquire each
source first. No unrelated request snapshot is composed for this operation.
7. Failures
| Case | Behaviour |
|---|---|
| Fewer than two snapshots | INVALID_REQUEST |
| Different zodiac/ayanamsa or position origin | PROFILE_INCOMPATIBLE |
Unknown method, tieBreak, angles or houses value |
INVALID_RULES |
| Source chart lacks a profile body | Omit that body, missing-bodies |
| Source chart lacks angles | angles: null, missing-angles |
| Cusp ring requested, a source ring missing or systems differ | houses: null, FACTOR_UNAVAILABLE (houses) |
| Geometry not connected | contacts omitted, missing-aspects |
time-place requested |
LAYER_UNSUPPORTED, never approximated by midpoints |
8. Worked examples
All values are exact for the stated inputs.
| Id | Input | Expected |
|---|---|---|
| C-F1 | A 10°, B 50° | 30° |
| C-F2 | A 350°, B 10° | 0° (wrap through 0) |
| C-F3 | A 359°, B 1° | 0° |
| C-F4 | A 100°, B 100° | 100° (identical charts are their own composite) |
| C-F5 | A 0°, B 180° | 90° — tie forward from A |
| C-F6 | A 270°, B 90° | 0° — tie forward from A; the antipode 180° is not chosen |
| C-F7 | A 10°, B 190° | 100° — tie forward from A |
| C-F8 | A has Sun/Moon, B has Sun only | Sun composed; Moon missing-bodies |
| C-F9 | A asc 15°, mc 105°; B asc 45°, mc 135° | asc 30°, mc 120° |
| C-F10 | A cusp 1 = 15°, B cusp 1 = 45° | derived cusp 1 = 30° = derived ascendant |
| C-F11 | Either chart position-only | bodies compose; houses: null, missing-houses |
| C-F12 | Geometry connected, derived Sun 30°, derived Moon 150° | trine contact (120°), phase: 'unknown', policy records motion: 'unknown' |
| C-F13 | A 10°, B 50°, then swapped | 30° both ways; sources order follows the request |
| C-F14 | A 0°, B 180°, then swapped | 90° then 270° — the tie-break is deliberately asymmetric |
C-F13 is the required symmetry check: for every pair except exact opposition,
midpoint(a, b) = midpoint(b, a). C-F14 records the one exception. A tie must break somewhere, and
“forward from A” makes the swap observable; a hidden symmetry rule (for example “prefer the
midpoint in the same sign as A”) is a different variant and must not be added to make the swap look
stable.
9. Prerequisites before W-06
The implementation and coordinator integration now satisfy these runtime seams; convention sources remain the responsibility of each supplied production rulebook. See the package tests.
| Id | Prerequisite | Owner |
|---|---|---|
| P1 | A chart-pair input (two admitted snapshots) is expressible by the engine request or a dedicated operation | W-11/W-04 owner (see techniques index) |
| P2 | A reviewed source for the midpoint rule and the forward tie-break recorded in the manifest | Composite reviewer |
| P3 | composite.midpoint@1 payload schema and validator registered |
W-06 |
10. Unsupported variants
| Variant | Why unsupported |
|---|---|
time-place (Davison) |
Needs an averaged instant, a great-circle midpoint location and a real Composer chart there. Averaging longitudes is not a degraded version of it. |
house-ring-recomputed |
Recomputes cusps from the derived MC and a latitude; no Composer operation exposes that construction. |
sun-moon-only, angle-only |
Different point sets with different semantics; not a parameter of this method. |
| Interpretation, chart drawing, relationship scoring | Downstream products. |
The method is never chosen by the engine: the profile elects composite@1/midpoint, and an
unregistered or unreviewed method is LAYER_UNSUPPORTED.