Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions vessel/docs/ASTROLOGY_API_CLIENT.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,63 @@
# Astrology API HTTP Client Guide

## Lens Rotation Doctrine

In this architecture, **Sidereal is not a separate endpoint**—it is a parameter passed to the same core calculation endpoints. This mirrors the canonical principle: the sky doesn't change, the instrument simply applies a different mathematical offset.

When calling standard endpoints (like `POST /api/v3/charts/natal` or `POST /api/v3/data/positions`), the URL remains identical. The coder injects a configuration flag—specifically an **ayanamsa** value (the mathematical offset between the Tropical and Sidereal zodiacs)—into the request body.

### Why This Matters

If Sidereal were a completely different endpoint, we would risk maintaining two separate calculation pipelines, introducing mathematical drift. By using the same endpoint and toggling the `ayanamsa` parameter, the system looks at the exact same Swiss Ephemeris data, just through a different lens.

### Payload Examples

**Tropical Payload (The Weather):**
```json
{
"year": 2026,
"month": 6,
"day": 28,
"hour": 18,
"minute": 5,
"latitude": 30.24,
"longitude": -85.64,
"house_system": "P",
"zodiac_type": "tropical"
}
```

**Sidereal Payload (The Bones):**
```json
{
"year": 2026,
"month": 6,
"day": 28,
"hour": 18,
"minute": 5,
"latitude": 30.24,
"longitude": -85.64,
"house_system": "P",
"zodiac_type": "sidereal",
"ayanamsa": 1
}
```

**Critical:** When `zodiac_type` is set to `"sidereal"`, the `ayanamsa` parameter **must** be explicitly defined. The proxy automatically defaults to `ayanamsa: 1` (Lahiri/Chitra Paksha) if not specified, as this is the most astronomically stable and widely accepted offset. Arbitrary offsets are prohibited—deterministic geometry is required.

### Supported Ayanamsa Values

| Value | Name | Tradition |
|-------|------|-----------|
| 1 | Lahiri (Chitra Paksha) | Indian/Vedic (default) |
| 2 | Raman | Indian/Vedic |
| 3 | Fagan-Bradley | Western |
| 4 | Krishnamurti | Indian/Vedic |
| 5 | Ushashashi | Indian/Vedic |
| 6-35 | Various | Indian, Western, Babylonian, Other |

See the upstream API documentation for the complete list of 35 supported ayanamsa values.

## Recommended HTTP Client: `fetch`

For the Astrology API integration in this project, we recommend using the **native `fetch` API**. This is the standard approach already established throughout the Shipyard codebase.
Expand Down
9 changes: 9 additions & 0 deletions vessel/sherlog-velocity/data/active-session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"feature": "Raven Dynamic Driver Licensing",
"type": "implementation",
"startTime": "2026-06-28T22:59:29.332Z",
"notes": [],
"branch": "main",
"startHead": "5346f4769f19ef67bb470ec6822406660d926c03",
"cwd": "."
}
1 change: 1 addition & 0 deletions vessel/src/app/api/raven-chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@
let subjectProfile = parsedSubjectProfile;
let stagedProfiles = parsedStagedProfiles;
let mode = parsedMode;
let enrichedProfilesForVault: Record<string, any> = {};

Check failure on line 762 in vessel/src/app/api/raven-chat/route.ts

View workflow job for this annotation

GitHub Actions / Advisory Lint Check

'enrichedProfilesForVault' is never reassigned. Use 'const' instead

Check failure on line 762 in vessel/src/app/api/raven-chat/route.ts

View workflow job for this annotation

GitHub Actions / Advisory Lint Check

'enrichedProfilesForVault' is never reassigned. Use 'const' instead

// ── LIFE STATUS RECONCILIATION ──
const lifeStatusSignal = detectLifeStatusSignal(message);
Expand Down Expand Up @@ -4729,6 +4729,7 @@
counterpartProfile,
relationalSharedStormReport,
polyadicPerSubjectContexts,
questionCategory: resolvedCategory,
});

if (turnReadoutArtifact && turnReadoutArtifact.artifactKind === 'turn_readout') {
Expand Down
11 changes: 9 additions & 2 deletions vessel/src/app/api/raven-chat/systemBlockBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1189,8 +1189,12 @@ export function buildDumbPromptSegments(plan: RavenRenderPlan, config: SystemBlo
`Signal Strength: ${plan.terrain.load.signalStrength} (Voltage: ${plan.terrain.load.systemVoltage.toFixed(1)})`,
plan.terrain.signalVoidReason ? `Signal Void Reason: ${plan.terrain.signalVoidReason}` : '',
``,
`PRIMARY DRIVERS:`,
...plan.terrain.primaryDrivers.map(d => `- ${d.planet}: ${d.activeAspects.join(', ')} (Mass: ${d.mass.toFixed(2)})`),
`LICENSED DRIVERS:`,
...plan.terrain.primaryDrivers.map(d => {
const role = d.licenseRole ? ` | role=${d.licenseRole}` : '';
const reasons = d.licenseReasons?.length ? ` | reasons=${d.licenseReasons.join(',')}` : '';
return `- ${d.planet}: ${d.activeAspects.join(', ')} (Mass: ${d.mass.toFixed(2)}${role}${reasons})`;
}),
``
]),
`=== BOUNDARIES ===`,
Expand All @@ -1215,6 +1219,9 @@ export function buildDumbPromptSegments(plan: RavenRenderPlan, config: SystemBlo

const instruction = [
`Use the certified terrain below. Do not invent geometry, chambers, events, motives, diagnoses, or timelines.`,
`Field-first directive: open with the measured pressure or lived thesis before naming individual aspects. Drivers are evidence for the field, not a menu to recite.`,
`Dynamic driver law: heavy drivers explain field magnitude; contextual drivers explain chamber or user-question texture. Licensed means lawful to use, not mandatory to mention.`,
`Grounding directive: do not imply a confirming event is owed. End Symbolic Moment reads with an open calibration question or clean landing; silence/no-contact is valid data.`,
`Choose the clearest unifying thread and translate the pressure in Raven voice. Keep the reading accessible, clear, and at a 9th-grade reading level.`,
``,
`--- CERTIFIED TERRAIN PACKET ---`,
Expand Down
8 changes: 7 additions & 1 deletion vessel/src/app/api/raven-chat/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,11 +529,17 @@ export type ChamberDomain =
| "THE_SHELL"; // 12th House

export interface DriverSummary {
planet: "SUN" | "MOON" | "MERCURY" | "VENUS" | "MARS" | "JUPITER" | "SATURN" | "URANUS" | "NEPTUNE" | "PLUTO" | "CHIRON" | "SOUTH_NODE";
planet: "SUN" | "MOON" | "MERCURY" | "VENUS" | "MARS" | "JUPITER" | "SATURN" | "URANUS" | "NEPTUNE" | "PLUTO" | "CHIRON" | "NORTH_NODE" | "SOUTH_NODE" | "LILITH" | "ASCENDANT" | "MEDIUM_COELI" | "UNKNOWN";
coordinate: string;
mass: number; // Mass weighting assigned by Math Brain
polarityBias: number; // Directional bias from -5.0 to +5.0
activeAspects: string[];
licenseRole?: "primary" | "contextual" | "background" | "excluded";
licenseReasons?: Array<"mass" | "salience" | "chamber_resonance" | "user_question_resonance" | "freshness" | "short_window_spike" | "sustained_frame">;
resonantChambers?: string[];
mayMention?: boolean;
shouldMention?: boolean;
mustMention?: boolean;
}

export interface BackgroundSummary {
Expand Down
3 changes: 2 additions & 1 deletion vessel/src/components/chat/ReadoutArtifactChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type ReadoutArtifactClientPayload,
buildEvidenceSummaryFromClient,
} from '../../lib/raven/readoutArtifactClient';
import { claimMayBeNamed } from '../../lib/raven/geometryArtifact';
import { meterDisplay } from '../../lib/raven/meterDisplay';
import { cn } from '../../lib/utils';

Expand Down Expand Up @@ -394,7 +395,7 @@ function GeometryDisclosure({ payload }: Readonly<{ payload: ReadoutArtifactClie
function TurnReadoutChipBody({ payload }: Readonly<{ payload: ReadoutArtifactClientPayload }>) {
const magnitude = meterDisplay(payload.balanceMeter.magnitude);
const bias = meterDisplay(payload.balanceMeter.directionalBias);
const peakDrivers = payload.drivers.filter((d) => d.isPeakDriver || d.tier !== 'minor').slice(0, 2);
const peakDrivers = payload.drivers.filter(claimMayBeNamed).slice(0, 2);

Comment on lines 396 to 399
return (
<>
Expand Down
83 changes: 83 additions & 0 deletions vessel/src/lib/__tests__/vaultDuplicateDetection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { findDuplicateProfileClusters, countDuplicateProfiles } from '../vaultDuplicateDetection';
import type { VaultProfile } from '../vaultSync';

function makeProfile(overrides: Partial<VaultProfile> = {}): VaultProfile {
return {
id: `profile_${Math.random().toString(36).slice(2, 8)}`,
isPrimary: false,
name: 'Daniel Cross',
notes: null,
natalChart: null,
cognitiveArchitecture: null,
cognitiveArchitectureProvenance: null,
relationshipContext: null,
constitutionalCalibrations: null,
birthData: {
year: 1973,
month: 7,
day: 24,
hour: 14,
minute: 30,
latitude: 40.023,
longitude: -75.315,
timezone: 'America/New_York',
city: 'Bryn Mawr',
state: 'Pennsylvania',
country_code: 'US',
},
birthTimeRecord: null,
provenance: 'CLIENT_SELF',
currentLocation: null,
lastUpdated: new Date().toISOString(),
...overrides,
} as VaultProfile;
}

test('findDuplicateProfileClusters returns empty for single profile', () => {
const clusters = findDuplicateProfileClusters([makeProfile()]);
assert.deepEqual(clusters, []);
});

test('findDuplicateProfileClusters detects exact duplicates', () => {
const a = makeProfile({ id: 'a', isPrimary: true });
const b = makeProfile({ id: 'b' });
const clusters = findDuplicateProfileClusters([a, b]);
assert.equal(clusters.length, 1);
assert.equal(clusters[0].reason, 'exact');
assert.equal(clusters[0].canonicalId, 'a');
assert.equal(clusters[0].duplicates.length, 1);
assert.equal(clusters[0].duplicates[0].id, 'b');
});

test('findDuplicateProfileClusters detects near duplicates when birth time differs', () => {
const a = makeProfile({ id: 'a', isPrimary: true });
const b = makeProfile({ id: 'b', birthData: { ...a.birthData, hour: 15, minute: 0 } });
const clusters = findDuplicateProfileClusters([a, b]);
assert.equal(clusters.length, 1);
assert.equal(clusters[0].reason, 'near_match_name_birth');
});

test('findDuplicateProfileClusters ignores profiles with different names', () => {
const a = makeProfile({ id: 'a', name: 'Daniel Cross' });
const b = makeProfile({ id: 'b', name: 'Sarah Cross' });
const clusters = findDuplicateProfileClusters([a, b]);
assert.equal(clusters.length, 0);
});

test('findDuplicateProfileClusters ignores profiles with different birth dates', () => {
const a = makeProfile({ id: 'a' });
const b = makeProfile({ id: 'b', birthData: { ...a.birthData, day: 25 } });
const clusters = findDuplicateProfileClusters([a, b]);
assert.equal(clusters.length, 0);
});

test('countDuplicateProfiles returns total duplicate count across clusters', () => {
const a = makeProfile({ id: 'a', name: 'Daniel Cross' });
const b = makeProfile({ id: 'b', name: 'Daniel Cross' });
const c = makeProfile({ id: 'c', name: 'Sarah Cross' });
const d = makeProfile({ id: 'd', name: 'Sarah Cross' });
const count = countDuplicateProfiles([a, b, c, d]);
assert.equal(count, 2);
});
72 changes: 72 additions & 0 deletions vessel/src/lib/bestAstrologyProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,50 @@
sidereal: 'Sidereal',
};

/**
* Ayanamsa values for Sidereal zodiac calculations.
* Default is 1 (Lahiri/Chitra Paksha), the most astronomically stable and widely accepted offset.
*/
const DEFAULT_AYANAMSA = 1;
const ALLOWED_AYANAMSA_VALUES = new Set([
1, // Lahiri (Chitra Paksha) - default
2, // Raman
3, // Fagan-Bradley
4, // Krishnamurti
5, // Ushashashi
6, // Lahiri (1940)
7, // Lahiri (ICRC)
8, // Lahiri (VP285)
9, // Krishnamurti (VP291)
10, // JN Bhasin
11, // Yukteshwar
12, // Aryabhata
13, // Aryabhata (522)
14, // Aryabhata (Msun)
15, // Suryasiddhanta
16, // Suryasiddhanta (Msun)
17, // SS Citra
18, // SS Revati
19, // True Citra
20, // True Mula
21, // True Pushya
22, // True Revati
23, // True Sheoran
24, // Deluce
25, // Djwhal Khul
26, // Hipparchos
27, // Sassanian
28, // Babyl Kugler1
29, // Babyl Kugler2
30, // Babyl Kugler3
31, // Babyl Huber
32, // Babyl ETPSC
33, // Babyl Britton
34, // Aldebaran 15 Tau
35, // Valens Moon
]);

function normalizeAstrologyPayload(payload: unknown): unknown {

Check failure on line 125 in vessel/src/lib/bestAstrologyProxy.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 42 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=DHCross_Shipyard&issues=AZ8QmHaDb5VpPISv61QV&open=AZ8QmHaDb5VpPISv61QV&pullRequest=883
if (!isRecord(payload)) return payload;

const result: Record<string, unknown> = { ...payload };
Expand All @@ -94,6 +137,15 @@
if (mapped) result.zodiac_type = mapped;
}

// Validate and normalize ayanamsa for Sidereal calculations
if (typeof result.ayanamsa === 'number') {
if (!ALLOWED_AYANAMSA_VALUES.has(result.ayanamsa)) {
throw new BestAstrologyProxyValidationError(
`Invalid ayanamsa value "${result.ayanamsa}". Supported values: 1-35 (1=Lahiri, 2=Raman, 3=Fagan-Bradley, etc.).`
);
}
}

if (isRecord(result.options)) {
const options: Record<string, unknown> = { ...result.options };

Expand All @@ -107,9 +159,29 @@
if (mapped) options.zodiac_type = mapped;
}

// Validate and normalize ayanamsa in options
if (typeof options.ayanamsa === 'number') {
if (!ALLOWED_AYANAMSA_VALUES.has(options.ayanamsa)) {
throw new BestAstrologyProxyValidationError(
`Invalid ayanamsa value "${options.ayanamsa}". Supported values: 1-35 (1=Lahiri, 2=Raman, 3=Fagan-Bradley, etc.).`
);
}
}

// Auto-append ayanamsa for Sidereal zodiac type in options
const zodiacType = typeof options.zodiac_type === 'string' ? options.zodiac_type : typeof result.zodiac_type === 'string' ? result.zodiac_type : null;

Check warning on line 172 in vessel/src/lib/bestAstrologyProxy.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=DHCross_Shipyard&issues=AZ8QmHaDb5VpPISv61QW&open=AZ8QmHaDb5VpPISv61QW&pullRequest=883
if (zodiacType === 'Sidereal' && typeof options.ayanamsa !== 'number') {
options.ayanamsa = DEFAULT_AYANAMSA;
}
Comment on lines +171 to +175

result.options = options;
}

// Auto-append ayanamsa for Sidereal zodiac type at root level
if (typeof result.zodiac_type === 'string' && result.zodiac_type === 'Sidereal' && typeof result.ayanamsa !== 'number' && !isRecord(result.options)) {
result.ayanamsa = DEFAULT_AYANAMSA;
}

// Validate progression_type - only "secondary" is supported
if (typeof result.progression_type === 'string') {
const allowed = new Set(['secondary']);
Expand Down
61 changes: 61 additions & 0 deletions vessel/src/lib/raven/__tests__/geometryArtifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,67 @@ test('api-verified anchor rows win over payload summaries', () => {
assert.equal(formatGeometryClaimLabel(claims[0]), 'Sun conjunction Saturn');
});

test('dynamic licensing can admit a salient chamber-resonant minor driver', () => {
const claims = resolveLicensedGeometryClaims({
date: '2026-06-21',
landingHouses: [3],
anchorTopDrivers: [
{ label: 'Saturn square Moon', x: 3.0, transitingPlanet: 'saturn', stationedPlanet: 'moon', aspectType: 'square' },
{ label: 'Pluto opposition Sun', x: 2.8, transitingPlanet: 'pluto', stationedPlanet: 'sun', aspectType: 'opposition' },
{ label: 'Sun conjunction Venus', x: 2.6, transitingPlanet: 'sun', stationedPlanet: 'venus', aspectType: 'conjunction' },
{ label: 'Jupiter trine Mars', x: 2.4, transitingPlanet: 'jupiter', stationedPlanet: 'mars', aspectType: 'trine' },
{ label: 'Venus sextile Jupiter', x: 2.2, transitingPlanet: 'venus', stationedPlanet: 'jupiter', aspectType: 'sextile' },
{ label: 'Mercury square Uranus', x: 0.7, transitingPlanet: 'mercury', stationedPlanet: 'uranus', aspectType: 'square' },
],
});

const mercury = claims.find((claim) => claim.transitingPlanet === 'mercury');
assert.ok(mercury);
assert.equal(mercury?.tier, 'minor');
assert.equal(mercury?.licenseRole, 'contextual');
assert.ok(mercury?.licenseReasons?.includes('chamber_resonance'));
assert.ok(licensedDriverLabels(claims).includes('Mercury square Uranus'));
});

test('dynamic licensing keeps weak non-resonant minor drivers out of Raven voice', () => {
const claims = resolveLicensedGeometryClaims({
date: '2026-06-21',
landingHouses: [9],
anchorTopDrivers: [
{ label: 'Saturn square Moon', x: 3.0, transitingPlanet: 'saturn', stationedPlanet: 'moon', aspectType: 'square' },
{ label: 'Pluto opposition Sun', x: 2.8, transitingPlanet: 'pluto', stationedPlanet: 'sun', aspectType: 'opposition' },
{ label: 'Sun conjunction Venus', x: 2.6, transitingPlanet: 'sun', stationedPlanet: 'venus', aspectType: 'conjunction' },
{ label: 'Jupiter trine Mars', x: 2.4, transitingPlanet: 'jupiter', stationedPlanet: 'mars', aspectType: 'trine' },
{ label: 'Venus sextile Jupiter', x: 2.2, transitingPlanet: 'venus', stationedPlanet: 'jupiter', aspectType: 'sextile' },
{ label: 'Mercury square Uranus', x: 0.2, transitingPlanet: 'mercury', stationedPlanet: 'uranus', aspectType: 'square' },
],
});

const mercury = claims.find((claim) => claim.transitingPlanet === 'mercury');
assert.equal(mercury?.licenseRole, 'excluded');
assert.ok(!licensedDriverLabels(claims).includes('Mercury square Uranus'));
});

test('question resonance can license local texture without promoting it to mandatory speech', () => {
const claims = resolveLicensedGeometryClaims({
date: '2026-06-21',
questionCategory: 'COMMUNICATION_MESSAGE',
anchorTopDrivers: [
{ label: 'Saturn square Moon', x: 3.0, transitingPlanet: 'saturn', stationedPlanet: 'moon', aspectType: 'square' },
{ label: 'Pluto opposition Sun', x: 2.8, transitingPlanet: 'pluto', stationedPlanet: 'sun', aspectType: 'opposition' },
{ label: 'Sun conjunction Venus', x: 2.6, transitingPlanet: 'sun', stationedPlanet: 'venus', aspectType: 'conjunction' },
{ label: 'Jupiter trine Mars', x: 2.4, transitingPlanet: 'jupiter', stationedPlanet: 'mars', aspectType: 'trine' },
{ label: 'Venus sextile Jupiter', x: 2.2, transitingPlanet: 'venus', stationedPlanet: 'jupiter', aspectType: 'sextile' },
{ label: 'Mercury square Uranus', x: 0.4, transitingPlanet: 'mercury', stationedPlanet: 'uranus', aspectType: 'square' },
],
});

const mercury = claims.find((claim) => claim.transitingPlanet === 'mercury');
assert.equal(mercury?.licenseRole, 'contextual');
assert.equal(mercury?.mustMention, false);
assert.ok(mercury?.licenseReasons?.includes('user_question_resonance'));
});

test('peak driver without natal receiver cannot seal as a claim', () => {
const claims = buildGeometryClaimsFromPeakDrivers({
date: '2026-06-21',
Expand Down
Loading
Loading