fix: gate rectification candidate adoption flow

This commit is contained in:
Jesse_Chen
2026-08-04 16:47:23 +08:00
parent 7ba3abe92d
commit 79fe2d6c69
14 changed files with 436 additions and 56 deletions
+53 -12
View File
@@ -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; }
}
+2
View File
@@ -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,
};
+2 -1
View File
@@ -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-->.
+4 -2
View File
@@ -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 ?? ""),