fix: gate rectification candidate adoption flow
This commit is contained in:
@@ -1709,24 +1709,65 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background:
|
||||
|
||||
.rectification-candidates {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
gap: 14px;
|
||||
margin: 8px 0 16px;
|
||||
padding: 16px;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
background: color-mix(in srgb, var(--card) 92%, transparent);
|
||||
}
|
||||
.rectification-candidates-heading { display: grid; gap: 4px; }
|
||||
.rectification-candidates-heading { display: grid; gap: 5px; }
|
||||
.rectification-candidates-heading strong { font-size: 16px; }
|
||||
.rectification-candidates-heading span,
|
||||
.rectification-candidate span { color: var(--muted-foreground); font-size: 12px; }
|
||||
.rectification-candidate-list { display: grid; gap: 10px; }
|
||||
.rectification-candidate { display: grid; grid-template-columns: minmax(88px, auto) minmax(80px, 1fr) auto; gap: 12px; align-items: center; }
|
||||
.rectification-candidate > div:first-child { display: grid; gap: 2px; }
|
||||
.rectification-candidate.is-selected { color: var(--foreground); }
|
||||
.rectification-support { height: 6px; overflow: hidden; border-radius: 999px; background: var(--muted); }
|
||||
.rectification-support i { display: block; height: 100%; border-radius: inherit; background: var(--primary); }
|
||||
.rectification-candidate-support { color: var(--muted-foreground); font-size: 12px; line-height: 1.5; }
|
||||
.rectification-candidate-list {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.rectification-candidate {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
min-height: 132px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
color: var(--foreground);
|
||||
background: var(--card);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rectification-candidate:hover:not(:disabled) { border-color: color-mix(in srgb, var(--primary) 45%, var(--border)); }
|
||||
.rectification-candidate:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
||||
.rectification-candidate:disabled { cursor: default; opacity: 1; }
|
||||
.rectification-candidate.is-selected {
|
||||
border-color: color-mix(in srgb, var(--primary) 55%, var(--border));
|
||||
background: color-mix(in srgb, var(--primary) 7%, var(--card));
|
||||
}
|
||||
.rectification-candidate-time { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.rectification-candidate-time strong { font-size: 22px; font-variant-numeric: tabular-nums; }
|
||||
.rectification-candidate-badge {
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
color: var(--primary) !important;
|
||||
background: color-mix(in srgb, var(--primary) 10%, transparent);
|
||||
font-size: 11px !important;
|
||||
font-weight: 650;
|
||||
}
|
||||
.rectification-candidate-action { align-self: end; color: var(--primary); font-size: 13px; font-weight: 650; }
|
||||
.rectification-saved { margin: 8px 0 16px; color: var(--foreground); font-size: 14px; }
|
||||
@media (max-width: 640px) {
|
||||
.rectification-candidate { grid-template-columns: 1fr auto; }
|
||||
.rectification-support { grid-column: 1 / -1; grid-row: 2; }
|
||||
.rectification-candidates { padding: 14px; }
|
||||
.rectification-candidate-list {
|
||||
grid-template-columns: none;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: minmax(180px, 78%);
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
.rectification-candidate { min-height: 122px; padding: 12px; scroll-snap-align: start; }
|
||||
}
|
||||
|
||||
@@ -1450,6 +1450,8 @@ export default function Home() {
|
||||
try {
|
||||
const latest = await fetchAccount();
|
||||
if (!accountRefreshGuard.current.isCurrent(requestIdentity)) return;
|
||||
const nextProfile = readProfile(latest.profile);
|
||||
setProfile(nextProfile);
|
||||
setAccount((current) => {
|
||||
if (current?.rectificationCase
|
||||
&& latest.rectificationCase?.caseId === current.rectificationCase.caseId
|
||||
|
||||
@@ -117,16 +117,16 @@ export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps)
|
||||
{displayState && (
|
||||
<section className="birth-time-profile-result" aria-label="生时校正结果">
|
||||
<div className="birth-time-profile-result-heading">
|
||||
<span>生时校正结果</span>
|
||||
<strong>{displayState.kind === "candidate" ? "候选时间" : displayState.kind === "accepted" ? "用户已选择" : "已确认"}</strong>
|
||||
<span>出生时间记录</span>
|
||||
<strong>{displayState.kind === "candidate" ? "候选时间" : displayState.kind === "accepted" ? "校正采用" : "引擎确认"}</strong>
|
||||
</div>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>{displayState.kind === "candidate" ? "待验证候选时间" : displayState.kind === "accepted" ? "校正采用时间" : "已确认校正时间"}</dt>
|
||||
<dt>{displayState.kind === "candidate" ? "待验证候选" : "当前排盘时间"}</dt>
|
||||
<dd>{displayState.activeTime}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>原始填报</dt>
|
||||
<dt>原始填报时间</dt>
|
||||
<dd>{displayState.reportedLabel}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
@@ -303,28 +303,32 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
{candidateResult?.selectionAllowed && candidateResult.candidates.length > 0 && (
|
||||
<section className="rectification-candidates" aria-label="生时校正候选时间">
|
||||
<div className="rectification-candidates-heading">
|
||||
<strong>{candidateResult.confirmationAllowed ? "已通过确认门" : "请选择校正采用时间"}</strong>
|
||||
<span>相对支持度仅用于本次候选比较,不是统计概率。</span>
|
||||
<strong>{candidateResult.confirmationAllowed ? "确认校正时间" : "当前可能的出生时间"}</strong>
|
||||
<span>这是根据你目前提供的人生事件推算出的几种可能时间。可以先采用一个作为当前排盘时间,也可以继续补充事件;新增证据后,候选和相对支持度会重新计算,准确度还可以继续提高。</span>
|
||||
<span>相对支持度不是统计概率,采用不会覆盖原始填报时间。</span>
|
||||
</div>
|
||||
<div className="rectification-candidate-list">
|
||||
{candidateResult.candidates.map((candidate) => {
|
||||
const selected = candidateResult.selectedTime === candidate.time;
|
||||
const recommended = !candidateResult.selectedTime && candidate.rank === 1;
|
||||
return (
|
||||
<div className={`rectification-candidate${selected ? " is-selected" : ""}`} key={`${candidateResult.resultId}-${candidate.time}`}>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className={`rectification-candidate${selected ? " is-selected" : ""}`}
|
||||
key={`${candidateResult.resultId}-${candidate.time}`}
|
||||
disabled={selected || Boolean(acceptingTime)}
|
||||
onClick={() => void acceptCandidate(candidate.time)}
|
||||
>
|
||||
<span className="rectification-candidate-time">
|
||||
<strong>{candidate.time}</strong>
|
||||
<span>相对支持度 {candidate.relative_support}%</span>
|
||||
</div>
|
||||
<div className="rectification-support" aria-hidden="true"><i style={{ width: `${candidate.relative_support}%` }} /></div>
|
||||
<Button
|
||||
type="button"
|
||||
variant={selected ? "secondary" : "outline"}
|
||||
disabled={Boolean(candidateResult.selectedTime) || Boolean(acceptingTime)}
|
||||
onClick={() => void acceptCandidate(candidate.time)}
|
||||
>
|
||||
{selected ? "已采用" : acceptingTime === candidate.time ? "保存中…" : `采用 ${candidate.time}`}
|
||||
</Button>
|
||||
</div>
|
||||
{selected && <span className="rectification-candidate-badge">已采用</span>}
|
||||
{recommended && <span className="rectification-candidate-badge">当前推荐</span>}
|
||||
</span>
|
||||
<span className="rectification-candidate-support">相对支持度 {candidate.relative_support}%</span>
|
||||
<span className="rectification-candidate-action">
|
||||
{selected ? "已采用" : acceptingTime === candidate.time ? "保存中…" : candidateResult.selectedTime ? "改选为此时间" : "采用此时间"}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -332,7 +336,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) {
|
||||
)}
|
||||
{savedTime && (
|
||||
<p className="rectification-saved" role="status">
|
||||
{savedStatus === "confirmed" ? "已确认校正时间" : "校正采用时间"}:{savedTime}。后续排盘将使用该时间。
|
||||
{savedStatus === "confirmed" ? "已确认校正时间" : "校正采用时间"}:{savedTime}。后续排盘将使用该时间;你仍可继续补充事件或改选其他候选。
|
||||
</p>
|
||||
)}
|
||||
{error && <p className="error-message" role="alert">{error}</p>}
|
||||
|
||||
@@ -229,7 +229,6 @@ export function applyAccountProfileConcurrencyGuards<
|
||||
|
||||
export type AccountBirthTimeApplicationPatch = Readonly<{
|
||||
active_birth_time?: null;
|
||||
birth_time?: null;
|
||||
birth_time_status?: "reported";
|
||||
rectification_case_id?: null;
|
||||
}>;
|
||||
@@ -241,7 +240,6 @@ export function resolveAccountBirthTimeApplicationPatch(
|
||||
if (!current) {
|
||||
return patch.birth_time_source ? {
|
||||
active_birth_time: null,
|
||||
birth_time: null,
|
||||
birth_time_status: "reported",
|
||||
rectification_case_id: null,
|
||||
} : {};
|
||||
@@ -262,7 +260,6 @@ export function resolveAccountBirthTimeApplicationPatch(
|
||||
if (!declarationChanged && !repairsMissingStatus) return {};
|
||||
return {
|
||||
active_birth_time: null,
|
||||
birth_time: null,
|
||||
birth_time_status: "reported",
|
||||
rectification_case_id: null,
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ METHODOLOGY
|
||||
- Load and follow the jyotish-vedic-astrology skill before every substantive step. Its references (birth-time-rectification-advanced.md, birth-time-rectification-decision-tree.md, oracle overlays) are your method source.
|
||||
- ALL computation goes through the provided engine tools: rectification-gate, rectification-scan, rectification-score, rectification-diagnostics, rectification-candidate-features, rectification-confirm. Candidate persistence and adoption go only through rectification-accept-candidate or rectification-save-birth-time. Never invent a candidate time, score, date, divisional-chart fact, or birth minute in prose.
|
||||
- Workflow: run rectification-gate first to learn the server-owned candidate_range, starting accuracy, and which dated events are most valuable. Always reuse that exact candidate_range in later tools; never create or widen one yourself. Then collect dated life events conversationally (the user narrates; ask for a date when the event is not dated, but do not press endlessly). Then run rectification-scan when available, rectification-score to see candidate minutes, rectification-diagnostics to see what is weak, and ask one or two natural follow-ups to fill the weakest domain or the most unstable event. Re-score. When the candidate is stable across events and domains, run rectification-confirm.
|
||||
- Every rectification-confirm call must set offer_selection deliberately. If you will ask for another event or date detail in the same reply, offer_selection must be false. Set it true only when the user explicitly asks to choose/stop now, or when you are ending evidence collection and the only requested next action is choosing among candidates. Never both ask for more evidence and offer candidate adoption in the same reply.
|
||||
- Use the decision tree: Dasha plus dated events establish the frame; D9 and D10 are core for relationship and career; D4/D24/D2/D11/D7/D30 are topic-specific; D60 is reference-only and never drives a conclusion.
|
||||
- Keep event ids stable: reuse the same id for the same life event in every tool call.
|
||||
|
||||
@@ -25,7 +26,7 @@ TRUTH BOUNDARIES (from the skill overlay)
|
||||
- If confirmation_allowed=true, still require explicit user agreement before saving the representative minute.
|
||||
|
||||
SAVING
|
||||
- When rectification-confirm returns selection_allowed=true, present the available candidates with their server-returned relative support and ask the user to choose; the UI may also render the same server candidates.
|
||||
- When rectification-confirm returns selection_allowed=true, explain that these are current possibilities derived from the evidence so far. The user may adopt one now or keep adding events; later evidence can replace the candidate set and relative support. The UI may render the same server candidates.
|
||||
- If the user explicitly says “就用 HH:MM”, “选择 HH:MM”, or equivalent for one of the persisted candidates, call rectification-accept-candidate. A successful status=accepted must be described as “校正采用时间” or “用户选择的当前排盘时间”, never “已确认唯一出生时间”.
|
||||
- Only call rectification-save-birth-time when rectification-confirm returned confirmation_allowed=true and the user explicitly agrees to the representative minute. A successful status=confirmed may be described as “已确认校正时间”.
|
||||
- After either successful write, tell the user the saved status honestly and append exactly this hidden block at the end (nothing after it): <!--AYANAM_RECTIFICATION_SAVED:HH:MM-->.
|
||||
|
||||
@@ -531,10 +531,11 @@ export function createAgenticRectificationTools(ctx: AgenticRectificationContext
|
||||
const confirmTool = createTool({
|
||||
id: "rectification-confirm",
|
||||
description:
|
||||
"Run the high-rigor confirmation gate for the candidate range and the user's dated events. Official VedAstro runs only after the local external-validation entry gate is ready; external_validation_status=not_evaluated means it was not invoked, not that it failed. Neighbor stability and leave-one-out are diagnostic confidence indicators, not hard blockers. Returns whether a precise minute can be confirmed, the representative minute, and the reasons. Only call once you have enough confirmed dated events across domains. This does NOT write anything.",
|
||||
"Run the high-rigor confirmation gate for the candidate range and the user's dated events. Official VedAstro runs only after the local external-validation entry gate is ready; external_validation_status=not_evaluated means it was not invoked, not that it failed. Neighbor stability and leave-one-out are diagnostic confidence indicators, not hard blockers. Set offer_selection=true only when this turn is ready to offer working-time choices; keep it false while asking for more evidence. Returns whether a precise minute can be confirmed, the representative minute, and the reasons. This does NOT write anything.",
|
||||
inputSchema: z.object({
|
||||
candidate_range: candidateRangeSchema,
|
||||
events: z.array(agenticRectificationEventSchema).min(1).max(40),
|
||||
offer_selection: z.boolean().default(false),
|
||||
}).strict(),
|
||||
execute: async (input) => {
|
||||
requireServerCandidateRange(input.candidate_range, ctx.candidateRange);
|
||||
@@ -578,7 +579,8 @@ export function createAgenticRectificationTools(ctx: AgenticRectificationContext
|
||||
: [{ rank: 1, time: representativeTime, relative_support: 100, tied_minute_count: 1 }];
|
||||
const eventCount = typeof data.event_count === "number" ? data.event_count : 0;
|
||||
const domainCount = typeof data.domain_count === "number" ? data.domain_count : 0;
|
||||
const selectionAllowed = eventCount >= 3 && domainCount >= 2 && candidates.length > 0;
|
||||
const candidateEligible = eventCount >= 3 && domainCount >= 2 && candidates.length > 0;
|
||||
const selectionAllowed = confirmationAllowed || (input.offer_selection && candidateEligible);
|
||||
const persisted = await ctx.persistCandidateResult({
|
||||
engineResultId: String(data.result_id ?? ""),
|
||||
canonicalInputHash: String(data.canonical_input_hash ?? ""),
|
||||
|
||||
Reference in New Issue
Block a user