Merge pull request #2 from jesse-ux/codex/optimize-runtime-ux
feat: enforce commercial Jyotish workflow contracts
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"/);
|
||||
});
|
||||
@@ -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")
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user