feat(home): write the daily card with the Agent and vary the starter heading
Independent Staging Quality Gate / validate (push) Failing after 11m34s
Independent Staging Quality Gate / publish (push) Has been skipped

The daily starlanguage card claimed to be personal but was a four-card
rotation picked by hashing the date and birth place, with the same pool
duplicated as a client fallback. It now collects chart, Vimshottari and
Narayana dasha, D9/D10 and today's transits, hands that evidence to a
dedicated Agent, and keeps the result per account per day in process. A
failed generation says so instead of printing generic advice.

The starter heading is drawn from a pool on each visit, and the
rectification card drops its fine print.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-17 22:27:49 +08:00
co-authored by Cursor
parent fd415e1d11
commit f6715ee4bf
10 changed files with 670 additions and 89 deletions
+202
View File
@@ -0,0 +1,202 @@
import { z } from "zod";
export type DailyStarlanguageCard = {
readonly trend: string;
readonly action: string;
readonly caution: string;
};
export type DailyStarlanguageEvidence = {
readonly today: string;
readonly birthTimeVerified: boolean;
readonly ascendantSign: string | null;
readonly moonSign: string | null;
readonly vimshottari: {
readonly mahadasha: string | null;
readonly antardasha: string | null;
readonly remainingDays: number | null;
readonly nakshatra: string | null;
} | null;
readonly narayanaSign: string | null;
readonly divisional: readonly {
readonly chart: string;
readonly ascendantSign: string | null;
readonly moonSign: string | null;
}[];
readonly transit: {
readonly totalTriggers: number;
readonly top: readonly string[];
} | null;
readonly functionalBenefics: readonly string[];
readonly functionalMalefics: readonly string[];
readonly missingLayers: readonly string[];
};
export type DailyStarlanguageEngineLayers = {
readonly chart: unknown;
readonly vimshottari: unknown;
readonly narayana: unknown;
readonly varga: unknown;
readonly transit: unknown;
};
const cardSchema = z.object({
trend: z.string().transform(normalizeLine).pipe(z.string().min(8)),
action: z.string().transform(normalizeLine).pipe(z.string().min(4)),
caution: z.string().transform(normalizeLine).pipe(z.string().min(4)),
});
function normalizeLine(value: string): string {
return value.replace(/\s+/g, " ").trim().slice(0, 90);
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
function asArray(value: unknown): readonly unknown[] {
return Array.isArray(value) ? value : [];
}
function asText(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim().slice(0, 60) : null;
}
function asCount(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function at(value: unknown, ...path: readonly string[]): unknown {
return path.reduce<unknown>((current, key) => asRecord(current)?.[key], value);
}
function chartModule(chart: unknown): unknown {
return at(chart, "modules", "chart") ?? chart;
}
function signOf(value: unknown): string | null {
return asText(asRecord(value)?.sign);
}
function planetSign(container: unknown, planet: string): string | null {
return signOf(at(container, "planets", planet));
}
function vimshottariLayer(response: unknown): DailyStarlanguageEvidence["vimshottari"] {
const current = at(response, "vimshottari_analysis", "current");
if (!asRecord(current)) return null;
return {
mahadasha: asText(at(current, "mahadasha", "lord")),
antardasha: asText(at(current, "antardasha", "lord")),
remainingDays: asCount(asRecord(current)?.remaining_days),
nakshatra: asText(at(response, "vimshottari_analysis", "nakshatra", "name")),
};
}
function narayanaSign(response: unknown, today: string): string | null {
const periods = asArray(asRecord(response)?.periods);
const running = periods.find((period) => {
const start = asText(asRecord(period)?.start);
const end = asText(asRecord(period)?.end);
return Boolean(start && end && start <= today && today <= end);
});
return asText(asRecord(running ?? periods[0])?.lord);
}
function divisionalLayer(response: unknown): DailyStarlanguageEvidence["divisional"] {
const result = asRecord(asRecord(response)?.result);
if (!result) return [];
return Object.entries(result)
.slice(0, 4)
.map(([chart, value]) => ({
chart,
ascendantSign: signOf(asRecord(value)?.ascendant),
moonSign: planetSign(value, "Moon"),
}))
.filter((entry) => entry.ascendantSign || entry.moonSign);
}
function transitLayer(response: unknown): DailyStarlanguageEvidence["transit"] {
const summary = asRecord(asRecord(response)?.summary);
if (!summary) return null;
const top = asArray(summary.top_triggers)
.slice(0, 3)
.map((trigger) => {
const planet = asText(asRecord(trigger)?.planet);
const target = asText(asRecord(trigger)?.target);
const event = asText(asRecord(trigger)?.event);
return [planet, target, event].filter(Boolean).join(" → ");
})
.filter((line) => line.length > 0);
return { totalTriggers: asCount(summary.total_triggers) ?? 0, top };
}
function functionalPlanets(chart: unknown, key: "functional_benefics" | "functional_malefics"): readonly string[] {
const snapshot = asRecord(at(chart, "ai_prompt_pack", "evidence_snapshot", "functional_benefic_malefic"));
if (!snapshot || snapshot.status !== "used") return [];
return asArray(snapshot[key])
.map((planet) => asText(planet))
.filter((planet): planet is string => Boolean(planet))
.slice(0, 5);
}
export function dailyStarlanguageEvidence(
layers: DailyStarlanguageEngineLayers,
today: string,
birthTimeVerified: boolean,
): DailyStarlanguageEvidence {
const chart = chartModule(layers.chart);
const vimshottari = vimshottariLayer(layers.vimshottari);
const divisional = divisionalLayer(layers.varga);
const transit = transitLayer(layers.transit);
const narayana = narayanaSign(layers.narayana, today);
const missingLayers = [
vimshottari ? "" : "Vimshottari",
narayana ? "" : "Narayana",
divisional.length ? "" : "分盘",
transit ? "" : "过境",
].filter((layer) => layer.length > 0);
return {
today,
birthTimeVerified,
ascendantSign: signOf(asRecord(chart)?.ascendant),
moonSign: planetSign(chart, "Moon"),
vimshottari,
narayanaSign: narayana,
divisional,
transit,
functionalBenefics: functionalPlanets(layers.chart, "functional_benefics"),
functionalMalefics: functionalPlanets(layers.chart, "functional_malefics"),
missingLayers,
};
}
export function dailyStarlanguagePrompt(evidence: DailyStarlanguageEvidence): string {
return [
`今天的日期:${evidence.today}`,
evidence.birthTimeVerified
? "出生时间状态:已确认。"
: "出生时间状态:未经校正确认,禁止任何依赖分钟精度的判断。",
JSON.stringify(evidence),
"只依据上面这份证据写今天的星语。缺失的层不要假装存在,也不要提到证据缺失本身。",
].join("\n");
}
export function parseDailyStarlanguageText(text: string): DailyStarlanguageCard | null {
const normalized = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
const start = normalized.indexOf("{");
const end = normalized.lastIndexOf("}");
if (start < 0 || end <= start) return null;
let payload: unknown;
try {
payload = JSON.parse(normalized.slice(start, end + 1));
} catch {
return null;
}
const parsed = cardSchema.safeParse(payload);
return parsed.success ? parsed.data : null;
}
export function dailyStarlanguageCacheKey(userId: string, profileFingerprint: string, today: string): string {
return JSON.stringify([userId, profileFingerprint, today]);
}
+17
View File
@@ -0,0 +1,17 @@
const starterPrompts = [
"今天想先理清什么?",
"现在最想弄明白哪件事?",
"今天想从哪里说起?",
"此刻最挂心的是什么?",
"最近有什么反复出现的问题?",
"眼下最想要一个答案的是什么?",
"想先把哪件事想清楚?",
"有什么事正等着你决定?",
] as const;
export const starterPromptVariants: readonly string[] = starterPrompts;
export function createStarterPrompt(selection = Math.random()): string {
const index = Math.floor(selection * starterPrompts.length);
return starterPrompts[Math.min(Math.max(index, 0), starterPrompts.length - 1)];
}