merge: synchronize remote main

This commit is contained in:
Jesse_Chen
2026-07-21 22:57:50 +08:00
130 changed files with 10441 additions and 553 deletions
+2 -1
View File
@@ -1,8 +1,9 @@
export type ReplyTheme = "career" | "marriage" | "timing" | "general";
export type ReplyTheme = "career" | "marriage" | "wealth" | "timing" | "general";
const fallbackSuggestions: Record<ReplyTheme, readonly [string, string, string]> = {
career: ["我更适合怎样的职业路径?", "未来一年事业上要避开什么?", "我该如何发挥自己的优势?"],
marriage: ["我在关系里容易重复什么模式?", "怎样的伴侣更适合我?", "未来一年关系上要注意什么?"],
wealth: ["我的财富增长方式是什么?", "接下来财务上要避开什么?", "我该如何稳定提升收入?"],
timing: ["接下来最值得把握的阶段是什么?", "哪些时期更适合主动行动?", "我现在应该优先准备什么?"],
general: ["未来一年,事业和收入该关注什么?", "我的关系模式是什么?", "未来哪些阶段值得把握?"],
};
@@ -5,38 +5,10 @@ type CandidateCompletionRequest = {
readonly time: string;
};
function record(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object"
? value as Record<string, unknown>
: null;
}
/** Direct adoption was superseded by versioned high-confidence confirmation. */
export function candidateWorkingTime(
stored: unknown,
request: CandidateCompletionRequest,
_stored: unknown,
_request: CandidateCompletionRequest,
): string | null {
const assessment = record(stored);
const candidate = record(assessment?.candidate_result);
const winner = record(candidate?.winningSegment);
const turn = record(assessment?.turn_state);
const action = record(turn?.nextAction);
const actionKind = action?.kind;
const terminal = actionKind === "present_low_result"
|| actionKind === "present_medium_result"
|| actionKind === "candidate_saved";
const terminalStatusMatches = actionKind === "present_low_result"
? assessment?.status === "rectifying"
: (actionKind === "present_medium_result" || actionKind === "candidate_saved")
&& assessment?.status === "candidate";
return assessment?.id === request.caseId
&& assessment?.user_id === request.userId
&& assessment?.journey_protocol === "legacy-guided-v1"
&& terminalStatusMatches
&& assessment.candidate_result_id === request.resultId
&& action?.resultId === request.resultId
&& terminal
&& winner?.representativeTime === request.time
? request.time
: null;
return null;
}
@@ -16,7 +16,6 @@ type DraftRevision = GuidedMutation & {
};
type CandidateSave = GuidedMutation & { readonly resultId: string };
type CandidateConfirmation = CandidateSave & { readonly time: string };
type CandidateCompletion = Pick<CandidateSave, "caseId" | "resultId"> & { readonly time: string };
const errorPayloadSchema = z.object({
message: z.string().optional(),
@@ -69,22 +68,3 @@ export function confirmGuidedBirthTimeCandidate(
) {
return send({ type: "confirm_guided_candidate", ...input });
}
export async function completeGuidedBirthTimeCandidate(
input: CandidateCompletion,
) {
const { response, payload } = await postJson({
url: "/api/birth-time-candidate-completion",
body: JSON.stringify(input),
retryLostResponse: false,
});
if (!response.ok) {
const parsed = errorPayloadSchema.safeParse(payload);
throw new GuidedBirthTimeRequestError(
response.status,
parsed.success
? parsed.data.message ?? parsed.data.error ?? "候选时间暂时无法保存"
: "候选时间暂时无法保存",
);
}
}
+1 -1
View File
@@ -203,7 +203,7 @@ export function isDeclaredBirthProfileComplete(
export function isBirthTimeReadyForConsultation(draft: BirthTimeDraft) {
return isBirthClockTime(draft.time)
&& (draft.birthTimeStatus === "candidate" || draft.birthTimeStatus === "confirmed");
&& draft.birthTimeStatus === "confirmed";
}
const declaredBirthInputKeys = [
+6
View File
@@ -3,6 +3,12 @@ export type ChatMessage = {
readonly text: string;
readonly suggestions?: readonly string[];
readonly techniqueTruth?: string;
readonly workflowReceipt?: {
readonly route: string;
readonly status: string;
readonly preciseTiming: string;
readonly missingLayers: readonly string[];
};
};
export type ChatMessageView = ChatMessage & {
@@ -0,0 +1,37 @@
import type { ChatMessage } from "./chat-message-view";
export function consultationReportMarkdown(input: {
title: string;
messages: readonly ChatMessage[];
}) {
const latestAssistant = [...input.messages].reverse().find((message) => message.role === "assistant");
const evidence = latestAssistant?.workflowReceipt;
return [
`# ${input.title}`,
"",
"## 最新回答",
latestAssistant?.text || "暂无回答。",
"",
"## Claim boundary",
`technique_truth: ${latestAssistant?.techniqueTruth || "unknown"}`,
evidence ? `workflow_route: ${evidence.route}` : "workflow_route: unknown",
evidence ? `workflow_status: ${evidence.status}` : "workflow_status: unknown",
evidence ? `precise_timing: ${evidence.preciseTiming}` : "precise_timing: unknown",
evidence ? `missing_layers: ${evidence.missingLayers.join(" / ") || "none"}` : "missing_layers: unknown",
"",
"## Boundary",
"本报告保留证据边界;未闭环内容不得包装成确定预测。",
].join("\n");
}
export function downloadMarkdownReport(title: string, markdown: string) {
const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = `${title.replace(/[\\/:*?"<>|]+/g, "-")}.md`;
document.body.append(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
}
@@ -2,15 +2,64 @@ export const consultationThemeValues = ["career", "marriage", "wealth", "timing"
export type ConsultationTheme = typeof consultationThemeValues[number];
export type ConsultationWorkflowRoute =
| "career"
| "marriage"
| "wealth"
| "timing"
| "rectification"
| "prashna"
| "general";
type WorkflowProjection = {
question: string;
themes: readonly ("career" | "marriage" | "wealth")[];
strictWorkflowRoute: ConsultationWorkflowRoute;
requiredLayers: readonly string[];
claimBoundary: string;
};
const routeRequirements: Record<ConsultationTheme, Omit<WorkflowProjection, "question"> & { prefix?: string }> = {
career: {
themes: ["career"],
strictWorkflowRoute: "career",
requiredLayers: ["D1", "D10", "10th house/lord", "A10", "AmK", "Vimshottari", "Narayana", "Transit"],
claimBoundary: "career_direction_and_broad_timing_only",
},
marriage: {
themes: ["marriage"],
strictWorkflowRoute: "marriage",
requiredLayers: ["D1", "D9", "7th house/lord", "Venus/Jupiter", "DK", "UL", "A7", "Vimshottari", "Narayana", "Transit"],
claimBoundary: "relationship_pattern_and_broad_window_only",
},
wealth: {
themes: ["wealth"],
strictWorkflowRoute: "wealth",
requiredLayers: ["D1", "D2", "D11", "2nd/11th/9th/5th houses", "Wealth Yogas", "Ashtakavarga", "Dasha"],
claimBoundary: "wealth_structure_not_financial_advice",
},
timing: {
themes: ["career"],
strictWorkflowRoute: "timing",
requiredLayers: ["Vimshottari", "Narayana", "Transit", "Varga", "negative holdout gate"],
claimBoundary: "candidate_day_month_window_only_until_holdout_passes",
prefix: "应期与阶段问题:",
},
general: {
themes: ["career", "marriage", "wealth"],
strictWorkflowRoute: "general",
requiredLayers: ["D1", "D9", "D10", "D2", "Dasha", "Narayana", "Transit", "Functional Benefic/Malefic"],
claimBoundary: "multi_domain_summary_with_missing_layers_disclosed",
},
};
export function projectConsultationWorkflowRequest(question: string, theme: ConsultationTheme) {
switch (theme) {
case "career":
case "marriage":
case "wealth":
return { question, themes: [theme] } as const;
case "timing":
return { question: `应期与阶段问题:${question}`, themes: ["career"] } as const;
case "general":
return { question, themes: ["career", "marriage", "wealth"] } as const;
}
const requirement = routeRequirements[theme];
return {
question: `${requirement.prefix ?? ""}${question}`,
themes: requirement.themes,
strictWorkflowRoute: requirement.strictWorkflowRoute,
requiredLayers: requirement.requiredLayers,
claimBoundary: requirement.claimBoundary,
} satisfies WorkflowProjection;
}
+50
View File
@@ -0,0 +1,50 @@
import type { ConsultationTheme } from "./consultation-workflow-request";
export type GuidedJyotishTopic = {
id: ConsultationTheme;
label: string;
prompt: string;
strictWorkflowRoute: string;
evidencePreview: string[];
confidenceCap: "low" | "medium";
claimBoundary: string;
};
export const defaultGuidedJyotishTopics: GuidedJyotishTopic[] = [
{
id: "career",
label: "事业",
prompt: "未来一年,事业和收入该关注什么?",
strictWorkflowRoute: "career",
evidencePreview: ["D1", "D10", "A10", "Vimshottari", "Narayana", "Transit"],
confidenceCap: "medium",
claimBoundary: "输出职业结构与阶段判断;具体日/月只作为候选窗口。",
},
{
id: "marriage",
label: "关系",
prompt: "我的关系模式是什么?",
strictWorkflowRoute: "marriage",
evidencePreview: ["D1", "D9", "7宫", "DK", "UL", "Vimshottari", "Narayana"],
confidenceCap: "medium",
claimBoundary: "输出关系模式与宽窗口;不承诺某日必然发生事件。",
},
{
id: "wealth",
label: "财富",
prompt: "我的财富增长方式和风险点是什么?",
strictWorkflowRoute: "wealth",
evidencePreview: ["D1", "D2", "D11", "2/11宫", "财富 Yoga", "Ashtakavarga"],
confidenceCap: "medium",
claimBoundary: "输出财富结构和风险类型;不替代投资建议。",
},
{
id: "timing",
label: "时运",
prompt: "未来哪些阶段值得把握?",
strictWorkflowRoute: "timing",
evidencePreview: ["Dasha", "Narayana", "Transit", "Varga"],
confidenceCap: "low",
claimBoundary: "精确月/日仍是探索性候选,未通过独立 holdout 前不升级。",
},
];
+2 -1
View File
@@ -77,7 +77,7 @@ type OnboardingProfileFingerprintInput = {
};
export type OnboardingSuggestion = {
readonly theme: "career" | "marriage" | "timing";
readonly theme: ConsultationTheme;
readonly text: string;
};
@@ -268,3 +268,4 @@ export async function requestOnboardingWithRecovery(
throw lastError ?? new OnboardingRequestError("pending");
}
import type { ConsultationTheme } from "./consultation-workflow-request";
@@ -0,0 +1,82 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
type OracleSummary = {
ready: string[];
partial: string[];
blocked: string[];
};
export type TruthSourceRuntimeIdentity = {
status: "ok" | "blocked";
path: string;
mountStatus: "mounted" | "not_mounted";
commit: string;
skillVersion: string;
evidencePacketCount: number | null;
oracleSummary: OracleSummary;
claimGateStatus: "ready" | "partial_or_blocked_present" | "not_mounted";
};
export const DEFAULT_RESEARCH_TRUTH_SOURCE_PATH = "/Users/wuyongnaren/Documents/印度占星";
function readJson(path: string): unknown {
return JSON.parse(readFileSync(path, "utf8"));
}
function readEvidencePacketCount(researchPath: string): number | null {
const indexPath = join(researchPath, "references/oracle/evidence_packet_index_2026_07_19.json");
if (!existsSync(indexPath)) return null;
const data = readJson(indexPath) as { summary?: { packet_count?: unknown } };
return typeof data.summary?.packet_count === "number" ? data.summary.packet_count : null;
}
function readSkillVersion(researchPath: string): string {
const skillPath = join(researchPath, "SKILL.md");
if (!existsSync(skillPath)) return "unknown";
const source = readFileSync(skillPath, "utf8");
const version = source.match(/(?:version|Version)[:]\s*([^\n]+)/);
return version?.[1]?.trim() || "present_unversioned";
}
function readCommit(researchPath: string): string {
const headPath = join(researchPath, ".git/HEAD");
if (!existsSync(headPath)) return "unknown";
const head = readFileSync(headPath, "utf8").trim();
if (!head.startsWith("ref: ")) return head.slice(0, 40);
const refPath = join(researchPath, ".git", head.slice(5));
if (!existsSync(refPath)) return "unknown";
return readFileSync(refPath, "utf8").trim().slice(0, 40);
}
export function getTruthSourceRuntimeIdentity(
researchPath = process.env.JYOTISH_RESEARCH_TRUTH_SOURCE_PATH ?? DEFAULT_RESEARCH_TRUTH_SOURCE_PATH,
): TruthSourceRuntimeIdentity {
if (!existsSync(researchPath)) {
return {
status: "blocked",
path: researchPath,
mountStatus: "not_mounted",
commit: "unknown",
skillVersion: "unknown",
evidencePacketCount: null,
oracleSummary: { ready: [], partial: [], blocked: ["research_truth_source_not_mounted"] },
claimGateStatus: "not_mounted",
};
}
const evidencePacketCount = readEvidencePacketCount(researchPath);
const blocked = evidencePacketCount === null ? ["evidence_packet_index_missing"] : [];
const partial = ["commercial_runtime_identity_only"];
return {
status: blocked.length ? "blocked" : "ok",
path: researchPath,
mountStatus: "mounted",
commit: readCommit(researchPath),
skillVersion: readSkillVersion(researchPath),
evidencePacketCount,
oracleSummary: { ready: [], partial, blocked },
claimGateStatus: blocked.length ? "partial_or_blocked_present" : "partial_or_blocked_present",
};
}