Merge pull request #2 from jesse-ux/codex/optimize-runtime-ux

feat: enforce commercial Jyotish workflow contracts
This commit is contained in:
732642856
2026-07-19 10:11:54 +08:00
committed by GitHub
15 changed files with 475 additions and 16 deletions
@@ -22,7 +22,7 @@ OUT = ROOT / 'outputs'
CANON = OUT / 'canonical'
REPORT = OUT / 'jyotish_benchmark_round6_ashtakavarga_compare.md'
MATRIX = OUT / 'ashtakavarga_comparison_matrix.csv'
SKILL_SCRIPTS = Path(__file__).resolve().parents[2] / 'scripts'
SKILL_SCRIPTS = Path(__file__).resolve().parents[3] / 'scripts'
PYJHORA_SITE = Path(__import__('os').environ.get('PYJHORA_SITE', ''))
PYJHORA_COMPAT = ROOT / 'scripts/pyjhora_compat'
@@ -13,7 +13,7 @@ from shadbala import calc_shadbala, NAISARGIKA_BALA
from jhora import const, utils
from jhora.panchanga import drik
from jhora.horoscope.chart import charts
from jhora.horoscope.strength import shadbala as pj_shadbala
from jhora.horoscope.chart import strength as pj_strength
TEST_CASES = [
{"id": "beijing_1990_noon", "year":1990, "month":6, "day":15, "hour":12, "minute":0, "lat":39.9, "lon":116.4, "tz":8},
@@ -56,8 +56,8 @@ def run_benchmark():
sign = pp[pid + 1][1][0]
deg = pp[pid + 1][1][1]
house = (sign - asc_sign_idx) % 12 + 1
retro = pp[pid + 1][4] # retrograde flag in PyJHora
speed = pp[pid + 1][3] # speed in degrees/day
retro = pp[pid + 1][4] if len(pp[pid + 1]) > 4 else False
speed = pp[pid + 1][3] if len(pp[pid + 1]) > 3 else 1.0
planets[pname] = {
'sign': SIGNS[sign],
'degree': sign * 30 + deg,
@@ -73,7 +73,7 @@ def run_benchmark():
# PyJHora Shadbala
try:
pj = pj_shadbala.get_shadbala_scores(jd, place, divisional_chart_factor=1)
pj_total_rupas = pj_strength.shad_bala(jd, place)[7]
except:
print(f" PyJHora shadbala failed for {case['id']}")
continue
@@ -87,9 +87,7 @@ def run_benchmark():
continue
our_rupas = our['planets'][pname]['total_rupas']
pj_rupas = pj.get(pname, {}).get('shadbala', {}).get('total', 0)
if isinstance(pj_rupas, dict):
pj_rupas = pj_rupas.get('rupas', 0)
pj_rupas = pj_total_rupas[PLANET_NAMES.index(pname)]
total_planets += 1
if pj_rupas > 0:
+5
View File
@@ -137,3 +137,8 @@ for d in <home>/Documents <home>/WorkBuddy <home>/.workbuddy <home>/Downloads <h
[ -d "$d" ] && find "$d" -maxdepth 7 -type f \( -iname '*jyotish*' -o -iname '*vedic*' -o -iname '*jhora*' -o -iname '*shadbala*' -o -iname '*ashtakoot*' -o -iname '*印度占星*' -o -iname '*yinduzhanxing*' \) 2>/dev/null
done
```
## ERR-083 | Commercial Next production build terminates before artifact generation | active 2026-07-19
`frontend` local `npm run build` ends during Next.js 16.2.10 compile/static generation, both without configuration and with the CI Supabase placeholders. It leaves no `.next/BUILD_ID` or `.next/prerender-manifest.json`, and no application stack trace. Frontend contracts (`270 passed`), lint, and the selected Python commercial workflow regressions (`128 passed`) remain green.
Prevention: do not equate this local host failure with an astrology capability regression. Treat GitHub Actions Node 22 build evidence as the deployment gate before merge. Keep VedAstro `premium_key_missing` and official raw snapshot status explicitly degraded.
@@ -0,0 +1,136 @@
# Commercial Jyotish Invocation Optimization Plan
## Scope
This plan optimizes the commercial repository's actual Jyotish skill execution and the results returned to paying users. It does not modify, reorganize, or treat the research repository as a runtime dependency. Generic frontend, account, billing, and deployment work is out of scope unless it directly prevents an astrology capability from being invoked, verified, or truthfully surfaced.
## Verified Runtime Chain
```text
POST /api/consult
-> getJyotishAgent(model)
-> consultationTool / runConsultationWorkflow
-> POST ${JYOTISH_API_BASE}/api/consultation_workflow
-> JyotishHandler._compute_consultation_workflow
-> UnifiedConsultationOrchestrator route selection + strict workflow
-> chart/evidence/technique/dasha/oracle layers
-> consumer_context + auditable response
-> language model renders the bounded user answer
```
Other live product paths:
- Daily guidance: `frontend/src/app/api/daily-starlanguage/route.ts` -> Python `/api/chart` and daily guidance logic.
- Synastry: `frontend/src/app/api/synastry/route.ts` -> Python `/api/chart` for both charts and `/api/synastry`.
- Birth-time flows call the bounded guide/rectification routes and must not obtain unrestricted consultation authority.
Verified baseline:
- `python3 scripts/user_invocation_acceptance_check.py`: pass. Strict routes available: career, relationship, finance.
- Core API and external adapters are available; the official VedAstro snapshot is not ready in this environment (`fast_local_fallback`, premium key absent). This is an explicit degraded external layer, not a failed local calculation.
- `skills/jyotish-vedic-astrology/SKILL.md` exists and is the default Mastra skill path.
- The Python server has many registered endpoints, but only the paths above are currently commercial-user reachable. Registered-but-unreachable techniques are not treated as product capability.
## Non-Negotiable Product Truth Rules
1. A user-facing claim must originate from a computed invocation result, never from an LLM choosing to improvise chart facts.
2. For career, relationship, wealth, timing, health, or event claims, the returned evidence must contain the domain-required strict workflow layers and an explicit status for every missing layer.
3. The commercial response may simplify language, but it may not drop a `blocked`, `degraded`, conflict, ayanamsa/node-mode, Dasha-boundary, functional-benefic/malefic, or external-evidence limitation that materially changes the claim.
4. Local engine success, external-engine availability, executed raw coverage, parity, and predictive calibration remain separate states. No response upgrades one state into another.
5. No research checkout/path, raw private case, oracle credential, or unapproved source artifact becomes a commercial runtime dependency.
## Phase 0: Make Invocation Deterministic
### Problem
The chat agent receives instructions to call `consultationTool`, but model tool choice is probabilistic. An instruction-level requirement is not a server-side guarantee that a new chart claim has traversed the Jyotish engine.
### Work
1. Extract a deterministic `requiresJyotishWorkflow(question, profile, conversationState)` classifier in the commercial server layer.
2. For a new chart claim, call `runConsultationWorkflow` before streaming model output, then inject only the validated `consumer_context`/evidence projection into the agent. Do not depend on the model to decide whether to call the tool.
3. Preserve tool use for follow-ups only when the stored invocation result covers the request; otherwise re-run the workflow with an explicit reason.
4. Add an invocation receipt to the stream metadata: immutable request ID, workflow version, route, entry mode, local/external status, and evidence-packet ID. Never include birth data, question text, prompts, or secrets.
5. Define explicit `not_astrology`, `needs_profile`, `computed`, `degraded`, and `blocked` branches. Non-astrology conversation must not spend a calculation; incomplete birth data must not produce synthetic chart claims.
### Tests
- Contract tests prove career, relationship, wealth, and timing questions invoke `/api/consultation_workflow` exactly once before text is emitted.
- Follow-up reuse is allowed only for matching chart/configuration/version; differing birth data, reference date, ayanamsa, node mode, or required domain invalidates reuse.
- Model-mock tests prove a model cannot bypass a required invocation.
- Stream tests prove receipts contain no personal or secret fields.
## Phase 1: Enforce Route-Specific Skill Completeness
1. Create one versioned TypeScript schema for the commercial projection of the Python workflow response. Parse it in `frontend/src/mastra/index.ts`; reject malformed or incomplete results instead of passing untyped records to the model.
2. Build an executable route matrix from `SKILL.md`, strict-workflow contracts, and the technique registry. At minimum enforce:
- career: D10 + A10 and functional benefic/malefic;
- relationship: D9 + UL and functional benefic/malefic;
- wealth: D2/D11 and functional benefic/malefic;
- timing/event: Vimshottari + Narayana Dasha, required Dasha boundaries, transit status, and external-evidence status.
3. The API projection carries technique `used/not_used/blocked`, missing layers, conflict resolution, confidence boundary, and the raw-evidence identifiers required by the selected route.
4. Add a Python-to-TypeScript golden fixture for every route and every terminal status (`ready`, `degraded`, `blocked`). Fixtures must use public synthetic cases only.
5. Cross-check the frontend-reachable endpoint inventory against Python dispatch. Fail CI when a new user-reachable operation has no handler, no schema, or no capability contract.
### Tests
- Golden parity for route projection and error/status preservation.
- Negative tests delete each required layer and assert the commercial answer cannot make the corresponding claim.
- Endpoint reachability inventory test includes `/api/consultation_workflow`, daily guidance, synastry, and all birth-time routes.
## Phase 2: Truthful External-Oracle Degradation
1. Treat the current `fast_local_fallback` as a first-class execution state in the commercial response contract, not an incidental log value.
2. Map external layers independently: PyJHora/JHora comparison, jyotishganit, VedAstro official raw snapshot, parity status, and real-case calibration. Preserve license boundaries.
3. When a premium key or official snapshot is unavailable, retain local computation but force the exact affected conclusion to `degraded` or `blocked` according to the strict-workflow contract. Do not silently say an external check ran.
4. Add a configurable official-snapshot budget/timeout with a circuit state and sanitized telemetry. The fallback must be deterministic, bounded, and visible to audit metadata.
5. Add a deployment gate that runs the existing adapter diagnostics and a selected public-synthetic same-chart replay. It must report `unavailable`, `partial_verified`, and `mismatch` distinctly rather than failing open.
### Tests
- No-key, timeout, malformed-provider, parity-mismatch, and fully-available fixtures.
- Assertions that the language-model context never upgrades `partial_verified` to verified or hides a material blocked layer.
- License/attribution snapshot test for every enabled external adapter.
## Phase 3: Optimize Real Engine Cost And Reliability
1. Instrument only actual entrypoints (`consultation_workflow`, chart, synastry, daily guidance, high-rigor workflow) with sanitized elapsed-time spans: routing, ephemeris/chart, divisional charts, dasha, Shadbala/Ashtakavarga, external adapters, serialization.
2. Benchmark public synthetic charts by route/configuration on the production-equivalent 1-vCPU/2-GB budget. Establish p50/p95, response-size, timeout, and queue-depth budgets before caching or parallelization.
3. Memoize only immutable, configuration-keyed computation fragments. Use a bounded TTL/LRU keyed by a cryptographic digest of normalized input and computation settings; never cache raw profile data, user text, credentials, or final personalized prose.
4. Keep high-rigor/batch/external-heavy paths asynchronous when their measured p95 exceeds the interactive budget. Preserve existing job identity, polling, cancellation, and evidence-packet retrieval semantics.
5. Reduce model context to the route-required evidence projection. Full raw packets remain retrievable only through the authenticated/auditable path, avoiding token cost and accidental evidence loss in chat rendering.
### Tests
- Determinism before/after cache hit; ayanamsa/node-mode/reference-date changes must miss cache.
- Concurrent identical requests do not duplicate expensive work; cancellation never corrupts a shared result.
- Benchmark regression thresholds and response-shape snapshots.
## Phase 4: Wire Secondary Live Flows Into The Same Truth Contract
1. Daily guidance must carry reference date, transit source/configuration, local/external state, and a bounded claim scope; no generic model prose may replace chart-derived daily data.
2. Synastry must preserve both chart settings, Ashtakoot method/version, D9 evidence, relationship-route required layers, and non-comparability states.
3. Birth-time guidance/rectification remains evidence-collection and candidate-scoring only. It cannot present a candidate as a verified birth time until its configured evidence threshold and strict workflow state are met.
4. Use one commercial `AstrologyExecutionEnvelope` for the shared status/audit fields while retaining route-specific payloads. This is an adapter boundary, not a rewrite of engine formulas.
### Tests
- Daily date-boundary and fallback fixtures.
- Synastry settings mismatch/non-comparability fixtures.
- Rectification candidate confidence and no-premature-certainty fixtures.
## Phase 5: Commercial Capability Intake
When the owner approves a research-derived capability, add it only through a commercial intake manifest containing capability ID/version, approved interface, supplied source hash, license/attribution decision, input/output contract, privacy class, test fixture provenance, rollout flag, observability, and rollback path. The commercial adapter consumes that interface only; it does not import a research working tree.
## Execution Order
1. Reconcile local branch with upstream before editing runtime behavior.
2. Phase 0 deterministic invocation.
3. Phase 1 route-completeness schemas and golden contracts.
4. Phase 2 external-degradation truth handling.
5. Phase 3 measured engine performance/reliability.
6. Phase 4 secondary live flows.
7. Phase 5 only after an owner-approved capability handoff.
Each phase requires relevant Python + frontend tests, user-invocation acceptance, adapter diagnostics, production build, and `git diff --check`. Push/deploy needs separate owner authorization.
@@ -0,0 +1,82 @@
# Commercial Repository Optimization Plan
**Scope:** `jesse-ux/Jyotisha` commercial repository only. The research repository remains an external, owner-approved source of validated capability artifacts. No research source code, raw data, or internal validation chain is modified by this plan.
## Baseline And Evidence
- Local branch: `codex/optimize-runtime-ux`, based on `3d6d498`; upstream `main` is now `51decd5003df1a33f49e71d6469e5a0cd382e7dc`. Reconcile before implementation; do not overwrite local changes.
- Existing local change: lazy-load birth-time rectification. Largest first-page chunk measured `826,370 B -> 710,726 B` (about `115.6 KB` reduction).
- `npm test`: `264/264` pass. `npm run lint`: exit success, two existing `react-hooks/exhaustive-deps` warnings in `frontend/src/app/page.tsx` lines 1176 and 1192.
- Production smoke: `https://jyotisha.chat/` returns `200`, static cache hit, and `/api/health` returns environment/provider status plus internal API latency publicly.
- Official-registry dependency audit: 5 findings (3 low, 2 moderate), including direct `next`/transitive `postcss` and `@mastra/core`/AI SDK dependency paths. The configured `npmmirror` cannot provide npm security advisories; its audit endpoint returns `404`.
- Public evidence consulted: [Next.js lazy loading](https://nextjs.org/docs/app/guides/lazy-loading), [Next.js production checklist](https://nextjs.org/docs/app/guides/production-checklist), [Supabase SSR](https://supabase.com/docs/guides/auth/server-side/creating-a-client), and [GitHub Actions secure use](https://docs.github.com/en/actions/reference/security/secure-use).
## Acceptance Rules
- No research repository write, dependency, path, raw evidence, or secret enters the commercial repository without an explicit approved intake record.
- Preserve account, credits, cancellation, and existing business tests. Do not expose user identity, birth data, prompts, model keys, or Supabase service keys in logs, telemetry, responses, or build artifacts.
- A public liveness route may reveal only a coarse status. Dependency readiness and diagnostic detail must remain Docker-private or token-gated.
- Every performance claim needs before/after build evidence. Every dependency update needs audit output and full test/build evidence.
## Phase 0: Reconcile And Freeze Baseline
1. Run `git fetch origin main`, inspect `3d6d498..origin/main`, and rebase or manually port only the local lazy-load change after reviewing conflicts.
2. Record immutable baseline artifacts: `npm ci`, `npm test`, `npm run lint`, `npm run build`, official-registry `npm audit --omit=dev --registry=https://registry.npmjs.org`, route-size manifest, and production header/health samples.
3. Add a CI-independent script, `frontend/scripts/verify-production-baseline.mjs`, that emits sanitized JSON for test/build/audit/bundle-budget evidence. It must explicitly select `registry.npmjs.org` for audit.
**Tests:** existing test suite; build; `git diff --check`; baseline script fixture test.
## Phase 1: Supply Chain And Release Controls
1. Upgrade direct vulnerable dependencies to the newest compatible patched releases, regenerate only `frontend/package-lock.json`, and re-run the official audit. Do not accept the audit tool's suggested semver-major downgrade/upgrade blindly; inspect the resolved tree first.
2. Add `dependabot.yml` for GitHub Actions, root Python, `frontend`, and `jyotish-app`; group patch/minor updates by ecosystem while retaining PR review.
3. In CI, run official-registry production audit, frontend lint/tests/build, and a lockfile integrity check. Use GitHub Actions concurrency to cancel obsolete PR runs. Pin third-party actions to immutable commit SHAs after verifying publishers and versions.
4. Keep deployment gated on the same commit that passed CI. Add a release evidence artifact containing dependency audit summary, build identifier, and sanitized production smoke result.
**Tests:** audit reaches zero high/critical; dependency-specific regression tests; full frontend test/build; workflow syntax validation.
## Phase 2: Public Edge And API Hardening
1. Split health semantics:
- `GET /api/health`: public liveness only, no environment names, provider presence, topology, or internal latency.
- an internal-only diagnostic route or direct Docker healthcheck: detailed dependency checks, accessible only from the compose network or through a deployment-only token.
- Add route tests proving detail cannot reach the public response.
2. Add Caddy edge headers: `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, clickjacking protection, and production HSTS. Design CSP in report-only mode first because Next.js/Supabase scripts and streaming require nonce/hash validation; promote only after browser and login-flow verification.
3. Map all mutating API routes to explicit authentication, schema validation, authorization, idempotency, timeout, and rate-limit behavior. Preserve the existing credit RPC as the billing authority; add a durable per-user guard only where the current credit lifecycle does not already prevent expensive model work.
4. Add request IDs and sanitized structured event fields (`route`, `status`, `latency_ms`, `model_id`, `credit_transition`, `error_class`). Explicitly prohibit prompt/profile/body logging.
**Tests:** public health redaction, internal health success in compose, unauthenticated/malformed/over-limit cases for every mutation route, streamed consultation cancellation, browser login/account/consult smoke.
## Phase 3: First-Use Performance And Frontend Maintainability
1. Keep the existing lazy-loaded rectification subtree and turn its measured saving into a regression budget. Add a bundle report that identifies first-page JS separately from deferred chunks; fail CI only on a deliberate, reviewed budget breach.
2. Defer heavy onboarding-only data, beginning with `china-locations`, until the location step opens. Preserve typed loading/error states and keyboard behavior.
3. Split `frontend/src/app/page.tsx` (2,643 lines) by stable product boundaries, not generic abstractions:
- session/chat composer and streaming lifecycle;
- onboarding/profile and birth-time flow;
- chart library and synastry history;
- account/model-selection orchestration.
State ownership remains at the smallest shared parent; pure transformations move to tested `lib` modules.
4. Resolve the two exhaustive-deps warnings by proving the intended dependency model through tests, rather than silencing lint rules. Avoid adding `profile` wholesale if that would cause duplicate network calls; extract stable primitive dependencies or a memoized request key.
5. Validate desktop/mobile rendering, loading fallback, focus order, reduced-motion behavior, and long Chinese content after each split.
**Tests:** unit tests for extracted state transitions; existing source-contract tests updated only for behavior; Playwright flows for login, onboarding, guided rectification, cancel/retry, chat session switching, account dialogs; production bundle comparison.
## Phase 4: Commercial Capability Intake Boundary
1. Add `docs/commercial-capability-intake.md` plus a machine-readable `frontend/src/lib/capability-manifest.ts` only when a research capability is approved for commercial use.
2. Each intake row records: capability ID/version, permitted interface, source commit/hash supplied by the owner, license/attribution decision, accepted input/output schema, user-facing fallback, privacy classification, test fixture provenance, and rollback switch.
3. Commercial adapters call only the approved stable interface. They must not import a research checkout, scrape research artifacts, or claim research-level validation beyond the supplied manifest.
4. Gate every intake behind contract tests, a staged feature flag, production observability, and an explicit rollback procedure.
**Tests:** manifest schema validation; adapter contract fixtures; feature-flag off fallback; rollback integration test.
## Execution Order
1. Phase 0 reconcile/baseline.
2. Phase 1 dependencies and CI controls.
3. Phase 2 health/edge/API hardening.
4. Phase 3 performance decomposition and browser verification.
5. Phase 4 only when an owner-approved research capability arrives.
Each phase ends with `git diff --check`, full relevant tests, production build, and an evidence note. Deployment/push remains separate owner authorization.
+11 -1
View File
@@ -1,7 +1,9 @@
import { NextResponse } from "next/server";
import {
consultationInputSchema,
consultationWorkflowReceipt,
getJyotishAgent,
runConsultationWorkflow,
} from "@/mastra";
import {
languageModelConfigurationMessage,
@@ -167,8 +169,10 @@ export async function POST(request: Request) {
try {
const { history, name } = parsed.data;
const toolInput = consultationInputSchema.parse(parsed.data);
const workflowContext = await runConsultationWorkflow(toolInput);
const workflowReceipt = consultationWorkflowReceipt(workflowContext);
const result = await getJyotishAgent(selectedModel).stream([
const result = await getJyotishAgent(selectedModel, workflowContext).stream([
...history.map((message) => message.role === "user"
? { role: "user" as const, content: message.text }
: { role: "assistant" as const, content: message.text }),
@@ -191,6 +195,12 @@ export async function POST(request: Request) {
return streamTextResponse(result.textStream, {
mode: "mastra",
requestId,
headers: {
"x-jyotish-workflow-route": workflowReceipt.route,
"x-jyotish-workflow-status": workflowReceipt.status,
"x-jyotish-precise-timing": workflowReceipt.preciseTiming,
"x-jyotish-missing-layers": workflowReceipt.missingLayers,
},
onComplete: () => settle(completeAndRecordUsage),
onError: (_error, emitted) => settleInterrupted(emitted),
onCancel: settleInterrupted,
+11 -3
View File
@@ -1,12 +1,12 @@
"use client";
import Link from "next/link";
import dynamic from "next/dynamic";
import { ArrowUp, ArrowUpRight, Sparkles, Square, X } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import type { FormEvent, KeyboardEvent } from "react";
import { AppSidebar } from "@/components/app-sidebar";
import { BirthTimeIntakeFields } from "@/components/birth-time-intake";
import { BirthTimeRectification } from "@/components/birth-time-rectification";
import { ChatMessageContent } from "@/components/chat-message-content";
import { ModelSelector } from "@/components/model-selector";
import { Button } from "@/components/ui/button";
@@ -46,6 +46,14 @@ import {
} from "@/lib/public-models";
import { createBrowserSupabaseClient } from "@/lib/supabase/client";
const BirthTimeRectification = dynamic(
() => import("@/components/birth-time-rectification").then((module) => module.BirthTimeRectification),
{
ssr: false,
loading: () => <p className="birth-time-assistant-intent" role="status">...</p>,
},
);
type Theme = ReplyTheme;
type Message = { role: "user" | "assistant"; text: string; suggestions?: string[] };
type Profile = BirthTimeDraft & {
@@ -1165,7 +1173,7 @@ export default function Home() {
return () => {
cancelled = true;
};
}, [hydrated, profile.date, profile.time, profile.provinceCode, profile.cityCode, profileComplete]);
}, [hydrated, profile, profileComplete]);
useEffect(() => {
if (!hydrated || !profileComplete) return;
@@ -1181,7 +1189,7 @@ export default function Home() {
return () => {
cancelled = true;
};
}, [hydrated, profile.date, profile.time, profile.provinceCode, profile.cityCode, profileComplete]);
}, [hydrated, profile, profileComplete]);
useEffect(() => {
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+2
View File
@@ -7,6 +7,7 @@ type StreamHooks = {
type StreamTextResponseOptions = StreamHooks & {
readonly mode: "engine" | "mastra";
readonly requestId: string;
readonly headers?: Record<string, string>;
};
export function streamTextResponse(
@@ -62,6 +63,7 @@ export function streamTextResponse(
"x-accel-buffering": "no",
"x-ayanam-mode": options.mode,
"x-ayanam-request-id": options.requestId,
...options.headers,
},
});
}
+78 -4
View File
@@ -24,6 +24,25 @@ export type ConsultationInput = z.infer<typeof consultationInputSchema>;
type JsonRecord = Record<string, unknown>;
const workflowConsumerContextSchema = z.object({
route: z.string().min(1),
core_status: z.enum(["ready", "degraded", "blocked"]),
available_layers: z.array(z.string()),
missing_route_layers: z.array(z.string()),
hard_blockers: z.array(z.string()),
answer_policy: z.object({
can_answer_direction: z.boolean(),
can_answer_precise_timing: z.boolean(),
}).passthrough(),
}).passthrough();
export const consultationWorkflowResponseSchema = z.object({
success: z.boolean(),
chart: z.record(z.unknown()),
routing: z.record(z.unknown()),
consumer_context: workflowConsumerContextSchema,
}).passthrough();
function record(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value)
? value as JsonRecord
@@ -52,7 +71,21 @@ export async function runConsultationWorkflow(input: ConsultationInput) {
if (!response.ok || !data) {
throw new Error(data?.error || data?.message || `Jyotish API returned ${response.status}`);
}
return data as JsonRecord;
const parsed = consultationWorkflowResponseSchema.safeParse(data);
if (!parsed.success) {
throw new Error("Jyotish API returned an incomplete consultation contract");
}
return parsed.data;
}
export function consultationWorkflowReceipt(data: JsonRecord) {
const consumerContext = workflowConsumerContextSchema.parse(data.consumer_context);
return {
route: consumerContext.route,
status: consumerContext.core_status,
preciseTiming: consumerContext.answer_policy.can_answer_precise_timing ? "allowed" : "blocked",
missingLayers: consumerContext.missing_route_layers.join(",") || "none",
};
}
export function toAgentConsultationContext(data: JsonRecord) {
@@ -64,12 +97,22 @@ export function toAgentConsultationContext(data: JsonRecord) {
const primaryTheme = String(routing.primary_theme || routing.question_type || "general");
const selectedTheme = record(themes[primaryTheme]);
const rectification = record(data.rectification);
const consumerContext = record(data.consumer_context);
return {
success: data.success === true,
question: data.question,
routing,
consumer_context: record(data.consumer_context),
consumer_context: consumerContext,
evidence_contract: {
route: consumerContext.route,
core_status: consumerContext.core_status,
available_layers: consumerContext.available_layers,
missing_route_layers: consumerContext.missing_route_layers,
hard_blockers: consumerContext.hard_blockers,
answer_policy: consumerContext.answer_policy,
user_facing_limitation: consumerContext.user_facing_limitation,
},
chart: {
birth: chart.birth,
ascendant: chart.ascendant,
@@ -81,18 +124,27 @@ export function toAgentConsultationContext(data: JsonRecord) {
yogas: chart.yogas,
},
local_layers: {
shadbala_boundary: "Shadbala is a locally consistent relative-strength layer; external component-level absolute parity remains partial and must not be stated as closed.",
varga_full: modules.varga_full,
arudha_padas: modules.arudha_padas,
ashtakavarga: modules.ashtakavarga,
dasha_boundaries: modules.dasha_boundaries,
narayana_dasha: modules.narayana_dasha,
functional_benefic_malefic: record(data.machine_evidence_packet).functional_benefic_malefic,
},
rectification: {
boundary: "not_auto_rectified",
summary: rectification.summary,
enabled_vargas: rectification.enabled_vargas,
lagna_boundary: rectification.lagna_boundary,
},
thematic_evidence: selectedTheme,
vedastro_gateway: record(data.vedastro_gateway),
external_engine_evidence: {
runtime_truth: record(data.runtime_truth),
numerical_parity: record(data.external_parity_gate),
real_case_calibration: record(data.real_case_calibration),
},
reference_transparency: record(data.reference_transparency),
};
}
@@ -125,6 +177,8 @@ When reference_transparency is present:
- If should_lead_with_limitations is false, do not lead with limitations. If a limitation is relevant, put it in one short sentence at the end.
- Only say the chart calculation failed when hard_blockers is non-empty.
- Never claim D2, D11, D9, D10, A10, UL, or Narayana Dasha is missing when it appears in available_layers, chart, or local_layers.
- Treat evidence_contract.answer_policy as a hard output contract. When can_answer_precise_timing is false, provide only direction or structure and do not state a month, date, or guaranteed timing outcome.
- Treat rectification.boundary=not_auto_rectified as final: a candidate time or score is not a verified birth time and must not be presented as one.
Usually answer in 2-5 short paragraphs. Ask one clarifying question only when the user's intent is genuinely unclear.
After every substantive answer, append exactly two hidden blocks in this order and nothing after the second block:
<!--AYANAM_SUGGESTIONS:["问题一","问题二","问题三"]-->
@@ -137,7 +191,27 @@ Do not provide medical, legal, investment, or safety-critical instructions. Do n
const jyotishAgents = new Map<string, Agent>();
export function getJyotishAgent(model: ResolvedLanguageModel) {
function groundedJyotishInstructions(workflowContext: JsonRecord) {
return `${jyotishInstructions}
The server-computed Jyotish workflow below is the only source for this chart claim. Use it directly, preserve its truth boundaries, and do not run a second consultation workflow.
<server-computed-jyotish-workflow>
${JSON.stringify(toAgentConsultationContext(workflowContext))}
</server-computed-jyotish-workflow>`;
}
export function getJyotishAgent(model: ResolvedLanguageModel, workflowContext?: JsonRecord) {
if (workflowContext) {
return new Agent({
id: `jyotish-guide-${model.id}-grounded`,
name: "Jyotish Guide",
model: model.model,
instructions: groundedJyotishInstructions(workflowContext),
skills: [jyotishSkillPath],
tools: workflowContext ? {} : { consultationTool },
});
}
const cached = jyotishAgents.get(model.id);
if (cached) return cached;
const agent = new Agent({
@@ -146,7 +220,7 @@ export function getJyotishAgent(model: ResolvedLanguageModel) {
model: model.model,
instructions: jyotishInstructions,
skills: [jyotishSkillPath],
tools: { consultationTool },
tools: workflowContext ? {} : { consultationTool },
});
jyotishAgents.set(model.id, agent);
return agent;
@@ -23,3 +23,23 @@ test("passes transparent public-case references into the agent context", () => {
assert.match(source, /Shadbala\/Ashtakavarga component differences/);
assert.match(source, /D2, D11/);
});
test("keeps strength, Ashtakavarga, and timing evidence available to the answer model", () => {
const source = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
assert.match(source, /shadbala: chart\.shadbala/);
assert.match(source, /shadbala_boundary:/);
assert.match(source, /ashtakavarga: modules\.ashtakavarga/);
assert.match(source, /dasha_boundaries: modules\.dasha_boundaries/);
assert.match(source, /narayana_dasha: modules\.narayana_dasha/);
assert.match(source, /evidence_contract:/);
assert.match(source, /missing_route_layers: consumerContext\.missing_route_layers/);
assert.match(source, /answer_policy: consumerContext\.answer_policy/);
assert.match(source, /evidence_contract\.answer_policy/);
assert.match(source, /can_answer_precise_timing/);
assert.match(source, /boundary: "not_auto_rectified"/);
assert.match(source, /rectification\.boundary=not_auto_rectified/);
assert.match(source, /external_engine_evidence:/);
assert.match(source, /runtime_truth: record\(data\.runtime_truth\)/);
assert.match(source, /numerical_parity: record\(data\.external_parity_gate\)/);
assert.match(source, /real_case_calibration: record\(data\.real_case_calibration\)/);
});
@@ -0,0 +1,28 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
test("runs the Jyotish workflow before streaming a commercial consultation", () => {
assert.match(route, /runConsultationWorkflow/);
assert.match(route, /await runConsultationWorkflow\(toolInput\)/);
assert.match(route, /getJyotishAgent\(selectedModel, workflowContext\)\.stream/);
assert.ok(route.indexOf("await runConsultationWorkflow(toolInput)") < route.indexOf(".stream(["));
});
test("grounds the answer in the server-computed workflow without a second tool run", () => {
assert.match(mastra, /function getJyotishAgent\(model: ResolvedLanguageModel, workflowContext\?/);
assert.match(mastra, /workflowContext \? \{\} : \{ consultationTool \}/);
assert.match(mastra, /server-computed Jyotish workflow/);
});
test("validates and emits a non-sensitive workflow receipt", () => {
assert.match(mastra, /consultationWorkflowResponseSchema/);
assert.match(mastra, /safeParse\(data\)/);
assert.match(mastra, /consultationWorkflowReceipt/);
assert.match(route, /workflowReceipt/);
assert.match(route, /x-jyotish-workflow-route/);
assert.match(route, /x-jyotish-workflow-status/);
});
@@ -0,0 +1,29 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const root = new URL("../../", import.meta.url);
const readRoot = (path: string) => readFileSync(new URL(path, root), "utf8");
const mastra = readRoot("frontend/src/mastra/index.ts");
const rectification = readRoot("frontend/src/lib/birth-time-journey-engine.ts");
const synastry = readRoot("frontend/src/app/api/synastry/route.ts");
const apiServer = readRoot("scripts/jyotish_api_server.py");
test("commercial Jyotish paths resolve to a registered Python handler", () => {
for (const path of [
"/api/consultation_workflow",
"/api/active_rectification_questions",
"/api/active_rectification_score",
"/api/active_rectification_events",
"/api/varga_full",
"/api/synastry",
]) {
assert.match(apiServer, new RegExp(`['\"]${path.replaceAll("/", "\\/")}['\"]`));
}
assert.match(mastra, /\/api\/consultation_workflow/);
assert.match(rectification, /\/api\/active_rectification_questions/);
assert.match(rectification, /\/api\/active_rectification_score/);
assert.match(rectification, /\/api\/active_rectification_events/);
assert.match(synastry, /\/api\/varga_full/);
assert.match(synastry, /\/api\/synastry/);
});
@@ -0,0 +1,13 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
test("loads birth-time rectification only when its onboarding stage is reached", () => {
assert.match(page, /import dynamic from "next\/dynamic"/);
assert.match(page, /const BirthTimeRectification = dynamic\(/);
assert.match(page, /import\("@\/components\/birth-time-rectification"\)/);
assert.match(page, /ssr: false/);
assert.match(page, /role="status"/);
});
+31
View File
@@ -0,0 +1,31 @@
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def run(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, *args],
cwd=ROOT,
text=True,
capture_output=True,
timeout=120,
check=False,
)
def test_ashtakavarga_compare_uses_repository_scripts_and_public_baselines() -> None:
baseline = run("benchmarks/jyotish/scripts/run_pyjhora_compare.py", "--build-local", "--output-prefix", "ashtakavarga_contract")
assert baseline.returncode == 0, baseline.stderr or baseline.stdout
comparison = run("benchmarks/jyotish/scripts/run_ashtakavarga_compare.py")
assert comparison.returncode == 0, comparison.stderr or comparison.stdout
report = ROOT / "benchmarks/jyotish/outputs/jyotish_benchmark_round6_ashtakavarga_compare.md"
assert report.exists()
assert "BAV" in report.read_text(encoding="utf-8")
+23
View File
@@ -0,0 +1,23 @@
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_shadbala_compare_supports_installed_pyjhora_api() -> None:
completed = subprocess.run(
[sys.executable, "benchmarks/jyotish/scripts/run_shadbala_compare.py"],
cwd=ROOT,
text=True,
capture_output=True,
timeout=90,
check=False,
)
assert completed.returncode == 0, completed.stderr or completed.stdout
assert "Shadbala Benchmark" in completed.stdout
assert "Total planets: 35" in completed.stdout