fix(rectification): treat a yes discriminator as covering that domain's collect

A choice-card yes/weak_yes already told us the domain happened, so skip the same-domain "which year" collect without writing the ledger.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-05 16:07:27 +08:00
parent 2dc6194671
commit 988d97ae62
9 changed files with 263 additions and 9 deletions
@@ -1048,9 +1048,15 @@ async function persistExhaustionCollect(input: {
hostNarration: string;
focus?: ConversationFocus | null;
}> {
const catalog = rectificationFollowupCatalog(
input.dossier.latestResult ?? null,
input.dossier.evidence,
);
const followup = exhaustionSpokenCollectFollowup({
evidence: input.dossier.evidence,
declinedTopics: input.dossier.conversationSummary.declinedSkippedTopics,
answeredProbes: catalog.answeredProbes,
eventProbes: catalog.eventProbes,
});
const range = nonConvergingRangeNarration({
...input.decision,
@@ -282,6 +282,7 @@ export function rectificationFollowupCatalog(
contrastPacket: contrastPacketFromLatestResult(latest ?? null, evidence),
topCandidateTimes,
askedProbeKeys: askedKeys,
answeredProbes: inference?.answered_probes ?? [],
eventProbes: refinement.discriminating_event_probes,
eventClarificationProbes: refinement.event_clarification_probes,
evidenceCollectionProbes: refinement.evidence_collection_probes,
@@ -33,6 +33,8 @@
* Known-event quality probes (exam went badly for a year already
* in the ledger) are not reverse-inference cards. Dasha existence
* probes skip a year already in the ledger, not the whole domain.
* A yes/weak_yes discriminator answer covers that domain's spoken
* collect without writing the ledger. no/unsure does not.
* Contrast-packet ranking must use the same year rule: a dated
* unstructured probe stays eligible when the domain already has a
* different year. Information gain is recomputed on the current
@@ -223,6 +225,47 @@ function hasConfirmedDomain(evidence: readonly MethodFollowupEvidence[], domain:
return evidence.some((item) => item.status === "confirmed" && item.domain === domain);
}
export type AnsweredProbeCoverageRow = Readonly<{
semantic_key: string;
probe_id?: string;
answer_class: string;
classified_from?: string;
}>;
function domainForAnsweredProbe(
answer: AnsweredProbeCoverageRow,
eventProbes: readonly DiscriminatingEventProbe[],
): string | null {
const keys = new Set(
[answer.semantic_key, answer.probe_id].filter((key): key is string => Boolean(key?.trim())),
);
if (keys.size === 0) return null;
for (const probe of eventProbes) {
const semantic = probe.semantic_key?.trim() ?? "";
if (!semantic || !keys.has(semantic)) continue;
const domain = probe.domain?.trim();
if (domain) return domain;
}
return null;
}
/** Domains where a discriminator was answered yes/weak_yes. Lookup is by probe identity, not key prefix. */
export function domainsAnsweredYes(
answeredProbes: readonly AnsweredProbeCoverageRow[] | undefined,
eventProbes: readonly DiscriminatingEventProbe[] | undefined,
): Set<string> {
const domains = new Set<string>();
if (!answeredProbes?.length) return domains;
const catalog = eventProbes ?? [];
for (const answer of answeredProbes) {
if (answer.answer_class !== "yes" && answer.answer_class !== "weak_yes") continue;
if (answer.classified_from && answer.classified_from !== "choice") continue;
const domain = domainForAnsweredProbe(answer, catalog);
if (domain) domains.add(domain);
}
return domains;
}
function evidenceYear(item: MethodFollowupEvidence): number | null {
const raw = item.occurredFrom || item.occurredTo;
if (!raw || raw.length < 4 || !/^\d{4}/.test(raw)) return null;
@@ -1070,19 +1113,26 @@ function datedCollectDomainBlocked(
domain: (typeof DATED_COLLECT_ORDER)[number],
evidence: readonly MethodFollowupEvidence[],
declined: ReadonlySet<string>,
answeredYes: ReadonlySet<string> = new Set(),
): boolean {
if (domain === "health_pressure") {
return declinedHealth(declined) || hasConfirmedHealth(evidence);
return declinedHealth(declined)
|| hasConfirmedHealth(evidence)
|| answeredYes.has("health_pressure")
|| answeredYes.has("health");
}
return declined.has(domain) || hasConfirmedDomain(evidence, domain);
return declined.has(domain)
|| hasConfirmedDomain(evidence, domain)
|| answeredYes.has(domain);
}
export function nextDatedCollectFollowup(
evidence: readonly MethodFollowupEvidence[],
declined: ReadonlySet<string>,
answeredYes: ReadonlySet<string> = new Set(),
): MethodFollowup | null {
for (const domain of DATED_COLLECT_ORDER) {
if (datedCollectDomainBlocked(domain, evidence, declined)) continue;
if (datedCollectDomainBlocked(domain, evidence, declined, answeredYes)) continue;
const next = datedCollectFollowup(domain, evidence);
if (next) return next;
}
@@ -1125,9 +1175,12 @@ function otherCollectFollowup(evidence: readonly MethodFollowupEvidence[]): Meth
export function exhaustionSpokenCollectFollowup(input: {
evidence: readonly MethodFollowupEvidence[];
declinedTopics?: readonly Readonly<Record<string, unknown>>[];
answeredProbes?: readonly AnsweredProbeCoverageRow[];
eventProbes?: readonly DiscriminatingEventProbe[];
}): MethodFollowup | null {
const declined = declinedDomains(input.declinedTopics ?? []);
const dated = nextDatedCollectFollowup(input.evidence, declined);
const answeredYes = domainsAnsweredYes(input.answeredProbes, input.eventProbes);
const dated = nextDatedCollectFollowup(input.evidence, declined, answeredYes);
if (dated) return dated;
if (
!declined.has("occupation")
@@ -1506,6 +1559,7 @@ export function buildMethodFollowupPlan(input: {
eventClarificationProbes?: readonly DiscriminatingEventProbe[];
evidenceCollectionProbes?: readonly DiscriminatingEventProbe[];
askedProbeKeys?: readonly string[];
answeredProbes?: readonly AnsweredProbeCoverageRow[];
closedCollectFocuses?: readonly Readonly<Record<string, unknown>>[];
birthDate?: string | null;
accepted?: boolean;
@@ -1559,10 +1613,18 @@ export function buildMethodFollowupPlan(input: {
const ask = (why: string, varga: string, extra = "") =>
agentHint(why, varga, extra, input.evidence);
const declined = declinedDomains(input.declinedTopics ?? []);
const answeredYes = domainsAnsweredYes(input.answeredProbes, [
...(input.eventProbes ?? []),
...(input.eventClarificationProbes ?? []),
...(input.evidenceCollectionProbes ?? []),
]);
const dashaCovered = input.evidence.some(isConfirmedDated);
const relationshipCovered = hasConfirmedDomain(input.evidence, "relationship");
const careerCovered = hasConfirmedDomain(input.evidence, "career");
const familyCovered = hasConfirmedDomain(input.evidence, "family");
const relationshipCovered = hasConfirmedDomain(input.evidence, "relationship")
|| answeredYes.has("relationship");
const careerCovered = hasConfirmedDomain(input.evidence, "career")
|| answeredYes.has("career");
const familyCovered = hasConfirmedDomain(input.evidence, "family")
|| answeredYes.has("family");
const financeCovered = hasConfirmedDomain(input.evidence, "finance");
const healthCovered = hasConfirmedHealth(input.evidence);
const occupationCovered = hasConfirmedDomain(input.evidence, "occupation")
@@ -2120,7 +2182,7 @@ export function buildMethodFollowupPlan(input: {
&& precisionCard?.choice_frame
) {
next = precisionCard;
} else if ((datedCollect = nextDatedCollectFollowup(input.evidence, declined))) {
} else if ((datedCollect = nextDatedCollectFollowup(input.evidence, declined, answeredYes))) {
next = makeFollowup(datedCollect);
} else if (!occupationCovered) {
if (meetsAcceptanceEventQuality(input.evidence)) {