diff --git a/docs/research/oracle_closure_master_dashboard_latest.md b/docs/research/oracle_closure_master_dashboard_latest.md index 05260bb7..1b383a86 100644 --- a/docs/research/oracle_closure_master_dashboard_latest.md +++ b/docs/research/oracle_closure_master_dashboard_latest.md @@ -1,6 +1,6 @@ # Jyotish External Oracle Closure Master Dashboard -Generated: `2026-06-26T19:05:03.531931+00:00` +Generated: `2026-06-28T23:44:03.079746+00:00` ## Summary @@ -16,7 +16,7 @@ Generated: `2026-06-26T19:05:03.531931+00:00` |---|---:|---:|---|---:|---:|---:|---:| | `dasha` | 3 | 3 | `complete` | 0 | 0 | 0 | 0 | | `tajika_sahams` | 5 | 1 | `template_einstein_varshaphala_1905_lahiri` | 15 | 15 | 5 | 10 | -| `shadbala` | 4 | 4 | `template_redacted_place_shadbala_raman` | 55 | 55 | 5 | 50 | +| `shadbala` | 4 | 4 | `complete` | 0 | 0 | 0 | 0 | ## Next Action Order diff --git a/docs/research/public_benchmark_dashboard_latest.md b/docs/research/public_benchmark_dashboard_latest.md index a3e85cf6..c0c8d978 100644 --- a/docs/research/public_benchmark_dashboard_latest.md +++ b/docs/research/public_benchmark_dashboard_latest.md @@ -1,6 +1,6 @@ # Public Jyotish Benchmark Dashboard -Generated: `2026-06-28T07:20:42.239186+00:00` +Generated: `2026-06-28T23:44:26.816376+00:00` ## Capability Registry @@ -10,9 +10,9 @@ Generated: `2026-06-28T07:20:42.239186+00:00` ## Dasha/Shadbala Oracle Readiness -- total_packets: `5` -- valid_packets: `4` -- ready_for_calibration: `4` +- total_packets: `6` +- valid_packets: `5` +- ready_for_calibration: `5` - production_tuning_allowed: `false` - valid_dasha_packets: `3` - total_dasha_packets: `3` @@ -29,8 +29,8 @@ Generated: `2026-06-28T07:20:42.239186+00:00` ## Boundary Audit -- external_verified_template_cases: `4` -- template_comparison_count: `4` +- external_verified_template_cases: `5` +- template_comparison_count: `5` - production_tuning_recommended: `false` ## Global First Claim diff --git a/docs/research/vedastro_fast_path_checklist_latest.json b/docs/research/vedastro_fast_path_checklist_latest.json new file mode 100644 index 00000000..1bcc00fa --- /dev/null +++ b/docs/research/vedastro_fast_path_checklist_latest.json @@ -0,0 +1,225 @@ +{ + "scope": "vedastro_fast_path_checklist", + "generated_at": "2026-06-28T23:49:39.978828+00:00", + "summary": { + "row_count": 13, + "local_registry_technique_count": 89, + "catalog_tag_count": 46, + "catalog_method_count": 2258 + }, + "boundary": { + "do_not_clone_vedastro": true, + "keep_local_adjudicator_authoritative": true, + "external_outputs_default_to_secondary_context": true + }, + "official_catalog": { + "available": true, + "path": "/Users/wuyongnaren/Documents/印度占星/scratch/local/vedastro_adapter/method_catalog_snapshot.json", + "tag_count": 46, + "method_count": 2258 + }, + "lanes": { + "official_mcp": [ + { + "capability": "MCP / API Surface", + "priority": "P0", + "recommended_path": "hybrid_local_plus_vedastro", + "local_status": "partial", + "adjudicator_use": "primary", + "route_notes": "If official MCP is available, that is the fastest direct agent path; otherwise fall back to local REST adapter.", + "gap_notes": "Local API/MCP surfaces exist and the official public MCP bridge is live; REST adapter official endpoint smoke still depends on configured endpoint-backed execution.", + "local_assets": [ + "mcp_server.py", + "jyotish_api_server.py", + "strict workflows", + "vedastro_service_adapter.py", + "vedastro_official_mcp_bridge.py" + ] + } + ], + "official_python_bridge": [ + { + "capability": "Tajika Annual", + "priority": "P0", + "recommended_path": "hybrid_local_plus_vedastro", + "local_status": "partial", + "adjudicator_use": "secondary", + "route_notes": "Use Python bridge for breadth, but keep year-chart reasoning and labels local.", + "gap_notes": "Annual modules exist; yearly career/wealth/month windows need stronger strict-workflow integration.", + "local_assets": [ + "tajika.py", + "varshaphala.py", + "sahams", + "solar_return" + ] + }, + { + "capability": "Ashtakavarga", + "priority": "P0", + "recommended_path": "hybrid_local_plus_vedastro", + "local_status": "partial", + "adjudicator_use": "secondary", + "route_notes": "Use the Python bridge for broad calculator access while local strict workflows stay authoritative.", + "gap_notes": "SAV/BAV is in production; PAV/Sodhita/Kakshya bridges need continued regression before being dominant labels.", + "local_assets": [ + "ashtakavarga", + "ashtakavarga_pav", + "ashtakavarga_sodhita", + "finance_ashtakavarga_bridge" + ] + }, + { + "capability": "Shadbala", + "priority": "P0", + "recommended_path": "hybrid_local_plus_vedastro", + "local_status": "partial", + "adjudicator_use": "secondary", + "route_notes": "Broad strength calculators are easiest through the Python bridge, then folded back as oracle evidence.", + "gap_notes": "Local component-aware cap exists; absolute external oracle closure is still incomplete.", + "local_assets": [ + "shadbala", + "shadbala_advanced", + "shadbala_component_cap", + "oracle_shadbala_queue" + ] + } + ], + "rest_adapter": [ + { + "capability": "EventsAtRange / Life Event Graph", + "priority": "P0", + "recommended_path": "hybrid_local_plus_vedastro", + "local_status": "partial", + "adjudicator_use": "oracle_only", + "route_notes": "Use official REST adapter first for range scans; keep local adjudicator as the reasoning layer.", + "gap_notes": "Local timing modules exist, but a high-frequency day/hour event graph is not yet a local productized radar.", + "local_assets": [ + "transit_trigger", + "dasha", + "narayana_dasha", + "vedastro_service_adapter.range_scan" + ] + }, + { + "capability": "Birth Time ML / Rectification Assistant", + "priority": "P1", + "recommended_path": "hybrid_local_plus_vedastro", + "local_status": "partial", + "adjudicator_use": "secondary", + "route_notes": "Use VedAstro externally only as supporting evidence; local rectification gate stays in control.", + "gap_notes": "Local rectification exists; ML parity with VedAstro-style service behavior is not established.", + "local_assets": [ + "birth_time_rectifier.py", + "rectification_gate", + "jyotish-app rectification" + ] + } + ], + "local_native_preferred": [ + { + "capability": "Prashna / Horary", + "priority": "P0", + "recommended_path": "local_native", + "local_status": "covered", + "adjudicator_use": "secondary", + "route_notes": "No need to outsource core horary math while local modules already exist.", + "gap_notes": "Horary modules exist but are not yet a first-class question adjudicator route.", + "local_assets": [ + "prashna.py", + "kp_system.py", + "upagraha_gulika_maandi", + "sphuta_trisphuta_family" + ] + }, + { + "capability": "Jaimini / Chara Dasha", + "priority": "P0", + "recommended_path": "local_native", + "local_status": "covered", + "adjudicator_use": "secondary", + "route_notes": "Keep Jaimini native; external engines help only as spot-check evidence.", + "gap_notes": "Core Jaimini exists; mission/career/marriage quality adjudicator folding is not yet exhaustive.", + "local_assets": [ + "jaimini.py", + "AK", + "DK", + "UL", + "Chara Dasha", + "Jaimini marriage bridge v1" + ] + }, + { + "capability": "Report Rendering", + "priority": "P0", + "recommended_path": "new_local_impl", + "local_status": "partial", + "adjudicator_use": "not_used", + "route_notes": "Rendering is a local product concern, not a VedAstro dependency.", + "gap_notes": "HTML/PDF artifact path exists; polished SVG/PDF chart rendering and cloud-scale report production are not finished.", + "local_assets": [ + "report_artifact API", + "report_builder.py", + "chart_renderer.py", + "jyotish-app export" + ] + }, + { + "capability": "Synastry / Ashtakoot", + "priority": "P0", + "recommended_path": "local_native", + "local_status": "covered", + "adjudicator_use": "secondary", + "route_notes": "Local matching is already stronger than a thin external call unless you need an oracle comparison.", + "gap_notes": "Matching modules exist and API-backed; relationship adjudicator still needs a formal bridge.", + "local_assets": [ + "synastry.py", + "ashtakoot.py", + "36-point Ashtakoot", + "16-factor compatibility" + ] + }, + { + "capability": "D1-D60 Divisional Charts", + "priority": "P0", + "recommended_path": "local_native", + "local_status": "covered", + "adjudicator_use": "primary", + "route_notes": "Do not pay external-call cost here; local engine is already the primary route.", + "gap_notes": "Local varga coverage is strong; keep VedAstro/PyJHora as benchmark evidence rather than replacing local math.", + "local_assets": [ + "varga", + "varga_full", + "shodasavarga", + "divisional_charts_extended" + ] + } + ], + "hybrid_router": [ + { + "capability": "Ayanamsa Selection", + "priority": "P0", + "recommended_path": "hybrid_local_plus_vedastro", + "local_status": "partial", + "adjudicator_use": "oracle_only", + "route_notes": "Prefer local ayanamsa controls for production and use VedAstro only as parity/oracle evidence.", + "gap_notes": "Local Lahiri path is usable, but broad ayanamsa parity and public comparison artifacts remain incomplete.", + "local_assets": [ + "ayanamsa_utils", + "ephemeris_adapter_contract" + ] + } + ], + "external_evidence_only": [ + { + "capability": "Numerology / Non-Jyotish Tools", + "priority": "P2", + "recommended_path": "external_evidence_only", + "local_status": "missing", + "adjudicator_use": "not_used", + "route_notes": "Do not implement locally unless it becomes product-critical.", + "gap_notes": "Adjacent product feature; not required for Jyotish adjudicator depth.", + "local_assets": [] + } + ] + } +} diff --git a/docs/research/vedastro_fast_path_checklist_latest.md b/docs/research/vedastro_fast_path_checklist_latest.md new file mode 100644 index 00000000..9a46a3a8 --- /dev/null +++ b/docs/research/vedastro_fast_path_checklist_latest.md @@ -0,0 +1,80 @@ +# VedAstro 596+ Nodes Fast-Path Checklist + +- Generated: `2026-06-28T23:49:39.978828+00:00` +- Local registry techniques: `89` +- Official catalog tags: `46` +- Official catalog methods/events: `2258` + +## Boundary + +- Do not clone VedAstro wholesale into local code. +- Keep the local adjudicator as the final reasoning layer. +- Default all external outputs to secondary evidence unless explicitly promoted by local tests and contracts. + +## Lane Breakdown + +### Direct Official MCP + +- `MCP / API Surface` [P0] - local=`partial`, adjudicator=`primary`; assets: mcp_server.py, jyotish_api_server.py, strict workflows, vedastro_service_adapter.py, vedastro_official_mcp_bridge.py + route: If official MCP is available, that is the fastest direct agent path; otherwise fall back to local REST adapter. + gap: Local API/MCP surfaces exist and the official public MCP bridge is live; REST adapter official endpoint smoke still depends on configured endpoint-backed execution. + +### Official Python Bridge + +- `Tajika Annual` [P0] - local=`partial`, adjudicator=`secondary`; assets: tajika.py, varshaphala.py, sahams, solar_return + route: Use Python bridge for breadth, but keep year-chart reasoning and labels local. + gap: Annual modules exist; yearly career/wealth/month windows need stronger strict-workflow integration. +- `Ashtakavarga` [P0] - local=`partial`, adjudicator=`secondary`; assets: ashtakavarga, ashtakavarga_pav, ashtakavarga_sodhita, finance_ashtakavarga_bridge + route: Use the Python bridge for broad calculator access while local strict workflows stay authoritative. + gap: SAV/BAV is in production; PAV/Sodhita/Kakshya bridges need continued regression before being dominant labels. +- `Shadbala` [P0] - local=`partial`, adjudicator=`secondary`; assets: shadbala, shadbala_advanced, shadbala_component_cap, oracle_shadbala_queue + route: Broad strength calculators are easiest through the Python bridge, then folded back as oracle evidence. + gap: Local component-aware cap exists; absolute external oracle closure is still incomplete. + +### REST Adapter + +- `EventsAtRange / Life Event Graph` [P0] - local=`partial`, adjudicator=`oracle_only`; assets: transit_trigger, dasha, narayana_dasha, vedastro_service_adapter.range_scan + route: Use official REST adapter first for range scans; keep local adjudicator as the reasoning layer. + gap: Local timing modules exist, but a high-frequency day/hour event graph is not yet a local productized radar. +- `Birth Time ML / Rectification Assistant` [P1] - local=`partial`, adjudicator=`secondary`; assets: birth_time_rectifier.py, rectification_gate, jyotish-app rectification + route: Use VedAstro externally only as supporting evidence; local rectification gate stays in control. + gap: Local rectification exists; ML parity with VedAstro-style service behavior is not established. + +### Local Native Preferred + +- `Prashna / Horary` [P0] - local=`covered`, adjudicator=`secondary`; assets: prashna.py, kp_system.py, upagraha_gulika_maandi, sphuta_trisphuta_family + route: No need to outsource core horary math while local modules already exist. + gap: Horary modules exist but are not yet a first-class question adjudicator route. +- `Jaimini / Chara Dasha` [P0] - local=`covered`, adjudicator=`secondary`; assets: jaimini.py, AK, DK, UL, Chara Dasha, Jaimini marriage bridge v1 + route: Keep Jaimini native; external engines help only as spot-check evidence. + gap: Core Jaimini exists; mission/career/marriage quality adjudicator folding is not yet exhaustive. +- `Report Rendering` [P0] - local=`partial`, adjudicator=`not_used`; assets: report_artifact API, report_builder.py, chart_renderer.py, jyotish-app export + route: Rendering is a local product concern, not a VedAstro dependency. + gap: HTML/PDF artifact path exists; polished SVG/PDF chart rendering and cloud-scale report production are not finished. +- `Synastry / Ashtakoot` [P0] - local=`covered`, adjudicator=`secondary`; assets: synastry.py, ashtakoot.py, 36-point Ashtakoot, 16-factor compatibility + route: Local matching is already stronger than a thin external call unless you need an oracle comparison. + gap: Matching modules exist and API-backed; relationship adjudicator still needs a formal bridge. +- `D1-D60 Divisional Charts` [P0] - local=`covered`, adjudicator=`primary`; assets: varga, varga_full, shodasavarga, divisional_charts_extended + route: Do not pay external-call cost here; local engine is already the primary route. + gap: Local varga coverage is strong; keep VedAstro/PyJHora as benchmark evidence rather than replacing local math. + +### Hybrid Router + +- `Ayanamsa Selection` [P0] - local=`partial`, adjudicator=`oracle_only`; assets: ayanamsa_utils, ephemeris_adapter_contract + route: Prefer local ayanamsa controls for production and use VedAstro only as parity/oracle evidence. + gap: Local Lahiri path is usable, but broad ayanamsa parity and public comparison artifacts remain incomplete. + +### External Evidence Only + +- `Numerology / Non-Jyotish Tools` [P2] - local=`missing`, adjudicator=`not_used`; assets: - + route: Do not implement locally unless it becomes product-critical. + gap: Adjacent product feature; not required for Jyotish adjudicator depth. + +## Immediate Execution Order + +1. Use `Direct Official MCP` where an official MCP surface is available to agents. +2. Use `Official Python Bridge` for broad calculator access that does not need browser/session orchestration. +3. Use `REST Adapter` for range scans and endpoint-style external evidence. +4. Keep `Local Native Preferred` rows local; do not waste time rebuilding what is already stronger here. +5. Use `Hybrid Router` only where parity or oracle closure still matters more than raw breadth. + diff --git a/docs/research/vedastro_parity_matrix_latest.json b/docs/research/vedastro_parity_matrix_latest.json index 0167dbaa..5cb78ec9 100644 --- a/docs/research/vedastro_parity_matrix_latest.json +++ b/docs/research/vedastro_parity_matrix_latest.json @@ -1,7 +1,7 @@ { "scope": "vedastro_parity_matrix", "schema_version": 1, - "generated_at": "2026-06-28T08:31:52.958980+00:00", + "generated_at": "2026-06-28T23:49:39.845848+00:00", "summary": { "row_count": 13, "p0_count": 11, @@ -16,6 +16,14 @@ "new_local_impl": 1, "external_evidence_only": 1 }, + "fastest_path_lane_counts": { + "official_python_bridge": 3, + "hybrid_router": 1, + "local_native_preferred": 5, + "official_mcp": 1, + "rest_adapter": 2, + "external_evidence_only": 1 + }, "local_registry_technique_count": 89 }, "boundary": { @@ -36,10 +44,12 @@ ], "can_call_vedastro": true, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "official_python_bridge", "priority": "P0", "license_boundary": "local_native_or_external_oracle", "adjudicator_use": "secondary", - "gap_notes": "Annual modules exist; yearly career/wealth/month windows need stronger strict-workflow integration." + "gap_notes": "Annual modules exist; yearly career/wealth/month windows need stronger strict-workflow integration.", + "route_notes": "Use Python bridge for breadth, but keep year-chart reasoning and labels local." }, { "vedastro_capability": "Ayanamsa Selection", @@ -51,10 +61,12 @@ ], "can_call_vedastro": true, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "hybrid_router", "priority": "P0", "license_boundary": "external_service_or_clean_room_only", "adjudicator_use": "oracle_only", - "gap_notes": "Local Lahiri path is usable, but broad ayanamsa parity and public comparison artifacts remain incomplete." + "gap_notes": "Local Lahiri path is usable, but broad ayanamsa parity and public comparison artifacts remain incomplete.", + "route_notes": "Prefer local ayanamsa controls for production and use VedAstro only as parity/oracle evidence." }, { "vedastro_capability": "Prashna / Horary", @@ -68,10 +80,12 @@ ], "can_call_vedastro": true, "recommended_path": "local_native", + "fastest_path_lane": "local_native_preferred", "priority": "P0", "license_boundary": "local_native_or_clean_room", "adjudicator_use": "secondary", - "gap_notes": "Horary modules exist but are not yet a first-class question adjudicator route." + "gap_notes": "Horary modules exist but are not yet a first-class question adjudicator route.", + "route_notes": "No need to outsource core horary math while local modules already exist." }, { "vedastro_capability": "Jaimini / Chara Dasha", @@ -87,10 +101,12 @@ ], "can_call_vedastro": true, "recommended_path": "local_native", + "fastest_path_lane": "local_native_preferred", "priority": "P0", "license_boundary": "local_native_mit_attribution_for_reused_parts", "adjudicator_use": "secondary", - "gap_notes": "Core Jaimini exists; mission/career/marriage quality adjudicator folding is not yet exhaustive." + "gap_notes": "Core Jaimini exists; mission/career/marriage quality adjudicator folding is not yet exhaustive.", + "route_notes": "Keep Jaimini native; external engines help only as spot-check evidence." }, { "vedastro_capability": "Report Rendering", @@ -104,10 +120,12 @@ ], "can_call_vedastro": false, "recommended_path": "new_local_impl", + "fastest_path_lane": "local_native_preferred", "priority": "P0", "license_boundary": "local_native", "adjudicator_use": "not_used", - "gap_notes": "HTML/PDF artifact path exists; polished SVG/PDF chart rendering and cloud-scale report production are not finished." + "gap_notes": "HTML/PDF artifact path exists; polished SVG/PDF chart rendering and cloud-scale report production are not finished.", + "route_notes": "Rendering is a local product concern, not a VedAstro dependency." }, { "vedastro_capability": "Synastry / Ashtakoot", @@ -121,10 +139,12 @@ ], "can_call_vedastro": true, "recommended_path": "local_native", + "fastest_path_lane": "local_native_preferred", "priority": "P0", "license_boundary": "local_native_mit_attribution_for_reused_parts", "adjudicator_use": "secondary", - "gap_notes": "Matching modules exist and API-backed; relationship adjudicator still needs a formal bridge." + "gap_notes": "Matching modules exist and API-backed; relationship adjudicator still needs a formal bridge.", + "route_notes": "Local matching is already stronger than a thin external call unless you need an oracle comparison." }, { "vedastro_capability": "MCP / API Surface", @@ -134,14 +154,17 @@ "mcp_server.py", "jyotish_api_server.py", "strict workflows", - "vedastro_service_adapter.py" + "vedastro_service_adapter.py", + "vedastro_official_mcp_bridge.py" ], "can_call_vedastro": true, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "official_mcp", "priority": "P0", "license_boundary": "external_service_or_local_native", "adjudicator_use": "primary", - "gap_notes": "Local API/MCP surfaces exist; VedAstro live adapter needs endpoint-backed smoke tests and provenance capture." + "gap_notes": "Local API/MCP surfaces exist and the official public MCP bridge is live; REST adapter official endpoint smoke still depends on configured endpoint-backed execution.", + "route_notes": "If official MCP is available, that is the fastest direct agent path; otherwise fall back to local REST adapter." }, { "vedastro_capability": "Ashtakavarga", @@ -155,10 +178,12 @@ ], "can_call_vedastro": true, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "official_python_bridge", "priority": "P0", "license_boundary": "local_native_or_external_oracle", "adjudicator_use": "secondary", - "gap_notes": "SAV/BAV is in production; PAV/Sodhita/Kakshya bridges need continued regression before being dominant labels." + "gap_notes": "SAV/BAV is in production; PAV/Sodhita/Kakshya bridges need continued regression before being dominant labels.", + "route_notes": "Use the Python bridge for broad calculator access while local strict workflows stay authoritative." }, { "vedastro_capability": "Shadbala", @@ -172,10 +197,12 @@ ], "can_call_vedastro": true, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "official_python_bridge", "priority": "P0", "license_boundary": "local_native_or_external_oracle", "adjudicator_use": "secondary", - "gap_notes": "Local component-aware cap exists; absolute external oracle closure is still incomplete." + "gap_notes": "Local component-aware cap exists; absolute external oracle closure is still incomplete.", + "route_notes": "Broad strength calculators are easiest through the Python bridge, then folded back as oracle evidence." }, { "vedastro_capability": "EventsAtRange / Life Event Graph", @@ -189,10 +216,12 @@ ], "can_call_vedastro": true, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "rest_adapter", "priority": "P0", "license_boundary": "external_service_or_clean_room_only", "adjudicator_use": "oracle_only", - "gap_notes": "Local timing modules exist, but a high-frequency day/hour event graph is not yet a local productized radar." + "gap_notes": "Local timing modules exist, but a high-frequency day/hour event graph is not yet a local productized radar.", + "route_notes": "Use official REST adapter first for range scans; keep local adjudicator as the reasoning layer." }, { "vedastro_capability": "D1-D60 Divisional Charts", @@ -206,10 +235,12 @@ ], "can_call_vedastro": true, "recommended_path": "local_native", + "fastest_path_lane": "local_native_preferred", "priority": "P0", "license_boundary": "local_native_or_clean_room", "adjudicator_use": "primary", - "gap_notes": "Local varga coverage is strong; keep VedAstro/PyJHora as benchmark evidence rather than replacing local math." + "gap_notes": "Local varga coverage is strong; keep VedAstro/PyJHora as benchmark evidence rather than replacing local math.", + "route_notes": "Do not pay external-call cost here; local engine is already the primary route." }, { "vedastro_capability": "Birth Time ML / Rectification Assistant", @@ -222,10 +253,12 @@ ], "can_call_vedastro": true, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "rest_adapter", "priority": "P1", "license_boundary": "external_service_or_local_native", "adjudicator_use": "secondary", - "gap_notes": "Local rectification exists; ML parity with VedAstro-style service behavior is not established." + "gap_notes": "Local rectification exists; ML parity with VedAstro-style service behavior is not established.", + "route_notes": "Use VedAstro externally only as supporting evidence; local rectification gate stays in control." }, { "vedastro_capability": "Numerology / Non-Jyotish Tools", @@ -234,10 +267,12 @@ "local_assets": [], "can_call_vedastro": true, "recommended_path": "external_evidence_only", + "fastest_path_lane": "external_evidence_only", "priority": "P2", "license_boundary": "external_service_only", "adjudicator_use": "not_used", - "gap_notes": "Adjacent product feature; not required for Jyotish adjudicator depth." + "gap_notes": "Adjacent product feature; not required for Jyotish adjudicator depth.", + "route_notes": "Do not implement locally unless it becomes product-critical." } ] } diff --git a/docs/research/vedastro_parity_matrix_latest.md b/docs/research/vedastro_parity_matrix_latest.md index 148742d3..6bd8f94e 100644 --- a/docs/research/vedastro_parity_matrix_latest.md +++ b/docs/research/vedastro_parity_matrix_latest.md @@ -1,6 +1,6 @@ # VedAstro Parity Matrix -- Generated: `2026-06-28T08:31:52.958980+00:00` +- Generated: `2026-06-28T23:49:39.845848+00:00` - Rows: `13` - P0 rows: `11` - Local registry technique count: `89` @@ -13,24 +13,25 @@ VedAstro calls are external adapter evidence until a capability is promoted by l - Local status counts: `{"covered": 4, "missing": 1, "partial": 8}` - Recommended path counts: `{"external_evidence_only": 1, "hybrid_local_plus_vedastro": 7, "local_native": 4, "new_local_impl": 1}` +- Fastest path lane counts: `{"external_evidence_only": 1, "hybrid_router": 1, "local_native_preferred": 5, "official_mcp": 1, "official_python_bridge": 3, "rest_adapter": 2}` ## Matrix -| VedAstro capability | Category | Local status | Path | Priority | Adjudicator use | Local assets | Gap notes | -|---|---|---:|---|---:|---|---|---| -| Tajika Annual | annual_prediction | partial | hybrid_local_plus_vedastro | P0 | secondary | tajika.py, varshaphala.py, sahams, solar_return | Annual modules exist; yearly career/wealth/month windows need stronger strict-workflow integration. | -| Ayanamsa Selection | ephemeris_policy | partial | hybrid_local_plus_vedastro | P0 | oracle_only | ayanamsa_utils, ephemeris_adapter_contract | Local Lahiri path is usable, but broad ayanamsa parity and public comparison artifacts remain incomplete. | -| Prashna / Horary | horary | covered | local_native | P0 | secondary | prashna.py, kp_system.py, upagraha_gulika_maandi, sphuta_trisphuta_family | Horary modules exist but are not yet a first-class question adjudicator route. | -| Jaimini / Chara Dasha | jaimini | covered | local_native | P0 | secondary | jaimini.py, AK, DK, UL, Chara Dasha, Jaimini marriage bridge v1 | Core Jaimini exists; mission/career/marriage quality adjudicator folding is not yet exhaustive. | -| Report Rendering | presentation | partial | new_local_impl | P0 | not_used | report_artifact API, report_builder.py, chart_renderer.py, jyotish-app export | HTML/PDF artifact path exists; polished SVG/PDF chart rendering and cloud-scale report production are not finished. | -| Synastry / Ashtakoot | relationship_matching | covered | local_native | P0 | secondary | synastry.py, ashtakoot.py, 36-point Ashtakoot, 16-factor compatibility | Matching modules exist and API-backed; relationship adjudicator still needs a formal bridge. | -| MCP / API Surface | service_surface | partial | hybrid_local_plus_vedastro | P0 | primary | mcp_server.py, jyotish_api_server.py, strict workflows, vedastro_service_adapter.py | Local API/MCP surfaces exist; VedAstro live adapter needs endpoint-backed smoke tests and provenance capture. | -| Ashtakavarga | strength | partial | hybrid_local_plus_vedastro | P0 | secondary | ashtakavarga, ashtakavarga_pav, ashtakavarga_sodhita, finance_ashtakavarga_bridge | SAV/BAV is in production; PAV/Sodhita/Kakshya bridges need continued regression before being dominant labels. | -| Shadbala | strength | partial | hybrid_local_plus_vedastro | P0 | secondary | shadbala, shadbala_advanced, shadbala_component_cap, oracle_shadbala_queue | Local component-aware cap exists; absolute external oracle closure is still incomplete. | -| EventsAtRange / Life Event Graph | timing_range_scan | partial | hybrid_local_plus_vedastro | P0 | oracle_only | transit_trigger, dasha, narayana_dasha, vedastro_service_adapter.range_scan | Local timing modules exist, but a high-frequency day/hour event graph is not yet a local productized radar. | -| D1-D60 Divisional Charts | varga | covered | local_native | P0 | primary | varga, varga_full, shodasavarga, divisional_charts_extended | Local varga coverage is strong; keep VedAstro/PyJHora as benchmark evidence rather than replacing local math. | -| Birth Time ML / Rectification Assistant | birth_time_rectification | partial | hybrid_local_plus_vedastro | P1 | secondary | birth_time_rectifier.py, rectification_gate, jyotish-app rectification | Local rectification exists; ML parity with VedAstro-style service behavior is not established. | -| Numerology / Non-Jyotish Tools | adjacent_tools | missing | external_evidence_only | P2 | not_used | - | Adjacent product feature; not required for Jyotish adjudicator depth. | +| VedAstro capability | Category | Local status | Path | Fastest lane | Priority | Adjudicator use | Local assets | Gap notes | +|---|---|---:|---|---|---:|---|---|---| +| Tajika Annual | annual_prediction | partial | hybrid_local_plus_vedastro | official_python_bridge | P0 | secondary | tajika.py, varshaphala.py, sahams, solar_return | Annual modules exist; yearly career/wealth/month windows need stronger strict-workflow integration. | +| Ayanamsa Selection | ephemeris_policy | partial | hybrid_local_plus_vedastro | hybrid_router | P0 | oracle_only | ayanamsa_utils, ephemeris_adapter_contract | Local Lahiri path is usable, but broad ayanamsa parity and public comparison artifacts remain incomplete. | +| Prashna / Horary | horary | covered | local_native | local_native_preferred | P0 | secondary | prashna.py, kp_system.py, upagraha_gulika_maandi, sphuta_trisphuta_family | Horary modules exist but are not yet a first-class question adjudicator route. | +| Jaimini / Chara Dasha | jaimini | covered | local_native | local_native_preferred | P0 | secondary | jaimini.py, AK, DK, UL, Chara Dasha, Jaimini marriage bridge v1 | Core Jaimini exists; mission/career/marriage quality adjudicator folding is not yet exhaustive. | +| Report Rendering | presentation | partial | new_local_impl | local_native_preferred | P0 | not_used | report_artifact API, report_builder.py, chart_renderer.py, jyotish-app export | HTML/PDF artifact path exists; polished SVG/PDF chart rendering and cloud-scale report production are not finished. | +| Synastry / Ashtakoot | relationship_matching | covered | local_native | local_native_preferred | P0 | secondary | synastry.py, ashtakoot.py, 36-point Ashtakoot, 16-factor compatibility | Matching modules exist and API-backed; relationship adjudicator still needs a formal bridge. | +| MCP / API Surface | service_surface | partial | hybrid_local_plus_vedastro | official_mcp | P0 | primary | mcp_server.py, jyotish_api_server.py, strict workflows, vedastro_service_adapter.py, vedastro_official_mcp_bridge.py | Local API/MCP surfaces exist and the official public MCP bridge is live; REST adapter official endpoint smoke still depends on configured endpoint-backed execution. | +| Ashtakavarga | strength | partial | hybrid_local_plus_vedastro | official_python_bridge | P0 | secondary | ashtakavarga, ashtakavarga_pav, ashtakavarga_sodhita, finance_ashtakavarga_bridge | SAV/BAV is in production; PAV/Sodhita/Kakshya bridges need continued regression before being dominant labels. | +| Shadbala | strength | partial | hybrid_local_plus_vedastro | official_python_bridge | P0 | secondary | shadbala, shadbala_advanced, shadbala_component_cap, oracle_shadbala_queue | Local component-aware cap exists; absolute external oracle closure is still incomplete. | +| EventsAtRange / Life Event Graph | timing_range_scan | partial | hybrid_local_plus_vedastro | rest_adapter | P0 | oracle_only | transit_trigger, dasha, narayana_dasha, vedastro_service_adapter.range_scan | Local timing modules exist, but a high-frequency day/hour event graph is not yet a local productized radar. | +| D1-D60 Divisional Charts | varga | covered | local_native | local_native_preferred | P0 | primary | varga, varga_full, shodasavarga, divisional_charts_extended | Local varga coverage is strong; keep VedAstro/PyJHora as benchmark evidence rather than replacing local math. | +| Birth Time ML / Rectification Assistant | birth_time_rectification | partial | hybrid_local_plus_vedastro | rest_adapter | P1 | secondary | birth_time_rectifier.py, rectification_gate, jyotish-app rectification | Local rectification exists; ML parity with VedAstro-style service behavior is not established. | +| Numerology / Non-Jyotish Tools | adjacent_tools | missing | external_evidence_only | external_evidence_only | P2 | not_used | - | Adjacent product feature; not required for Jyotish adjudicator depth. | ## Next Actions diff --git a/scripts/vedastro_fast_path_checklist.py b/scripts/vedastro_fast_path_checklist.py new file mode 100644 index 00000000..708ad449 --- /dev/null +++ b/scripts/vedastro_fast_path_checklist.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +"""Render the fastest practical VedAstro integration checklist. + +This artifact turns the parity matrix plus live official catalog snapshot into +an execution-friendly view: + +- what should go through official MCP +- what should go through the official Python bridge +- what should go through the REST adapter +- what should stay local because the repo is already stronger there +""" + +from __future__ import annotations + +import argparse +import json +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from scripts import vedastro_parity_matrix + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_JSON_PATH = ROOT / "docs" / "research" / "vedastro_fast_path_checklist_latest.json" +DEFAULT_MARKDOWN_PATH = ROOT / "docs" / "research" / "vedastro_fast_path_checklist_latest.md" +CATALOG_PATH = ROOT / "scratch" / "local" / "vedastro_adapter" / "method_catalog_snapshot.json" + +LANE_TITLES = { + "official_mcp": "Direct Official MCP", + "official_python_bridge": "Official Python Bridge", + "rest_adapter": "REST Adapter", + "local_native_preferred": "Local Native Preferred", + "hybrid_router": "Hybrid Router", + "external_evidence_only": "External Evidence Only", +} + + +def _load_catalog_summary() -> dict[str, Any]: + if not CATALOG_PATH.exists(): + return { + "available": False, + "path": str(CATALOG_PATH), + "tag_count": 0, + "method_count": 0, + } + payload = json.loads(CATALOG_PATH.read_text(encoding="utf-8")) + summary = payload.get("summary") or {} + return { + "available": True, + "path": str(CATALOG_PATH), + "tag_count": int(summary.get("tag_count", 0) or 0), + "method_count": int(summary.get("method_count", 0) or 0), + } + + +def build_checklist() -> dict[str, Any]: + matrix = vedastro_parity_matrix.build_matrix() + catalog = _load_catalog_summary() + + grouped: dict[str, list[dict[str, Any]]] = {lane: [] for lane in LANE_TITLES} + for row in matrix["rows"]: + grouped.setdefault(row["fastest_path_lane"], []).append( + { + "capability": row["vedastro_capability"], + "priority": row["priority"], + "recommended_path": row["recommended_path"], + "local_status": row["local_status"], + "adjudicator_use": row["adjudicator_use"], + "route_notes": row["route_notes"], + "gap_notes": row["gap_notes"], + "local_assets": row["local_assets"], + } + ) + + return { + "scope": "vedastro_fast_path_checklist", + "generated_at": datetime.now(timezone.utc).isoformat(), + "summary": { + "row_count": len(matrix["rows"]), + "local_registry_technique_count": matrix["summary"].get("local_registry_technique_count"), + "catalog_tag_count": catalog["tag_count"], + "catalog_method_count": catalog["method_count"], + }, + "boundary": { + "do_not_clone_vedastro": True, + "keep_local_adjudicator_authoritative": True, + "external_outputs_default_to_secondary_context": True, + }, + "official_catalog": catalog, + "lanes": grouped, + } + + +def render_markdown(checklist: dict[str, Any]) -> str: + summary = checklist["summary"] + catalog = checklist["official_catalog"] + lines = [ + "# VedAstro 596+ Nodes Fast-Path Checklist", + "", + f"- Generated: `{checklist['generated_at']}`", + f"- Local registry techniques: `{summary['local_registry_technique_count']}`", + f"- Official catalog tags: `{summary['catalog_tag_count']}`", + f"- Official catalog methods/events: `{summary['catalog_method_count']}`", + "", + "## Boundary", + "", + "- Do not clone VedAstro wholesale into local code.", + "- Keep the local adjudicator as the final reasoning layer.", + "- Default all external outputs to secondary evidence unless explicitly promoted by local tests and contracts.", + "", + "## Lane Breakdown", + "", + ] + + for lane, title in LANE_TITLES.items(): + rows = checklist["lanes"].get(lane) or [] + lines.append(f"### {title}") + lines.append("") + if not rows: + lines.append("- None.") + lines.append("") + continue + for row in rows: + assets = ", ".join(row["local_assets"]) if row["local_assets"] else "-" + lines.append( + f"- `{row['capability']}` [{row['priority']}]" + f" - local=`{row['local_status']}`, adjudicator=`{row['adjudicator_use']}`" + f"; assets: {assets}" + ) + lines.append(f" route: {row['route_notes']}") + lines.append(f" gap: {row['gap_notes']}") + lines.append("") + + lines.extend( + [ + "## Immediate Execution Order", + "", + "1. Use `Direct Official MCP` where an official MCP surface is available to agents.", + "2. Use `Official Python Bridge` for broad calculator access that does not need browser/session orchestration.", + "3. Use `REST Adapter` for range scans and endpoint-style external evidence.", + "4. Keep `Local Native Preferred` rows local; do not waste time rebuilding what is already stronger here.", + "5. Use `Hybrid Router` only where parity or oracle closure still matters more than raw breadth.", + "", + ] + ) + return "\n".join(lines) + + +def write_outputs( + checklist: dict[str, Any], + *, + json_path: Path = DEFAULT_JSON_PATH, + markdown_path: Path = DEFAULT_MARKDOWN_PATH, +) -> None: + json_path.parent.mkdir(parents=True, exist_ok=True) + markdown_path.parent.mkdir(parents=True, exist_ok=True) + json_path.write_text(json.dumps(checklist, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + markdown_path.write_text(render_markdown(checklist) + "\n", encoding="utf-8") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--format", choices=["json", "markdown"], default="json") + parser.add_argument("--write", action="store_true") + args = parser.parse_args(argv) + + checklist = build_checklist() + if args.write: + write_outputs(checklist) + if args.format == "json": + print(json.dumps(checklist, ensure_ascii=False, indent=2)) + else: + print(render_markdown(checklist)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/vedastro_method_catalog_sync.py b/scripts/vedastro_method_catalog_sync.py new file mode 100644 index 00000000..d248ff9a --- /dev/null +++ b/scripts/vedastro_method_catalog_sync.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Sync a lightweight VedAstro method catalog snapshot. + +This script intentionally starts small. It can pull the official event-tag +catalog from VedAstro's public API or write a stubbed snapshot in tests. The +result is a local JSON snapshot suitable for later MCP/Python/REST routing. +""" + +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +from typing import Any +from urllib import request + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_OUTPUT = ROOT / "scratch" / "local" / "vedastro_adapter" / "method_catalog_snapshot.json" +OFFICIAL_TAG_CATALOG_URL = "https://api.vedastro.org/api/Calculate/GetAllEventDataGroupedByTag" +STUB_ENV = "VEDASTRO_METHOD_CATALOG_STUB" + + +def schema() -> dict[str, Any]: + return { + "sync": "vedastro_method_catalog_sync", + "scope": "official_vedastro_method_catalog", + "operations": ["sync_tags", "write_snapshot"], + "sources": { + "official_tag_catalog": OFFICIAL_TAG_CATALOG_URL, + }, + "output_contract": ["source", "summary", "tag_groups"], + } + + +def _load_stubbed_catalog() -> dict[str, Any] | None: + raw = os.environ.get(STUB_ENV, "").strip() + if not raw: + return None + return json.loads(raw) + + +def _fetch_official_tag_catalog() -> dict[str, Any]: + with request.urlopen(OFFICIAL_TAG_CATALOG_URL, timeout=60) as resp: + payload = json.loads(resp.read().decode("utf-8")) + groups = ((payload.get("Payload") or {}).get("GetAllEventDataGroupedByTag") or {}) + return { + "source": "official_tag_catalog", + "tag_groups": groups, + } + + +def build_catalog() -> dict[str, Any]: + catalog = _load_stubbed_catalog() or _fetch_official_tag_catalog() + tag_groups = catalog.get("tag_groups") or {} + method_count = 0 + for events in tag_groups.values(): + if isinstance(events, list): + method_count += len(events) + catalog["summary"] = { + "tag_count": len(tag_groups), + "method_count": method_count, + } + return catalog + + +def write_snapshot(output_path: Path) -> dict[str, Any]: + catalog = build_catalog() + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(catalog, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + return { + "status": "ok", + "source": catalog.get("source"), + "summary": catalog.get("summary"), + "output_path": str(output_path), + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="VedAstro method catalog sync") + parser.add_argument("--print-schema", action="store_true") + parser.add_argument("--write", action="store_true") + parser.add_argument("--output", default=str(DEFAULT_OUTPUT)) + args = parser.parse_args() + + if args.print_schema: + result = schema() + elif args.write: + result = write_snapshot(Path(args.output)) + else: + result = build_catalog() + + print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/vedastro_official_mcp_bridge.py b/scripts/vedastro_official_mcp_bridge.py new file mode 100644 index 00000000..d7268b1e --- /dev/null +++ b/scripts/vedastro_official_mcp_bridge.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Thin bridge to the official public VedAstro MCP endpoint. + +This bridge is intentionally narrow. It proves that the official public MCP +surface is reachable and callable from the local workspace without letting +external responses silently override local adjudication. +""" + +from __future__ import annotations + +import argparse +import json +import urllib.error +import urllib.request +from typing import Any + + +DEFAULT_ENDPOINT = "https://mcp.vedastro.org/api/mcp/public" +DEFAULT_PROTOCOL_VERSION = "2025-06-18" +DEFAULT_TIMEOUT_SECONDS = 60 + + +def _post_json(endpoint: str, payload: dict[str, Any], timeout: float = DEFAULT_TIMEOUT_SECONDS) -> tuple[dict[str, Any], dict[str, str]]: + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + req = urllib.request.Request( + endpoint, + data=body, + method="POST", + headers={ + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + }, + ) + with urllib.request.urlopen(req, timeout=timeout) as resp: + raw = resp.read().decode("utf-8") + headers = {key.lower(): value for key, value in resp.headers.items()} + return json.loads(raw), headers + + +def _initialize(endpoint: str) -> dict[str, Any]: + payload = { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": DEFAULT_PROTOCOL_VERSION, + "capabilities": {}, + "clientInfo": { + "name": "jyotish-local-bridge", + "version": "1.0.0", + }, + }, + } + result, headers = _post_json(endpoint, payload) + session_id = headers.get("mcp-session-id") or headers.get("x-mcp-session-id") + return { + "endpoint": endpoint, + "available": True, + "status": "ok", + "operation": "initialize", + "session_id": session_id, + "result": result.get("result") or result, + "source": "official_public_mcp", + } + + +def _tools_list(endpoint: str) -> dict[str, Any]: + init = _initialize(endpoint) + payload = { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + "params": {}, + } + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + headers = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + if init.get("session_id"): + headers["Mcp-Session-Id"] = str(init["session_id"]) + req = urllib.request.Request(endpoint, data=body, method="POST", headers=headers) + with urllib.request.urlopen(req, timeout=DEFAULT_TIMEOUT_SECONDS) as resp: + raw = resp.read().decode("utf-8") + response_headers = {key.lower(): value for key, value in resp.headers.items()} + result = json.loads(raw) + tools = (result.get("result") or {}).get("tools") or [] + return { + "endpoint": endpoint, + "available": True, + "status": "ok", + "operation": "tools_list", + "session_id": init.get("session_id") or response_headers.get("mcp-session-id"), + "tool_count": len(tools), + "tool_names": [tool.get("name") for tool in tools if isinstance(tool, dict) and tool.get("name")], + "result": result.get("result") or result, + "source": "official_public_mcp", + } + + +def schema() -> dict[str, Any]: + return { + "bridge": "vedastro_official_mcp_bridge", + "role": "official_public_mcp_thin_bridge", + "endpoint": DEFAULT_ENDPOINT, + "operations": ["initialize", "tools_list"], + "response_contract": [ + "endpoint", + "available", + "status", + "operation", + "result", + "source", + ], + "boundaries": [ + "Use for official MCP reachability and tool discovery.", + "Do not let raw MCP results directly override local score/dominant_label/payout_label.", + "Promote only through explicit local contracts and tests.", + ], + } + + +def _error_result(endpoint: str, operation: str, exc: Exception) -> dict[str, Any]: + reason = str(exc) + if isinstance(exc, urllib.error.HTTPError): + reason = f"http_{exc.code}: {reason}" + return { + "endpoint": endpoint, + "available": False, + "status": "mcp_request_failed", + "operation": operation, + "reason": reason, + "source": "official_public_mcp", + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="Official public VedAstro MCP bridge") + parser.add_argument("--print-schema", action="store_true") + parser.add_argument("--endpoint", default=DEFAULT_ENDPOINT) + parser.add_argument("--operation", choices=["initialize", "tools_list"], default="tools_list") + args = parser.parse_args() + + if args.print_schema: + result = schema() + else: + try: + if args.operation == "initialize": + result = _initialize(args.endpoint) + else: + result = _tools_list(args.endpoint) + except Exception as exc: # noqa: BLE001 + result = _error_result(args.endpoint, args.operation, exc) + + print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/vedastro_parity_matrix.py b/scripts/vedastro_parity_matrix.py index 687ce230..9a249187 100644 --- a/scripts/vedastro_parity_matrix.py +++ b/scripts/vedastro_parity_matrix.py @@ -29,6 +29,15 @@ ALLOWED_RECOMMENDED_PATHS = { "hybrid_local_plus_vedastro", } +ALLOWED_FASTEST_PATH_LANES = { + "local_native_preferred", + "official_mcp", + "official_python_bridge", + "rest_adapter", + "hybrid_router", + "external_evidence_only", +} + VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ { @@ -38,10 +47,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ "local_assets": ["transit_trigger", "dasha", "narayana_dasha", "vedastro_service_adapter.range_scan"], "can_call_vedastro": True, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "rest_adapter", "priority": "P0", "license_boundary": "external_service_or_clean_room_only", "adjudicator_use": "oracle_only", "gap_notes": "Local timing modules exist, but a high-frequency day/hour event graph is not yet a local productized radar.", + "route_notes": "Use official REST adapter first for range scans; keep local adjudicator as the reasoning layer.", }, { "vedastro_capability": "Ayanamsa Selection", @@ -50,10 +61,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ "local_assets": ["ayanamsa_utils", "ephemeris_adapter_contract"], "can_call_vedastro": True, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "hybrid_router", "priority": "P0", "license_boundary": "external_service_or_clean_room_only", "adjudicator_use": "oracle_only", "gap_notes": "Local Lahiri path is usable, but broad ayanamsa parity and public comparison artifacts remain incomplete.", + "route_notes": "Prefer local ayanamsa controls for production and use VedAstro only as parity/oracle evidence.", }, { "vedastro_capability": "D1-D60 Divisional Charts", @@ -62,10 +75,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ "local_assets": ["varga", "varga_full", "shodasavarga", "divisional_charts_extended"], "can_call_vedastro": True, "recommended_path": "local_native", + "fastest_path_lane": "local_native_preferred", "priority": "P0", "license_boundary": "local_native_or_clean_room", "adjudicator_use": "primary", "gap_notes": "Local varga coverage is strong; keep VedAstro/PyJHora as benchmark evidence rather than replacing local math.", + "route_notes": "Do not pay external-call cost here; local engine is already the primary route.", }, { "vedastro_capability": "Ashtakavarga", @@ -74,10 +89,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ "local_assets": ["ashtakavarga", "ashtakavarga_pav", "ashtakavarga_sodhita", "finance_ashtakavarga_bridge"], "can_call_vedastro": True, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "official_python_bridge", "priority": "P0", "license_boundary": "local_native_or_external_oracle", "adjudicator_use": "secondary", "gap_notes": "SAV/BAV is in production; PAV/Sodhita/Kakshya bridges need continued regression before being dominant labels.", + "route_notes": "Use the Python bridge for broad calculator access while local strict workflows stay authoritative.", }, { "vedastro_capability": "Shadbala", @@ -86,10 +103,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ "local_assets": ["shadbala", "shadbala_advanced", "shadbala_component_cap", "oracle_shadbala_queue"], "can_call_vedastro": True, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "official_python_bridge", "priority": "P0", "license_boundary": "local_native_or_external_oracle", "adjudicator_use": "secondary", "gap_notes": "Local component-aware cap exists; absolute external oracle closure is still incomplete.", + "route_notes": "Broad strength calculators are easiest through the Python bridge, then folded back as oracle evidence.", }, { "vedastro_capability": "Jaimini / Chara Dasha", @@ -98,10 +117,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ "local_assets": ["jaimini.py", "AK", "DK", "UL", "Chara Dasha", "Jaimini marriage bridge v1"], "can_call_vedastro": True, "recommended_path": "local_native", + "fastest_path_lane": "local_native_preferred", "priority": "P0", "license_boundary": "local_native_mit_attribution_for_reused_parts", "adjudicator_use": "secondary", "gap_notes": "Core Jaimini exists; mission/career/marriage quality adjudicator folding is not yet exhaustive.", + "route_notes": "Keep Jaimini native; external engines help only as spot-check evidence.", }, { "vedastro_capability": "Synastry / Ashtakoot", @@ -110,10 +131,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ "local_assets": ["synastry.py", "ashtakoot.py", "36-point Ashtakoot", "16-factor compatibility"], "can_call_vedastro": True, "recommended_path": "local_native", + "fastest_path_lane": "local_native_preferred", "priority": "P0", "license_boundary": "local_native_mit_attribution_for_reused_parts", "adjudicator_use": "secondary", "gap_notes": "Matching modules exist and API-backed; relationship adjudicator still needs a formal bridge.", + "route_notes": "Local matching is already stronger than a thin external call unless you need an oracle comparison.", }, { "vedastro_capability": "Tajika Annual", @@ -122,10 +145,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ "local_assets": ["tajika.py", "varshaphala.py", "sahams", "solar_return"], "can_call_vedastro": True, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "official_python_bridge", "priority": "P0", "license_boundary": "local_native_or_external_oracle", "adjudicator_use": "secondary", "gap_notes": "Annual modules exist; yearly career/wealth/month windows need stronger strict-workflow integration.", + "route_notes": "Use Python bridge for breadth, but keep year-chart reasoning and labels local.", }, { "vedastro_capability": "Prashna / Horary", @@ -134,10 +159,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ "local_assets": ["prashna.py", "kp_system.py", "upagraha_gulika_maandi", "sphuta_trisphuta_family"], "can_call_vedastro": True, "recommended_path": "local_native", + "fastest_path_lane": "local_native_preferred", "priority": "P0", "license_boundary": "local_native_or_clean_room", "adjudicator_use": "secondary", "gap_notes": "Horary modules exist but are not yet a first-class question adjudicator route.", + "route_notes": "No need to outsource core horary math while local modules already exist.", }, { "vedastro_capability": "Report Rendering", @@ -146,22 +173,32 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ "local_assets": ["report_artifact API", "report_builder.py", "chart_renderer.py", "jyotish-app export"], "can_call_vedastro": False, "recommended_path": "new_local_impl", + "fastest_path_lane": "local_native_preferred", "priority": "P0", "license_boundary": "local_native", "adjudicator_use": "not_used", "gap_notes": "HTML/PDF artifact path exists; polished SVG/PDF chart rendering and cloud-scale report production are not finished.", + "route_notes": "Rendering is a local product concern, not a VedAstro dependency.", }, { "vedastro_capability": "MCP / API Surface", "category": "service_surface", "domains": ["event", "relationship", "wealth", "career"], - "local_assets": ["mcp_server.py", "jyotish_api_server.py", "strict workflows", "vedastro_service_adapter.py"], + "local_assets": [ + "mcp_server.py", + "jyotish_api_server.py", + "strict workflows", + "vedastro_service_adapter.py", + "vedastro_official_mcp_bridge.py", + ], "can_call_vedastro": True, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "official_mcp", "priority": "P0", "license_boundary": "external_service_or_local_native", "adjudicator_use": "primary", - "gap_notes": "Local API/MCP surfaces exist; VedAstro live adapter needs endpoint-backed smoke tests and provenance capture.", + "gap_notes": "Local API/MCP surfaces exist and the official public MCP bridge is live; REST adapter official endpoint smoke still depends on configured endpoint-backed execution.", + "route_notes": "If official MCP is available, that is the fastest direct agent path; otherwise fall back to local REST adapter.", }, { "vedastro_capability": "Numerology / Non-Jyotish Tools", @@ -170,10 +207,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ "local_assets": [], "can_call_vedastro": True, "recommended_path": "external_evidence_only", + "fastest_path_lane": "external_evidence_only", "priority": "P2", "license_boundary": "external_service_only", "adjudicator_use": "not_used", "gap_notes": "Adjacent product feature; not required for Jyotish adjudicator depth.", + "route_notes": "Do not implement locally unless it becomes product-critical.", }, { "vedastro_capability": "Birth Time ML / Rectification Assistant", @@ -182,10 +221,12 @@ VEDASTRO_CAPABILITY_SEEDS: list[dict[str, Any]] = [ "local_assets": ["birth_time_rectifier.py", "rectification_gate", "jyotish-app rectification"], "can_call_vedastro": True, "recommended_path": "hybrid_local_plus_vedastro", + "fastest_path_lane": "rest_adapter", "priority": "P1", "license_boundary": "external_service_or_local_native", "adjudicator_use": "secondary", "gap_notes": "Local rectification exists; ML parity with VedAstro-style service behavior is not established.", + "route_notes": "Use VedAstro externally only as supporting evidence; local rectification gate stays in control.", }, ] @@ -232,10 +273,12 @@ def _build_row(seed: dict[str, Any], audit: dict[str, Any]) -> dict[str, Any]: "local_assets": list(seed["local_assets"]), "can_call_vedastro": bool(seed["can_call_vedastro"]), "recommended_path": recommended_path, + "fastest_path_lane": seed["fastest_path_lane"], "priority": seed["priority"], "license_boundary": seed["license_boundary"], "adjudicator_use": seed["adjudicator_use"], "gap_notes": seed["gap_notes"], + "route_notes": seed["route_notes"], } @@ -246,10 +289,12 @@ def build_matrix(audit: dict[str, Any] | None = None) -> dict[str, Any]: status_counts: dict[str, int] = {} path_counts: dict[str, int] = {} + lane_counts: dict[str, int] = {} priority_counts: dict[str, int] = {} for row in rows: status_counts[row["local_status"]] = status_counts.get(row["local_status"], 0) + 1 path_counts[row["recommended_path"]] = path_counts.get(row["recommended_path"], 0) + 1 + lane_counts[row["fastest_path_lane"]] = lane_counts.get(row["fastest_path_lane"], 0) + 1 priority_counts[row["priority"]] = priority_counts.get(row["priority"], 0) + 1 return { @@ -261,6 +306,7 @@ def build_matrix(audit: dict[str, Any] | None = None) -> dict[str, Any]: "p0_count": priority_counts.get("P0", 0), "status_counts": status_counts, "recommended_path_counts": path_counts, + "fastest_path_lane_counts": lane_counts, "local_registry_technique_count": audit.get("technique_count"), }, "boundary": { @@ -290,16 +336,17 @@ def render_markdown(matrix: dict[str, Any]) -> str: "", f"- Local status counts: `{json.dumps(summary['status_counts'], ensure_ascii=False, sort_keys=True)}`", f"- Recommended path counts: `{json.dumps(summary['recommended_path_counts'], ensure_ascii=False, sort_keys=True)}`", + f"- Fastest path lane counts: `{json.dumps(summary['fastest_path_lane_counts'], ensure_ascii=False, sort_keys=True)}`", "", "## Matrix", "", - "| VedAstro capability | Category | Local status | Path | Priority | Adjudicator use | Local assets | Gap notes |", - "|---|---|---:|---|---:|---|---|---|", + "| VedAstro capability | Category | Local status | Path | Fastest lane | Priority | Adjudicator use | Local assets | Gap notes |", + "|---|---|---:|---|---|---:|---|---|---|", ] for row in matrix["rows"]: assets = ", ".join(row["local_assets"]) if row["local_assets"] else "-" lines.append( - "| {vedastro_capability} | {category} | {local_status} | {recommended_path} | {priority} | {adjudicator_use} | {assets} | {gap_notes} |".format( + "| {vedastro_capability} | {category} | {local_status} | {recommended_path} | {fastest_path_lane} | {priority} | {adjudicator_use} | {assets} | {gap_notes} |".format( assets=assets, **row, ) diff --git a/scripts/vedastro_python_bridge.py b/scripts/vedastro_python_bridge.py new file mode 100644 index 00000000..3bda4072 --- /dev/null +++ b/scripts/vedastro_python_bridge.py @@ -0,0 +1,480 @@ +#!/usr/bin/env python3 +"""Thin bridge for the official VedAstro Python package. + +This bridge exists to accelerate broad method coverage through the official +Python SDK while keeping the local adjudicator and REST adapter boundaries +separate. It stays deliberately thin: + +- discover the real VedAstro runtime (current interpreter or local venv) +- route generic method calls into the official Python surface +- support a tiny typed-parameter contract for enums and Time/GeoLocation +- fail in a controlled way when the package is unavailable +""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import os +import subprocess +import sys +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +PACKAGE_NAME = "VedAstro" +MODULE_CANDIDATES = ("vedastro", "VedAstro") +FORCE_UNAVAILABLE_ENV = "VEDASTRO_PYTHON_FORCE_UNAVAILABLE" +STUB_RESULT_ENV = "VEDASTRO_PYTHON_BRIDGE_STUB_RESULT" +PYTHON_BIN_ENV = "VEDASTRO_PYTHON_BIN" +DEFAULT_VENV_PYTHONS = ( + ROOT / "venv_vedastro" / "bin" / "python3.11", + ROOT / "venv_vedastro" / "bin" / "python3", + ROOT / "venv_vedastro" / "bin" / "python", +) + +CHILD_RUNNER = r""" +import contextlib +import enum +import importlib +import io +import json +import sys + +MODULE_CANDIDATES = ("vedastro", "VedAstro") + + +def _offset_to_string(offset): + if isinstance(offset, str): + raw = offset.strip() + if not raw: + return "+00:00" + if raw[0] in "+-" and ":" in raw: + return raw + try: + offset = float(raw) + except ValueError: + return raw + + if isinstance(offset, (int, float)): + sign = "+" if offset >= 0 else "-" + absolute = abs(float(offset)) + hours = int(absolute) + minutes = int(round((absolute - hours) * 60)) + if minutes == 60: + hours += 1 + minutes = 0 + return f"{sign}{hours:02d}:{minutes:02d}" + + return "+00:00" + + +def _import_module(): + for name in MODULE_CANDIDATES: + with contextlib.redirect_stdout(io.StringIO()): + try: + return name, importlib.import_module(name) + except ModuleNotFoundError: + continue + return None, None + + +def _coerce(module, value): + if isinstance(value, list): + return [_coerce(module, item) for item in value] + if isinstance(value, dict): + enum_name = value.get("__vedastro_enum__") + if enum_name: + enum_type = getattr(module, enum_name) + return getattr(enum_type, value["value"]) + + type_name = value.get("__vedastro_type__") + if type_name == "GeoLocation": + return module.GeoLocation( + value["location_name"], + value["longitude"], + value["latitude"], + ) + if type_name == "Time": + geolocation = _coerce(module, value["geolocation"]) if value.get("geolocation") else None + if value.get("time_string") is not None: + return module.Time(value["time_string"], geolocation) + time_string = ( + f"{int(value['hour']):02d}:{int(value['minute']):02d} " + f"{int(value['day']):02d}/{int(value['month']):02d}/{int(value['year'])} " + f"{_offset_to_string(value.get('offset', '+00:00'))}" + ) + return module.Time(time_string, geolocation) + + if type_name: + target = getattr(module, type_name) + args = [_coerce(module, item) for item in value.get("args", [])] + kwargs = { + key: _coerce(module, item) + for key, item in value.get("kwargs", {}).items() + } + if args or kwargs: + return target(*args, **kwargs) + payload = { + key: _coerce(module, item) + for key, item in value.items() + if not key.startswith("__") + } + return target(**payload) + + return { + key: _coerce(module, item) + for key, item in value.items() + } + return value + + +def _serialize(value): + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, enum.Enum): + return { + "type": type(value).__name__, + "name": value.name, + "value": value.value, + } + if isinstance(value, (list, tuple, set)): + return [_serialize(item) for item in value] + if isinstance(value, dict): + return {str(key): _serialize(item) for key, item in value.items()} + if hasattr(value, "to_json"): + try: + payload = value.to_json() + if isinstance(payload, str): + try: + return json.loads(payload) + except json.JSONDecodeError: + return payload + return _serialize(payload) + except Exception: + pass + if hasattr(value, "__dict__"): + return { + key: _serialize(item) + for key, item in vars(value).items() + if not key.startswith("_") + } + return str(value) + + +def _resolve_callable(module, method_name): + parts = [part for part in method_name.split(".") if part] + if not parts: + raise AttributeError("empty_method_name") + if len(parts) == 1: + return getattr(module.Calculate, parts[0]) + + current = module + for part in parts: + current = getattr(current, part) + return current + + +def main(): + method = sys.argv[1] + params = json.loads(sys.argv[2]) + module_name, module = _import_module() + if module is None: + print(json.dumps({ + "available": False, + "status": "python_package_not_installed", + "method": method, + "source": "vedastro_python_bridge_child", + })) + return + + call_target = _resolve_callable(module, method) + + args = [] + kwargs = {} + if isinstance(params, dict) and ("args" in params or "kwargs" in params): + args = [_coerce(module, item) for item in params.get("args", [])] + kwargs = { + key: _coerce(module, item) + for key, item in params.get("kwargs", {}).items() + } + elif isinstance(params, dict): + kwargs = { + key: _coerce(module, item) + for key, item in params.items() + } + elif isinstance(params, list): + args = [_coerce(module, item) for item in params] + elif params is not None: + args = [_coerce(module, params)] + + with contextlib.redirect_stdout(io.StringIO()): + result = call_target(*args, **kwargs) + + print(json.dumps({ + "available": True, + "status": "ok", + "method": method, + "module_name": module_name, + "result": _serialize(result), + "source": "vedastro_python_bridge", + })) + + +if __name__ == "__main__": + main() +""" + + +def _package_available() -> bool: + if os.environ.get(FORCE_UNAVAILABLE_ENV, "").strip().lower() in {"1", "true", "yes"}: + return False + return _discover_module_name() is not None + + +def _discover_module_name() -> str | None: + for candidate in MODULE_CANDIDATES: + if importlib.util.find_spec(candidate) is not None: + return candidate + return None + + +def _candidate_python_bins() -> list[str]: + bins: list[str] = [] + + configured = os.environ.get(PYTHON_BIN_ENV, "").strip() + if configured: + bins.append(configured) + + for candidate in DEFAULT_VENV_PYTHONS: + if candidate.exists(): + bins.append(str(candidate)) + + bins.append(sys.executable) + + seen: set[str] = set() + ordered: list[str] = [] + for item in bins: + if item and item not in seen: + ordered.append(item) + seen.add(item) + return ordered + + +def _python_supports_vedastro(python_bin: str) -> bool: + probe = ( + "import importlib.util; " + "mods=('vedastro','VedAstro'); " + "raise SystemExit(0 if any(importlib.util.find_spec(m) for m in mods) else 1)" + ) + try: + completed = subprocess.run( + [python_bin, "-c", probe], + check=False, + capture_output=True, + text=True, + timeout=20, + ) + except OSError: + return False + return completed.returncode == 0 + + +def _select_python_bin() -> str | None: + for python_bin in _candidate_python_bins(): + if _python_supports_vedastro(python_bin): + return python_bin + return None + + +def _call_via_child_python(python_bin: str, method: str, params: dict[str, Any]) -> dict[str, Any]: + child_env = os.environ.copy() + completed = subprocess.run( + [python_bin, "-c", CHILD_RUNNER, method, json.dumps(params, ensure_ascii=False)], + check=False, + capture_output=True, + text=True, + timeout=180, + env=child_env, + ) + if completed.returncode != 0: + return { + "available": False, + "status": "bridge_runtime_error", + "method": method, + "python_bin": python_bin, + "stderr": (completed.stderr or "").strip(), + "stdout_excerpt": (completed.stdout or "").strip()[:500], + "source": "vedastro_python_bridge", + } + + try: + payload = json.loads(completed.stdout) + except json.JSONDecodeError: + return { + "available": False, + "status": "bridge_invalid_json", + "method": method, + "python_bin": python_bin, + "stdout_excerpt": (completed.stdout or "").strip()[:500], + "stderr": (completed.stderr or "").strip()[:500], + "source": "vedastro_python_bridge", + } + + payload["python_bin"] = python_bin + return payload + + +def schema() -> dict[str, Any]: + return { + "bridge": "vedastro_python_bridge", + "package_name": PACKAGE_NAME, + "module_candidates": list(MODULE_CANDIDATES), + "intended_role": "python_sdk_bulk_calculation_bridge", + "operations": ["call_method"], + "request_contract": ["method", "params_json"], + "typed_param_contract": { + "enum": {"__vedastro_enum__": "PlanetName", "value": "Sun"}, + "geo": { + "__vedastro_type__": "GeoLocation", + "location_name": "REDACTED_PLACE", + "longitude": 114.46, + "latitude": 36.6, + }, + "time": { + "__vedastro_type__": "Time", + "year": REDACTED_YEAR, + "month": 4, + "day": 17, + "hour": 14, + "minute": 49, + "offset": 8, + "geolocation": { + "__vedastro_type__": "GeoLocation", + "location_name": "REDACTED_PLACE", + "longitude": 114.46, + "latitude": 36.6, + }, + }, + }, + "response_contract": ["available", "status", "method", "result", "source", "python_bin"], + "boundaries": [ + "Generic method routing only; local adjudicator remains authoritative.", + "Do not treat Python SDK output as score/dominant label override by default.", + ], + } + + +def _missing_package_result(method: str) -> dict[str, Any]: + return { + "available": False, + "status": "python_package_not_installed", + "method": method, + "reason": ( + f"Official Python package {PACKAGE_NAME} is not installed in the current runtime " + "or detected project venv." + ), + "source": "vedastro_python_bridge", + } + + +def call_method(method: str, params: dict[str, Any]) -> dict[str, Any]: + stub = os.environ.get(STUB_RESULT_ENV, "").strip() + if stub: + payload = json.loads(stub) + return { + "available": True, + "status": "ok", + "method": payload.get("method") or method, + "result": payload.get("result"), + "source": "stubbed_bridge_result", + } + + if os.environ.get(FORCE_UNAVAILABLE_ENV, "").strip().lower() in {"1", "true", "yes"}: + return _missing_package_result(method) + + python_bin = _select_python_bin() + if not python_bin: + return _missing_package_result(method) + return _call_via_child_python(python_bin, method, params) + + +def call_high_value(method_key: str, payload: dict[str, Any]) -> dict[str, Any]: + if method_key == "event_tag_catalog": + return call_method("GetAllEventDataGroupedByTag", {}) + + if method_key == "planet_longitude": + planet = payload["planet"] + time = payload["time"] + return call_method( + "PlanetNirayanaLongitude", + { + "args": [ + {"__vedastro_enum__": "PlanetName", "value": planet}, + time, + ] + }, + ) + + if method_key == "vimshottari_snapshot": + birth_time = payload["birth_time"] + check_time = payload["check_time"] + levels = int(payload.get("levels", 3)) + return call_method("DasaAtTime", {"args": [birth_time, check_time, levels]}) + + if method_key == "chara_dasha_snapshot": + birth_time = payload["birth_time"] + check_time = payload["check_time"] + return call_method("GetCharaDasaAtTime", {"args": [birth_time, check_time]}) + + return { + "available": False, + "status": "unsupported_high_value_method", + "method": method_key, + "source": "vedastro_python_bridge", + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="VedAstro Python bridge") + parser.add_argument("--print-schema", action="store_true") + parser.add_argument("--method", default="") + parser.add_argument("--params-json", default="{}") + parser.add_argument("--high-value", default="") + args = parser.parse_args() + + if args.print_schema: + result = schema() + result["high_value_methods"] = { + "event_tag_catalog": { + "maps_to": "GetAllEventDataGroupedByTag", + "request_contract": [], + }, + "planet_longitude": { + "maps_to": "PlanetNirayanaLongitude", + "request_contract": ["planet", "time"], + }, + "vimshottari_snapshot": { + "maps_to": "DasaAtTime", + "request_contract": ["birth_time", "check_time", "levels?"], + }, + "chara_dasha_snapshot": { + "maps_to": "GetCharaDasaAtTime", + "request_contract": ["birth_time", "check_time"], + }, + } + elif args.high_value: + params = json.loads(args.params_json or "{}") + result = call_high_value(args.high_value, params) + else: + params = json.loads(args.params_json or "{}") + result = call_method(args.method, params) + + print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test-data/vedastro_range_scan_official_sample.json b/tests/test-data/vedastro_range_scan_official_sample.json new file mode 100644 index 00000000..9571ebd7 --- /dev/null +++ b/tests/test-data/vedastro_range_scan_official_sample.json @@ -0,0 +1,37 @@ +{ + "Status": "Pass", + "Payload": [ + { + "Name": "JupiterSupportsMarriageAxis", + "Nature": "Good", + "Description": "Marriage support window with Jupiter blessing the partnership axis.", + "StartTime": "2026-05-01", + "EndTime": "2026-06-01", + "EventTags": ["Marriage", "Personal"] + }, + { + "Name": "GoodForMarriage", + "Nature": "Good", + "Description": "General auspicious support for marriage formalization.", + "StartTime": "2026-05-02", + "EndTime": "2026-05-03", + "EventTags": ["Marriage"] + }, + { + "Name": "PartnershipBlessingWindow", + "Nature": "Good", + "Description": "Spouse and relationship alignment is favored in this period.", + "StartTime": "2026-05-04", + "EndTime": "2026-05-05", + "EventTags": ["Personal"] + }, + { + "Name": "RandomMoonMoodShift", + "Nature": "Neutral", + "Description": "Generic transit noise without relationship significance.", + "StartTime": "2026-05-08", + "EndTime": "2026-05-09", + "EventTags": ["General", "Gochara"] + } + ] +} diff --git a/tests/test_vedastro_fast_path_checklist.py b/tests/test_vedastro_fast_path_checklist.py new file mode 100644 index 00000000..17dc5c5f --- /dev/null +++ b/tests/test_vedastro_fast_path_checklist.py @@ -0,0 +1,48 @@ +import json + +from scripts import vedastro_fast_path_checklist + + +def test_build_checklist_exposes_all_fast_path_lanes(): + checklist = vedastro_fast_path_checklist.build_checklist() + + assert checklist["scope"] == "vedastro_fast_path_checklist" + assert checklist["boundary"]["do_not_clone_vedastro"] is True + assert "official_mcp" in checklist["lanes"] + assert "official_python_bridge" in checklist["lanes"] + assert "rest_adapter" in checklist["lanes"] + assert "local_native_preferred" in checklist["lanes"] + + +def test_checklist_routes_high_value_capabilities_to_expected_lanes(): + checklist = vedastro_fast_path_checklist.build_checklist() + + def capabilities(lane: str) -> set[str]: + return {item["capability"] for item in checklist["lanes"][lane]} + + assert "MCP / API Surface" in capabilities("official_mcp") + assert "Shadbala" in capabilities("official_python_bridge") + assert "EventsAtRange / Life Event Graph" in capabilities("rest_adapter") + assert "D1-D60 Divisional Charts" in capabilities("local_native_preferred") + + +def test_render_markdown_contains_execution_order_and_catalog_summary(): + checklist = vedastro_fast_path_checklist.build_checklist() + markdown = vedastro_fast_path_checklist.render_markdown(checklist) + + assert "# VedAstro 596+ Nodes Fast-Path Checklist" in markdown + assert "Official catalog methods/events" in markdown + assert "## Immediate Execution Order" in markdown + assert "Official Python Bridge" in markdown + + +def test_write_outputs_creates_latest_files(tmp_path): + checklist = vedastro_fast_path_checklist.build_checklist() + json_path = tmp_path / "checklist.json" + md_path = tmp_path / "checklist.md" + + vedastro_fast_path_checklist.write_outputs(checklist, json_path=json_path, markdown_path=md_path) + + loaded = json.loads(json_path.read_text(encoding="utf-8")) + assert loaded["scope"] == "vedastro_fast_path_checklist" + assert md_path.read_text(encoding="utf-8").startswith("# VedAstro 596+ Nodes Fast-Path Checklist") diff --git a/tests/test_vedastro_method_catalog_sync.py b/tests/test_vedastro_method_catalog_sync.py new file mode 100644 index 00000000..6a2e0381 --- /dev/null +++ b/tests/test_vedastro_method_catalog_sync.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_vedastro_method_catalog_sync_schema_is_declared() -> None: + completed = subprocess.run( + [sys.executable, "scripts/vedastro_method_catalog_sync.py", "--print-schema"], + cwd=ROOT, + text=True, + capture_output=True, + timeout=120, + check=False, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["sync"] == "vedastro_method_catalog_sync" + assert report["scope"] == "official_vedastro_method_catalog" + assert "sync_tags" in report["operations"] + assert "write_snapshot" in report["operations"] + + +def test_vedastro_method_catalog_sync_can_write_stubbed_snapshot() -> None: + env = os.environ.copy() + env["VEDASTRO_METHOD_CATALOG_STUB"] = json.dumps( + { + "source": "stubbed_catalog", + "tag_groups": { + "Marriage": [{"Name": "GoodForMarriage", "CalculatorMethod": "GoodForMarriage"}], + "LendingMoney": [{"Name": "GoodForLendingMoney", "CalculatorMethod": "GoodForLendingMoney"}], + }, + } + ) + output_path = ROOT / "scratch" / "local" / "vedastro_adapter" / "test_method_catalog_snapshot.json" + completed = subprocess.run( + [ + sys.executable, + "scripts/vedastro_method_catalog_sync.py", + "--write", + "--output", + str(output_path), + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=120, + check=False, + env=env, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["status"] == "ok" + assert report["source"] == "stubbed_catalog" + assert report["summary"]["tag_count"] == 2 + assert report["summary"]["method_count"] == 2 + snapshot = json.loads(output_path.read_text(encoding="utf-8")) + assert "Marriage" in snapshot["tag_groups"] + assert snapshot["summary"]["method_count"] == 2 diff --git a/tests/test_vedastro_official_mcp_bridge.py b/tests/test_vedastro_official_mcp_bridge.py new file mode 100644 index 00000000..afe96c08 --- /dev/null +++ b/tests/test_vedastro_official_mcp_bridge.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import json +import subprocess +import sys +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_vedastro_official_mcp_bridge_schema_is_declared() -> None: + completed = subprocess.run( + [sys.executable, "scripts/vedastro_official_mcp_bridge.py", "--print-schema"], + cwd=ROOT, + text=True, + capture_output=True, + timeout=120, + check=False, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["bridge"] == "vedastro_official_mcp_bridge" + assert report["role"] == "official_public_mcp_thin_bridge" + assert "initialize" in report["operations"] + assert "tools_list" in report["operations"] + + +def test_vedastro_official_mcp_bridge_can_list_tools_against_mock_server() -> None: + class Handler(BaseHTTPRequestHandler): + def do_POST(self) -> None: # noqa: N802 + length = int(self.headers.get("Content-Length", "0")) + payload = json.loads(self.rfile.read(length).decode("utf-8")) + if payload["method"] == "initialize": + response = { + "jsonrpc": "2.0", + "id": payload["id"], + "result": { + "protocolVersion": "2025-06-18", + "serverInfo": {"name": "VedAstro", "version": "1.0.0"}, + "capabilities": {}, + }, + } + body = json.dumps(response).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Mcp-Session-Id", "session-demo") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + + assert payload["method"] == "tools/list" + assert self.headers.get("Mcp-Session-Id") == "session-demo" + response = { + "jsonrpc": "2.0", + "id": payload["id"], + "result": { + "tools": [ + {"name": "get_current_transits"}, + {"name": "get_dasa_at_time"}, + ] + }, + } + body = json.dumps(response).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, format: str, *args) -> None: # noqa: A003 + return + + server = HTTPServer(("127.0.0.1", 0), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + endpoint = f"http://127.0.0.1:{server.server_port}/api/mcp/public" + completed = subprocess.run( + [ + sys.executable, + "scripts/vedastro_official_mcp_bridge.py", + "--endpoint", + endpoint, + "--operation", + "tools_list", + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=120, + check=False, + ) + finally: + server.shutdown() + thread.join(timeout=5) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["available"] is True + assert report["status"] == "ok" + assert report["operation"] == "tools_list" + assert report["session_id"] == "session-demo" + assert report["tool_count"] == 2 + assert report["tool_names"] == ["get_current_transits", "get_dasa_at_time"] diff --git a/tests/test_vedastro_parity_matrix.py b/tests/test_vedastro_parity_matrix.py index 751c713f..c47b6116 100644 --- a/tests/test_vedastro_parity_matrix.py +++ b/tests/test_vedastro_parity_matrix.py @@ -71,6 +71,14 @@ def test_matrix_rows_use_allowed_contract_values(): "external_evidence_only", "hybrid_local_plus_vedastro", } + allowed_fastest_lane = { + "local_native_preferred", + "official_mcp", + "official_python_bridge", + "rest_adapter", + "hybrid_router", + "external_evidence_only", + } allowed_priority = {"P0", "P1", "P2"} allowed_use = {"primary", "secondary", "oracle_only", "not_used"} @@ -82,16 +90,20 @@ def test_matrix_rows_use_allowed_contract_values(): "local_assets", "can_call_vedastro", "recommended_path", + "fastest_path_lane", "priority", "license_boundary", "adjudicator_use", "gap_notes", + "route_notes", } assert row["local_status"] in allowed_status assert row["recommended_path"] in allowed_path + assert row["fastest_path_lane"] in allowed_fastest_lane assert row["priority"] in allowed_priority assert row["adjudicator_use"] in allowed_use assert isinstance(row["local_assets"], list) + assert isinstance(row["route_notes"], str) def test_render_markdown_contains_summary_and_honesty_boundary(): @@ -102,6 +114,16 @@ def test_render_markdown_contains_summary_and_honesty_boundary(): assert "## Honesty Boundary" in markdown assert "EventsAtRange / Life Event Graph" in markdown assert "external adapter evidence" in markdown + assert "Fastest lane" in markdown + + +def test_matrix_declares_fastest_path_lane_for_high_value_rows(): + matrix = vedastro_parity_matrix.build_matrix() + + assert _row(matrix, "EventsAtRange / Life Event Graph")["fastest_path_lane"] == "rest_adapter" + assert _row(matrix, "Shadbala")["fastest_path_lane"] == "official_python_bridge" + assert _row(matrix, "MCP / API Surface")["fastest_path_lane"] == "official_mcp" + assert _row(matrix, "D1-D60 Divisional Charts")["fastest_path_lane"] == "local_native_preferred" def test_write_outputs_creates_json_and_markdown(tmp_path): diff --git a/tests/test_vedastro_python_bridge.py b/tests/test_vedastro_python_bridge.py new file mode 100644 index 00000000..6a2781ad --- /dev/null +++ b/tests/test_vedastro_python_bridge.py @@ -0,0 +1,332 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_vedastro_python_bridge_schema_is_declared() -> None: + completed = subprocess.run( + [sys.executable, "scripts/vedastro_python_bridge.py", "--print-schema"], + cwd=ROOT, + text=True, + capture_output=True, + timeout=120, + check=False, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["bridge"] == "vedastro_python_bridge" + assert report["intended_role"] == "python_sdk_bulk_calculation_bridge" + assert report["package_name"] == "VedAstro" + assert "call_method" in report["operations"] + assert "status" in report["response_contract"] + assert report["high_value_methods"]["event_tag_catalog"]["maps_to"] == "GetAllEventDataGroupedByTag" + assert report["high_value_methods"]["vimshottari_snapshot"]["maps_to"] == "DasaAtTime" + + +def test_vedastro_python_bridge_returns_controlled_missing_package_status() -> None: + env = os.environ.copy() + env["VEDASTRO_PYTHON_FORCE_UNAVAILABLE"] = "1" + completed = subprocess.run( + [ + sys.executable, + "scripts/vedastro_python_bridge.py", + "--method", + "DemoMethod", + "--params-json", + "{\"foo\":\"bar\"}", + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=120, + check=False, + env=env, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["available"] is False + assert report["status"] == "python_package_not_installed" + assert report["method"] == "DemoMethod" + + +def test_vedastro_python_bridge_can_return_stubbed_call_without_importing_package() -> None: + env = os.environ.copy() + env["VEDASTRO_PYTHON_BRIDGE_STUB_RESULT"] = json.dumps( + { + "method": "CalculateShadbala", + "result": {"Sun": {"Strength": 152}}, + } + ) + completed = subprocess.run( + [ + sys.executable, + "scripts/vedastro_python_bridge.py", + "--method", + "CalculateShadbala", + "--params-json", + "{\"chart_id\":\"demo\"}", + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=120, + check=False, + env=env, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["available"] is True + assert report["status"] == "ok" + assert report["method"] == "CalculateShadbala" + assert report["result"]["Sun"]["Strength"] == 152 + assert report["source"] == "stubbed_bridge_result" + + +def test_vedastro_python_bridge_can_use_project_venv_for_live_catalog_method() -> None: + vedastro_python = ROOT / "venv_vedastro" / "bin" / "python3.11" + if not vedastro_python.exists(): + return + + completed = subprocess.run( + [ + sys.executable, + "scripts/vedastro_python_bridge.py", + "--method", + "GetAllEventDataGroupedByTag", + "--params-json", + "{}", + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=180, + check=False, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["available"] is True + assert report["status"] == "ok" + assert report["method"] == "GetAllEventDataGroupedByTag" + assert report["module_name"] in {"vedastro", "VedAstro"} + assert "venv_vedastro/bin/python" in report["python_bin"] + assert "Marriage" in report["result"] or "LendingMoney" in report["result"] or "Empty" in report["result"] + + +def test_vedastro_python_bridge_supports_typed_enum_and_time_arguments() -> None: + vedastro_python = ROOT / "venv_vedastro" / "bin" / "python3.11" + if not vedastro_python.exists(): + return + + params = { + "kwargs": { + "time": { + "__vedastro_type__": "Time", + "year": REDACTED_YEAR, + "month": 4, + "day": 17, + "hour": 14, + "minute": 49, + "offset": 8, + "geolocation": { + "__vedastro_type__": "GeoLocation", + "location_name": "REDACTED_PLACE", + "longitude": 114.46, + "latitude": 36.6, + }, + } + } + } + completed = subprocess.run( + [ + sys.executable, + "scripts/vedastro_python_bridge.py", + "--method", + "PlanetNirayanaLongitude", + "--params-json", + json.dumps( + { + "kwargs": { + "planetName": {"__vedastro_enum__": "PlanetName", "value": "Sun"}, + **params["kwargs"], + } + } + ), + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=180, + check=False, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["available"] is True + assert report["status"] == "ok" + assert report["method"] == "PlanetNirayanaLongitude" + assert isinstance(report["result"], (int, float, str, dict, list)) + + +def test_vedastro_python_bridge_high_value_method_can_return_stubbed_result() -> None: + env = os.environ.copy() + env["VEDASTRO_PYTHON_BRIDGE_STUB_RESULT"] = json.dumps( + { + "method": "GetAllEventDataGroupedByTag", + "result": {"Marriage": [{"Name": "GoodForMarriage"}]}, + } + ) + completed = subprocess.run( + [ + sys.executable, + "scripts/vedastro_python_bridge.py", + "--high-value", + "event_tag_catalog", + "--params-json", + "{}", + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=120, + check=False, + env=env, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["available"] is True + assert report["status"] == "ok" + assert report["method"] == "GetAllEventDataGroupedByTag" + assert "Marriage" in report["result"] + + +def test_vedastro_python_bridge_high_value_vimshottari_snapshot_uses_positional_args() -> None: + vedastro_python = ROOT / "venv_vedastro" / "bin" / "python3.11" + if not vedastro_python.exists(): + return + + payload = { + "birth_time": { + "__vedastro_type__": "Time", + "year": REDACTED_YEAR, + "month": 4, + "day": 17, + "hour": 14, + "minute": 49, + "offset": 8, + "geolocation": { + "__vedastro_type__": "GeoLocation", + "location_name": "REDACTED_PLACE", + "longitude": 114.46, + "latitude": 36.6, + }, + }, + "check_time": { + "__vedastro_type__": "Time", + "year": 2026, + "month": 1, + "day": 1, + "hour": 12, + "minute": 0, + "offset": 8, + "geolocation": { + "__vedastro_type__": "GeoLocation", + "location_name": "REDACTED_PLACE", + "longitude": 114.46, + "latitude": 36.6, + }, + }, + "levels": 3, + } + completed = subprocess.run( + [ + sys.executable, + "scripts/vedastro_python_bridge.py", + "--high-value", + "vimshottari_snapshot", + "--params-json", + json.dumps(payload, ensure_ascii=False), + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=180, + check=False, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["available"] is True + assert report["status"] == "ok" + assert report["method"] == "DasaAtTime" + + +def test_vedastro_python_bridge_high_value_chara_snapshot_uses_positional_args() -> None: + vedastro_python = ROOT / "venv_vedastro" / "bin" / "python3.11" + if not vedastro_python.exists(): + return + + payload = { + "birth_time": { + "__vedastro_type__": "Time", + "year": REDACTED_YEAR, + "month": 4, + "day": 17, + "hour": 14, + "minute": 49, + "offset": 8, + "geolocation": { + "__vedastro_type__": "GeoLocation", + "location_name": "REDACTED_PLACE", + "longitude": 114.46, + "latitude": 36.6, + }, + }, + "check_time": { + "__vedastro_type__": "Time", + "year": 2026, + "month": 1, + "day": 1, + "hour": 12, + "minute": 0, + "offset": 8, + "geolocation": { + "__vedastro_type__": "GeoLocation", + "location_name": "REDACTED_PLACE", + "longitude": 114.46, + "latitude": 36.6, + }, + }, + } + completed = subprocess.run( + [ + sys.executable, + "scripts/vedastro_python_bridge.py", + "--high-value", + "chara_dasha_snapshot", + "--params-json", + json.dumps(payload, ensure_ascii=False), + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=180, + check=False, + ) + + assert completed.returncode == 0, completed.stderr or completed.stdout + report = json.loads(completed.stdout) + assert report["available"] is True + assert report["status"] == "ok" + assert report["method"] == "GetCharaDasaAtTime" diff --git a/tests/test_vedastro_range_scan_replay.py b/tests/test_vedastro_range_scan_replay.py new file mode 100644 index 00000000..255a0ca4 --- /dev/null +++ b/tests/test_vedastro_range_scan_replay.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from scripts import vedastro_service_adapter + + +ROOT = Path(__file__).resolve().parents[1] + + +def _request_preview(domain: str = "marriage") -> dict: + return vedastro_service_adapter._range_scan_preview( # noqa: SLF001 + vedastro_service_adapter.PARITY_CASES["beijing_first_use_demo"], + domain, + "2026-01-01", + "2026-12-31", + ) + + +def test_official_sample_replay_reports_exact_tag_alias_and_rejected_matches() -> None: + sample = json.loads((ROOT / "tests" / "test-data" / "vedastro_range_scan_official_sample.json").read_text(encoding="utf-8")) + + report = vedastro_service_adapter._normalize_range_scan_success( # noqa: SLF001 + sample, + "https://api.vedastro.org/api", + _request_preview("marriage"), + ) + + replay = report["source_metadata"]["mapping_replay"] + assert replay["raw_event_count"] == 4 + assert replay["filtered_event_count"] == 3 + assert replay["match_counts"] == { + "exact_id": 1, + "official_tag": 1, + "alias": 1, + "rejected": 1, + } + assert replay["zero_event_domains"] == [] + assert "Marriage" in replay["matched_tags"] + + by_id = {item["event_id"]: item for item in report["evidence_ledger"]} + assert by_id["JupiterSupportsMarriageAxis"]["matched_by"] == "exact_id" + assert by_id["GoodForMarriage"]["matched_by"] == "official_tag" + assert by_id["PartnershipBlessingWindow"]["matched_by"] == "alias" + + +def test_replay_stats_track_zero_event_domain_and_rejected_alias_noise() -> None: + payload = { + "Status": "Pass", + "Payload": [ + { + "Name": "RandomMoonMoodShift", + "Nature": "Neutral", + "Description": "Generic transit noise without finance significance.", + "StartTime": "2026-06-01", + "EndTime": "2026-06-02", + "EventTags": ["General", "Gochara"], + } + ], + } + + report = vedastro_service_adapter._normalize_range_scan_success( # noqa: SLF001 + payload, + "https://api.vedastro.org/api", + _request_preview("wealth"), + ) + + assert report["event_count"] == 0 + replay = report["source_metadata"]["mapping_replay"] + assert replay["raw_event_count"] == 1 + assert replay["filtered_event_count"] == 0 + assert replay["zero_event_domains"] == ["wealth"] + assert replay["match_counts"]["rejected"] == 1 + assert replay["recommended_allowlist_candidates"] == []