diff --git a/frontend/src/lib/birth-rectification-payload.ts b/frontend/src/lib/birth-rectification-payload.ts index d7fc3ba8..f65f9a2d 100644 --- a/frontend/src/lib/birth-rectification-payload.ts +++ b/frontend/src/lib/birth-rectification-payload.ts @@ -36,5 +36,6 @@ export async function payloadFromProfile(profile: BirthRectificationProfile) { lon, tz, ayanamsa: resolveAyanamsa(resolved), + node_mode: "mean", }; } diff --git a/frontend/src/lib/birth-time-journey-case-loader.ts b/frontend/src/lib/birth-time-journey-case-loader.ts index 7770ad17..b5b32463 100644 --- a/frontend/src/lib/birth-time-journey-case-loader.ts +++ b/frontend/src/lib/birth-time-journey-case-loader.ts @@ -23,6 +23,7 @@ import { } from "./birth-time-journey-dynamic-state.ts"; import { BirthTimeJourneyStoreError } from "./birth-time-journey-store-errors.ts"; import { resolveMissingBirthTimezoneOffset } from "./birth-profile-timezone.ts"; +import { resolveAyanamsa } from "./ayanamsa.ts"; type JourneyLoadResult = { readonly data: unknown; readonly error: unknown }; type JourneyLoadQuery = { @@ -141,7 +142,7 @@ export async function loadStoredRectificationCase( const parsed = storedCaseSchema.parse(data); const { data: profile, error: profileError } = await client .from("profiles") - .select("birth_date,reported_birth_time,birth_time_period,latitude,longitude,timezone_id,timezone_offset") + .select("birth_date,reported_birth_time,birth_time_period,latitude,longitude,timezone_id,timezone_offset,ayanamsa") .eq("id", userId) .maybeSingle(); if (profileError || !profile) throw new BirthTimeJourneyStoreError("load_case"); @@ -200,6 +201,7 @@ export async function loadStoredRectificationCase( lat: location.latitude, lon: location.longitude, tz: location.timezone_offset, + ayanamsa: resolveAyanamsa(resolvedProfile as { ayanamsa?: unknown }), }, lifeEvents: parsed.life_events, candidateResult, diff --git a/frontend/src/lib/birth-time-journey-engine-model.ts b/frontend/src/lib/birth-time-journey-engine-model.ts index 4cc83afd..363b4c9c 100644 --- a/frontend/src/lib/birth-time-journey-engine-model.ts +++ b/frontend/src/lib/birth-time-journey-engine-model.ts @@ -13,6 +13,7 @@ import type { DynamicChoiceScoreInput, JourneyEventScoreInput, } from "./birth-time-journey-service.ts"; +import { resolveAyanamsa } from "./ayanamsa.ts"; export class BirthTimeJourneyEngineError extends Error { readonly name = "BirthTimeJourneyEngineError"; @@ -87,6 +88,8 @@ export function eventScorePayload(input: JourneyEventScoreInput) { lat: input.lat, lon: input.lon, tz: input.tz, + ayanamsa: resolveAyanamsa(input), + node_mode: "mean", high_rigor: true, events: (input.events ?? []).map((event) => ({ id: event.id, @@ -163,6 +166,7 @@ export function createJourneyEngineMethods(wire: JourneyEngineWire): BirthTimeJo lon: input.lon, tz: input.tz, ayanamsa: input.ayanamsa, + node_mode: "mean", }, }); return { questionnaire: parseRectificationQuestionnaire(payload) }; diff --git a/frontend/src/lib/birth-time-journey-service.ts b/frontend/src/lib/birth-time-journey-service.ts index 3d257031..382cd9bc 100644 --- a/frontend/src/lib/birth-time-journey-service.ts +++ b/frontend/src/lib/birth-time-journey-service.ts @@ -70,6 +70,7 @@ export type JourneyEventScoreInput = { readonly lat: number; readonly lon: number; readonly tz: number; + readonly ayanamsa?: AyanamsaName; readonly events: readonly LifeEvent[]; }; @@ -123,6 +124,7 @@ type StoredRectificationCaseBase = { readonly lat: number; readonly lon: number; readonly tz: number; + readonly ayanamsa?: AyanamsaName; }; readonly lifeEvents?: readonly LifeEvent[]; readonly candidateResult?: CandidateResult | null; diff --git a/frontend/src/lib/rectification-agentic/session.ts b/frontend/src/lib/rectification-agentic/session.ts index 75d358ae..a89f1f20 100644 --- a/frontend/src/lib/rectification-agentic/session.ts +++ b/frontend/src/lib/rectification-agentic/session.ts @@ -4,6 +4,7 @@ import type { AgenticRectificationCandidateResult, AgenticRectificationContext, } from "@/mastra/rectification-tools"; +import { resolveAyanamsa, type AyanamsaName } from "../ayanamsa.ts"; import { normalizePersistedBirthDate } from "../birth-time-intake-model.ts"; import { declaredClockRange } from "../declared-birth-window.ts"; @@ -26,6 +27,7 @@ export type AgenticRectificationProfile = Readonly<{ lat: number; lon: number; tz: number; + ayanamsa: AyanamsaName; declaredAccuracy: AgenticRectificationContext["declaredAccuracy"]; timeSource: AgenticRectificationContext["timeSource"]; baselineReportedTime: string | null; @@ -166,7 +168,7 @@ export async function loadAgenticRectificationProfile( ): Promise { const { data, error } = await accounting .from("profiles") - .select("birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_offset") + .select("birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_offset,ayanamsa") .eq("id", userId) .single(); if (error || !data) throw new AgenticRectificationProfileError("profile_unavailable"); @@ -199,6 +201,7 @@ export async function loadAgenticRectificationProfile( lat, lon, tz, + ayanamsa: resolveAyanamsa(data), declaredAccuracy: declaredAccuracyFrom(uncertaintyBefore, uncertaintyAfter, rawSource), timeSource: timeSourceFrom(rawSource), baselineReportedTime: reportedTime, @@ -284,6 +287,7 @@ export function createAgenticRectificationContext( lat: profile.lat, lon: profile.lon, tz: profile.tz, + ayanamsa: profile.ayanamsa, }, candidateRange: profile.candidateRange, declaredAccuracy: profile.declaredAccuracy, diff --git a/frontend/src/lib/rectification-agentic/v9/engine-client.ts b/frontend/src/lib/rectification-agentic/v9/engine-client.ts index f07c122f..90e654c0 100644 --- a/frontend/src/lib/rectification-agentic/v9/engine-client.ts +++ b/frontend/src/lib/rectification-agentic/v9/engine-client.ts @@ -477,6 +477,7 @@ function engineRequestBody(input: { lon, tz, ayanamsa: resolveAyanamsa(snapshot), + node_mode: "mean", events: input.events, birth_time_source: snapshot.birth_time_source, timezone_id: snapshot.timezone_id, diff --git a/frontend/src/mastra/rectification-tools.ts b/frontend/src/mastra/rectification-tools.ts index 22cdf484..94a9f1c8 100644 --- a/frontend/src/mastra/rectification-tools.ts +++ b/frontend/src/mastra/rectification-tools.ts @@ -1,6 +1,7 @@ import { createTool } from "@mastra/core/tools"; import { createHash } from "node:crypto"; import { z } from "zod"; +import { resolveAyanamsa, type AyanamsaName } from "../lib/ayanamsa.ts"; import { dateRangeFromDeclared } from "../lib/rectification-agentic/date-range.ts"; /** @@ -27,6 +28,7 @@ export type AgenticRectificationBirth = Readonly<{ lat: number; lon: number; tz: number; + ayanamsa?: AyanamsaName | string | null; }>; export type AgenticRectificationCandidateRange = Readonly<{ @@ -250,6 +252,8 @@ function v5Request( lat: ctx.birth.lat, lon: ctx.birth.lon, tz: ctx.birth.tz, + ayanamsa: resolveAyanamsa(ctx.birth), + node_mode: "mean", events: events.map(toV5Event), }; } @@ -425,6 +429,8 @@ export function createAgenticRectificationTools(ctx: AgenticRectificationContext lat: ctx.birth.lat, lon: ctx.birth.lon, tz: ctx.birth.tz, + ayanamsa: resolveAyanamsa(ctx.birth), + node_mode: "mean", time_uncertainty_minutes: scanWindow.uncertaintyMinutes, step_minutes: input.step_minutes, }; @@ -533,6 +539,8 @@ export function createAgenticRectificationTools(ctx: AgenticRectificationContext lat: ctx.birth.lat, lon: ctx.birth.lon, tz: ctx.birth.tz, + ayanamsa: resolveAyanamsa(ctx.birth), + node_mode: "mean", events: [], }); const snapshot = (data.candidate_feature_snapshot && typeof data.candidate_feature_snapshot === "object") @@ -568,6 +576,8 @@ export function createAgenticRectificationTools(ctx: AgenticRectificationContext lat: ctx.birth.lat, lon: ctx.birth.lon, tz: ctx.birth.tz, + ayanamsa: resolveAyanamsa(ctx.birth), + node_mode: "mean", events: input.events.map(toV3Event), high_rigor: true, }; diff --git a/frontend/tests/birth-time-journey-engine.test.ts b/frontend/tests/birth-time-journey-engine.test.ts index 97dd966e..b329fe81 100644 --- a/frontend/tests/birth-time-journey-engine.test.ts +++ b/frontend/tests/birth-time-journey-engine.test.ts @@ -19,6 +19,7 @@ test("journey engine serializes only stored event-scoring inputs", () => { lat: 31.2304, lon: 121.4737, tz: 8, + ayanamsa: "lahiri", events: [ { id: "5cb071d6-6d99-46be-85dc-a9bf59ef6ac5", domain: "career", date: "2019-07", precision: "month", summary: "晋升为团队负责人" }, { id: "0790866c-ad5e-4a45-b2b4-a5c73f6be6ea", domain: "education", date: "2011", precision: "year" }, @@ -33,6 +34,8 @@ test("journey engine serializes only stored event-scoring inputs", () => { lat: 31.2304, lon: 121.4737, tz: 8, + ayanamsa: "lahiri", + node_mode: "mean", high_rigor: true, events: [ { id: "5cb071d6-6d99-46be-85dc-a9bf59ef6ac5", domain: "career", date: "2019-07", precision: "month", summary: "晋升为团队负责人" }, diff --git a/scripts/active_rectification_event_engine.py b/scripts/active_rectification_event_engine.py index 2c3ef98f..a2882450 100644 --- a/scripts/active_rectification_event_engine.py +++ b/scripts/active_rectification_event_engine.py @@ -289,7 +289,9 @@ def _controlled_transit_rules( transit_chart = domain_calculation_service.compute_chart({ "year": event_at.year, "month": event_at.month, "day": event_at.day, "hour": 12, "minute": 0, "lat": request["lat"], "lon": request["lon"], - "tz": request["tz"], "ayanamsa": AYANAMSA, "node_mode": NODE_MODE, + "tz": request["tz"], + "ayanamsa": request.get("ayanamsa", AYANAMSA), + "node_mode": request.get("node_mode", NODE_MODE), }) rules: list[str] = [] for planet in ("Jupiter", "Saturn"): @@ -425,8 +427,8 @@ def build_candidate_static_context( "lat": request["lat"], "lon": request["lon"], "tz": request["tz"], - "ayanamsa": AYANAMSA, - "node_mode": NODE_MODE, + "ayanamsa": request.get("ayanamsa", AYANAMSA), + "node_mode": request.get("node_mode", NODE_MODE), }) planet_longitudes = { name: float(data["lon"]) @@ -642,8 +644,8 @@ def _canonical_input_contract(request: RectificationEventRequest) -> tuple[dict, "summary": event.get("summary", ""), } for event in request["events"]], "calculation": { - "ayanamsa": AYANAMSA, - "node_mode": NODE_MODE, + "ayanamsa": request.get("ayanamsa", AYANAMSA), + "node_mode": request.get("node_mode", NODE_MODE), "ephemeris_source": "swisseph_calc_ut", }, } diff --git a/scripts/active_rectification_events.py b/scripts/active_rectification_events.py index e5314278..0f932f20 100644 --- a/scripts/active_rectification_events.py +++ b/scripts/active_rectification_events.py @@ -73,6 +73,8 @@ class RectificationEventRequest(TypedDict): lat: float lon: float tz: float + ayanamsa: NotRequired[str] + node_mode: NotRequired[str] events: list[LifeEvent] diff --git a/scripts/active_rectification_questions.py b/scripts/active_rectification_questions.py index 502cc3cd..6e6d58fa 100644 --- a/scripts/active_rectification_questions.py +++ b/scripts/active_rectification_questions.py @@ -16,6 +16,9 @@ if str(SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(SCRIPTS_DIR)) from active_rectification_selector import select_next_questions +from ayanamsa_utils import DEFAULT_AYANAMSA_NAME + +DEFAULT_NODE_MODE = "mean" OPTIONS = [ {"key": "A", "label": "明确有,且时间大致吻合", "score": 2}, @@ -60,7 +63,8 @@ def _candidate_scan( lat: float | None = None, lon: float | None = None, tz: float | None = None, - ayanamsa: str = "raman", + ayanamsa: str = DEFAULT_AYANAMSA_NAME, + node_mode: str = DEFAULT_NODE_MODE, ) -> dict[str, Any]: start = center - timedelta(minutes=uncertainty_minutes) end = center + timedelta(minutes=uncertainty_minutes) @@ -84,7 +88,14 @@ def _candidate_scan( "cluster": cluster, "sensitivity_flags": _sensitivity_flags(abs(offset)), } - recast = _candidate_recast(candidate, lat=lat, lon=lon, tz=tz, ayanamsa=ayanamsa) + recast = _candidate_recast( + candidate, + lat=lat, + lon=lon, + tz=tz, + ayanamsa=ayanamsa, + node_mode=node_mode, + ) if recast: sample.update(recast) samples.append(sample) @@ -106,6 +117,8 @@ def _candidate_scan( "lat": lat, "lon": lon, "tz": tz, + "ayanamsa": ayanamsa, + "node_mode": node_mode, }, uncertainty_minutes=uncertainty_minutes, step_minutes=step_minutes, @@ -126,6 +139,10 @@ def _candidate_scan( "cluster_labels": ["early_candidate_cluster", "middle_candidate_cluster", "late_candidate_cluster"], "samples": samples, "minute_scan": minute_scan, + "calculation": { + "ayanamsa": ayanamsa, + "node_mode": node_mode, + }, "sensitivity_summary": { "method": "minute_feature_scan_v2", "high_value_layers": ["D2", "D4", "D9", "D10", "D24", "D30", "D60", "UL", "A7", "A10", "KP_cusp"], @@ -152,6 +169,7 @@ def _candidate_recast( lon: float | None, tz: float | None, ayanamsa: str, + node_mode: str = DEFAULT_NODE_MODE, ) -> dict[str, Any] | None: if lat is None or lon is None or tz is None: return None @@ -171,6 +189,7 @@ def _candidate_recast( "lon": lon, "tz": tz, "ayanamsa": ayanamsa, + "node_mode": node_mode, }) planet_lons = { name: data["lon"] @@ -231,7 +250,8 @@ def build_questionnaire( lat: float | None = None, lon: float | None = None, tz: float | None = None, - ayanamsa: str = "raman", + ayanamsa: str = DEFAULT_AYANAMSA_NAME, + node_mode: str = DEFAULT_NODE_MODE, ) -> dict[str, Any]: questions = [] for qid, round_id, domain, sensitivity, window, prompt, yes_bias, no_bias in QUESTION_TEMPLATES: @@ -270,6 +290,7 @@ def build_questionnaire( lon=lon, tz=tz, ayanamsa=ayanamsa, + node_mode=node_mode, ) selection = select_next_questions( {"questions": questions, "candidate_scan": candidate_scan}, {}, limit=1 @@ -279,6 +300,10 @@ def build_questionnaire( "schema_version": 1, "candidate_scan": candidate_scan, "selection": selection, + "calculation": { + "ayanamsa": ayanamsa, + "node_mode": node_mode, + }, "workflow": [ "candidate_time_scan", "varga_arudha_kp_sensitivity_diff", @@ -433,6 +458,10 @@ def score_answers( "truth_status": "not_birth_time_truth", "formula_unit_parity_status": "partial", "timing_claim_status": "exploratory_unvalidated", + "calculation": questionnaire.get("calculation", { + "ayanamsa": DEFAULT_AYANAMSA_NAME, + "node_mode": DEFAULT_NODE_MODE, + }), "answered_count": len(applied), "candidate_cluster_rankings": rankings, "technique_audit_table": [ @@ -494,10 +523,18 @@ def main() -> int: parser.add_argument("--birth-time", required=True, help="Approximate local birth time, YYYY-MM-DD HH:MM") parser.add_argument("--uncertainty-minutes", type=int, default=30) parser.add_argument("--step-minutes", type=int, default=1) + parser.add_argument("--ayanamsa", default=DEFAULT_AYANAMSA_NAME) + parser.add_argument("--node-mode", choices=("mean", "true"), default=DEFAULT_NODE_MODE) parser.add_argument("--answers-json", default="", help="Optional JSON object mapping question id to A/B/C/D") parser.add_argument("--pretty", action="store_true") args = parser.parse_args() - questionnaire = build_questionnaire(args.birth_time, args.uncertainty_minutes, args.step_minutes) + questionnaire = build_questionnaire( + args.birth_time, + args.uncertainty_minutes, + args.step_minutes, + ayanamsa=args.ayanamsa, + node_mode=args.node_mode, + ) report = score_answers(questionnaire, json.loads(args.answers_json)) if args.answers_json else questionnaire print(json.dumps(report, ensure_ascii=False, indent=2 if args.pretty else None)) return 0 diff --git a/scripts/candidate_time_sensitivity_scan.py b/scripts/candidate_time_sensitivity_scan.py index 099600e0..01357bd4 100644 --- a/scripts/candidate_time_sensitivity_scan.py +++ b/scripts/candidate_time_sensitivity_scan.py @@ -34,6 +34,12 @@ def _engine_json(command: str, payload: dict[str, Any], *, timeout: int = 20) -> args = [sys.executable, str(ENGINE), command] for key in ("year", "month", "day", "hour", "minute", "lat", "lon", "tz"): args.extend([f"--{key}", str(payload[key])]) + args.extend([ + "--ayanamsa", + str(payload.get("ayanamsa") or DEFAULT_AYANAMSA_NAME).strip().lower(), + "--node-mode", + str(payload.get("node_mode", payload.get("nodeMode", "mean"))).strip().lower(), + ]) if command == "varga-full": args.extend(["--divisions", ",".join(_VARGAS)]) completed = subprocess.run(args, cwd=ROOT, capture_output=True, text=True, timeout=timeout, check=True) @@ -148,6 +154,8 @@ def main() -> int: parser.add_argument(f"--{field}", required=True, type=cast) parser.add_argument("--uncertainty-minutes", type=int, default=30) parser.add_argument("--step-minutes", type=int, default=1) + parser.add_argument("--ayanamsa", default=DEFAULT_AYANAMSA_NAME) + parser.add_argument("--node-mode", choices=("mean", "true"), default="mean") args = parser.parse_args() print(json.dumps(scan_candidate_times(vars(args), uncertainty_minutes=args.uncertainty_minutes, step_minutes=args.step_minutes), ensure_ascii=False, indent=2)) return 0 diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 2af6d45c..bd5d7a33 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -818,6 +818,19 @@ def _request_ayanamsa(*sources) -> str: return DEFAULT_AYANAMSA_NAME +def _request_node_mode(*sources) -> str: + for source in sources: + if isinstance(source, dict): + raw = source.get('node_mode', source.get('nodeMode')) + else: + raw = source + if raw not in (None, ''): + if not isinstance(raw, str) or raw.strip().lower() not in {'mean', 'true'}: + raise BadRequest('node_mode must be mean or true') + return raw.strip().lower() + return 'mean' + + def _consultation_declared_routes(body: dict) -> set[str]: values: list[str] = [] for key in ('strict_workflow_route', 'theme'): @@ -8792,8 +8805,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): lon = self._get_float(body, 'lon', 0, -180, 180) tz = self._get_float(body, 'tz', 0, -14, 14) ayanamsa = _request_ayanamsa(body) - if not isinstance(ayanamsa, str): - raise BadRequest('ayanamsa must be a string') + node_mode = _request_node_mode(body) try: module = _load_local_module('active_rectification_questions') result = module.build_questionnaire( @@ -8804,6 +8816,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): lon=lon, tz=tz, ayanamsa=ayanamsa, + node_mode=node_mode, ) except ValueError as e: raise BadRequest('birth_time must be YYYY-MM-DD HH:MM') from e @@ -8839,6 +8852,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): def _compute_active_rectification_events(self, body): allowed_fields = { 'birth_date', 'start_time', 'end_time', 'lat', 'lon', 'tz', 'events', 'high_rigor', + 'ayanamsa', 'node_mode', } unsupported_fields = sorted(set(body) - allowed_fields) if unsupported_fields: @@ -8864,6 +8878,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): lat = self._get_float(body, 'lat', 0, -90, 90) lon = self._get_float(body, 'lon', 0, -180, 180) tz = self._get_float(body, 'tz', 0, -14, 14) + ayanamsa = _request_ayanamsa(body) + node_mode = _request_node_mode(body) events = body.get('events') if not isinstance(events, list) or len(events) < MIN_SCORING_EVENTS: raise BadRequest(f'events must contain at least {MIN_SCORING_EVENTS} item') @@ -8911,6 +8927,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'lat': lat, 'lon': lon, 'tz': tz, + 'ayanamsa': ayanamsa, + 'node_mode': node_mode, 'events': normalized_events, }) external_validation = { @@ -8935,6 +8953,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'lat': lat, 'lon': lon, 'tz': tz, + 'ayanamsa': ayanamsa, + 'node_mode': node_mode, } three_engine_packet = build_packet(engine_case) vedastro_events = [{ diff --git a/scripts/rectification/contracts.py b/scripts/rectification/contracts.py index d046dfa9..05e7cc0f 100644 --- a/scripts/rectification/contracts.py +++ b/scripts/rectification/contracts.py @@ -6,6 +6,8 @@ import uuid from datetime import date from typing import Any, Literal, NotRequired, TypedDict, cast +from scripts.ayanamsa_utils import UnsupportedAyanamsaError, normalize_ayanamsa_name + DatePrecision = Literal["day", "month", "quarter", "year", "range"] EVENT_CONTRACT_VERSION = "rectification-event-contract-v2" @@ -48,7 +50,10 @@ _EVENT_PROVENANCE_FIELDS = frozenset({ "date_source", "date_reliability", "date_corroboration", "date_conflict_status", "source_turn_id", "subject", }) -_REQUEST_FIELDS = frozenset({"birth_date", "start_time", "end_time", "lat", "lon", "tz", "events"}) | _REQUEST_PROVENANCE_FIELDS +_REQUEST_FIELDS = frozenset({ + "birth_date", "start_time", "end_time", "lat", "lon", "tz", "events", + "ayanamsa", "node_mode", +}) | _REQUEST_PROVENANCE_FIELDS _EVENT_FIELDS = frozenset({"id", "domain", "event_kind", "date_start", "date_end", "precision", "summary"}) | _EVENT_PROVENANCE_FIELDS _CLOCK = re.compile(r"(?:[01]\d|2[0-3]):[0-5]\d\Z") @@ -77,6 +82,8 @@ class RectificationRequest(TypedDict): lon: float tz: float events: list[LifeEvent] + ayanamsa: NotRequired[str] + node_mode: NotRequired[str] birth_time_source: NotRequired[str | None] timezone_id: NotRequired[str | None] timezone_source: NotRequired[str | None] @@ -227,6 +234,16 @@ def normalize_rectification_request(body: Any, *, today: date | None = None) -> "tz": _bounded_number(body, "tz", -14, 14), "events": cleaned_events, } + if "ayanamsa" in body: + try: + cleaned_request["ayanamsa"] = normalize_ayanamsa_name(body.get("ayanamsa")) + except UnsupportedAyanamsaError as exc: + raise ValueError(str(exc)) from exc + if "node_mode" in body: + node_mode = body.get("node_mode") + if not isinstance(node_mode, str) or node_mode.strip().lower() not in {"mean", "true"}: + raise ValueError("node_mode must be mean or true") + cleaned_request["node_mode"] = node_mode.strip().lower() _copy_nullable_text(body, cleaned_request, "birth_time_source", "birth_time_source", 120, _BIRTH_TIME_SOURCES) _copy_nullable_text(body, cleaned_request, "timezone_id", "timezone_id", 120) _copy_nullable_text(body, cleaned_request, "timezone_source", "timezone_source", 80) diff --git a/scripts/rectification/scoring_service.py b/scripts/rectification/scoring_service.py index 92332124..9684929e 100644 --- a/scripts/rectification/scoring_service.py +++ b/scripts/rectification/scoring_service.py @@ -103,7 +103,7 @@ def sample_event_dates(event: LifeEvent) -> list[str]: def _legacy_request(request: RectificationRequest, event: LifeEvent, sampled_date: str) -> dict[str, Any]: engine_domain, _ = _ENGINE_KIND_BY_NATIVE_KIND[event["event_kind"]] - return { + legacy_request = { "birth_date": request["birth_date"], "start_time": request["start_time"], "end_time": request["end_time"], @@ -116,6 +116,10 @@ def _legacy_request(request: RectificationRequest, event: LifeEvent, sampled_dat "date": sampled_date, "precision": "day", "summary": event.get("summary", ""), }], } + for key in ("ayanamsa", "node_mode"): + if key in request: + legacy_request[key] = request[key] + return legacy_request def _canonical(value: Any) -> str: @@ -349,7 +353,9 @@ def calculation_spec(request: RectificationRequest) -> dict[str, Any]: "latitude": json_number(request["lat"]), "longitude": json_number(request["lon"]), "timezoneOffsetHours": json_number(request["tz"]), - "ayanamsa": "raman", "nodeMode": "mean", "minuteStep": 1, + "ayanamsa": request.get("ayanamsa", "raman"), + "nodeMode": request.get("node_mode", "mean"), + "minuteStep": 1, } for source, target in ( ("birth_time_source", "birthTimeSource"),