fix(rectification): compare health and occupation aliases through one merge (BUG-672)
Independent Staging Quality Gate / validate (push) Successful in 12m12s
Independent Staging Quality Gate / publish (push) Successful in 24m21s

Skipped or already-reported health must not be asked again as health_pressure, and occupation declined as other must still close the occupation line.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-13 23:08:55 +08:00
co-authored by Cursor
parent 9912760104
commit 14d199e694
13 changed files with 442 additions and 57 deletions
@@ -23,6 +23,7 @@ import {
SEMANTIC_YEAR_KEY,
} from "../v9/probe-question-contract.ts";
import { probeBelowAdultFloor } from "../v9/adult-floor.ts";
import { canonicalCollectDomain, sameCollectDomain } from "../v9/domain-alias.ts";
export type ContrastChoiceKind = "existence" | "varga_style" | "event_quality";
@@ -235,7 +236,7 @@ export function mentionedVargaKeysFromLedgerEvidence(
keys.add("varga.d2");
keys.add("varga.d11");
}
if (item.domain === "health_pressure" || HEALTH_RE.test(summary)) keys.add("varga.d30");
if (sameCollectDomain(item.domain, "health_pressure") || HEALTH_RE.test(summary)) keys.add("varga.d30");
}
return [...keys];
}
@@ -254,7 +255,7 @@ export function askedEventProbeKeysFromLedgerEvidence(
if (!item.domain) continue;
for (const date of [item.occurredFrom, item.occurredTo]) {
const year = date?.match(/^(\d{4})/)?.[1];
if (year) keys.add(`${item.domain}.${year}`);
if (year) keys.add(`${canonicalCollectDomain(item.domain)}.${year}`);
}
}
return [...keys];
@@ -273,7 +274,7 @@ export function datedDomainsFromEvidence(
if (item.status && !LIVE_EVIDENCE.has(item.status)) continue;
if (!item.domain) continue;
const dated = [item.occurredFrom, item.occurredTo].some((value) => /^\d{4}/.test(value ?? ""));
if (dated) domains.add(item.domain);
if (dated) domains.add(canonicalCollectDomain(item.domain));
}
return [...domains];
}
@@ -287,7 +288,7 @@ export function volunteeredDomainsFromEvidence(
const domains = new Set<string>();
for (const item of evidence) {
if (item.status && !LIVE_EVIDENCE.has(item.status)) continue;
if (item.domain) domains.add(item.domain);
if (item.domain) domains.add(canonicalCollectDomain(item.domain));
}
return [...domains];
}
@@ -329,18 +330,20 @@ export function buildCandidateContrastPacket(input: {
}): CandidateContrastPacket {
const asked = new Set(input.askedKeys ?? []);
// mentionedKeys are ranking-only; passing them here must not skip remaining varga probes.
const provided = new Set(input.providedDomains ?? []);
const provided = new Set((input.providedDomains ?? []).map((item) => canonicalCollectDomain(item)));
const fromEngine = (input.engineProbes ?? []).flatMap((probe) => {
const built = probeFromEngine(probe, input.candidateSetVersion, input.calculationResultId ?? null);
if (!built) return [];
const eventKey = built.domain && built.year ? `${built.domain}.${built.year}` : null;
const eventKey = built.domain && built.year
? `${canonicalCollectDomain(built.domain)}.${built.year}`
: null;
const isStructured = isStructuredDiscriminator(built);
if (
asked.has(built.semanticKey)
|| asked.has(built.candidateSplitHash)
|| asked.has(built.probeId)
|| (!isStructured && eventKey && asked.has(eventKey))
|| (!isStructured && !(built.year && built.year > 0) && built.domain && provided.has(built.domain))
|| (!isStructured && !(built.year && built.year > 0) && built.domain && provided.has(canonicalCollectDomain(built.domain)))
) {
return [];
}
@@ -5,6 +5,7 @@
*/
import { USER_COLLECT_QUESTION } from "../user-copy.ts";
import { canonicalCollectDomain } from "./domain-alias.ts";
export const COLLECT_KIND_ORDER = [
"education",
@@ -83,9 +84,9 @@ const INVITE_VALUE = 2;
const GENERIC_VALUE = 0.15;
export function normalizeCollectKind(domain: string | null | undefined): CollectKind | null {
if (domain === "health" || domain === "health_pressure") return "health_pressure";
if ((COLLECT_KIND_ORDER as readonly string[]).includes(domain ?? "")) {
return domain as CollectKind;
const canonical = canonicalCollectDomain(domain);
if ((COLLECT_KIND_ORDER as readonly string[]).includes(canonical)) {
return canonical as CollectKind;
}
return null;
}
@@ -0,0 +1,42 @@
/**
* Read-side aliases for collect domains.
*
* Ledger and DB focus store `health`; the planner / engine catalog use
* `health_pressure`. Occupation focuses persist as `other` under a
* `collect:occupation:` question id. Comparisons go through here so the
* two spellings are one domain. Persistable focus domains stay unchanged.
*/
export function canonicalCollectDomain(
raw: string | null | undefined,
questionId?: string | null,
): string {
const value = typeof raw === "string" ? raw.trim() : "";
if (value === "health") return "health_pressure";
const id = typeof questionId === "string" ? questionId : "";
if (value === "other" && id.startsWith("collect:occupation:")) return "occupation";
return value;
}
export function sameCollectDomain(
left: string | null | undefined,
right: string | null | undefined,
questionId?: string | null,
): boolean {
const a = canonicalCollectDomain(left);
const b = canonicalCollectDomain(right, questionId);
return Boolean(a) && a === b;
}
export function declinedHasDomain(
declined: ReadonlySet<string>,
domain: string | null | undefined,
questionId?: string | null,
): boolean {
const wanted = canonicalCollectDomain(domain, questionId);
if (!wanted) return false;
for (const item of declined) {
if (canonicalCollectDomain(item) === wanted) return true;
}
return false;
}
@@ -105,6 +105,11 @@ import {
type CollectKind,
type CollectionPoolItem,
} from "./collection-question-pool.ts";
import {
canonicalCollectDomain,
declinedHasDomain,
sameCollectDomain,
} from "./domain-alias.ts";
export { GENERIC_COLLECT_QUESTION };
import {
@@ -275,7 +280,7 @@ function isConfirmedDated(
}
function hasConfirmedDomain(evidence: readonly MethodFollowupEvidence[], domain: string): boolean {
return evidence.some((item) => item.status === "confirmed" && item.domain === domain);
return evidence.some((item) => item.status === "confirmed" && sameCollectDomain(item.domain, domain));
}
export type AnsweredProbeCoverageRow = Readonly<{
@@ -314,7 +319,7 @@ export function domainsAnsweredYes(
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);
if (domain) domains.add(canonicalCollectDomain(domain));
}
return domains;
}
@@ -367,7 +372,7 @@ export function existenceProbeAsked(
if (nearby <= 0) return false;
for (const key of asked) {
const match = key.match(SEMANTIC_YEAR_KEY);
if (!match || match[1] !== domain) continue;
if (!match || !sameCollectDomain(match[1], domain)) continue;
const askedYear = Number(match[2]);
if (Number.isInteger(askedYear) && Math.abs(askedYear - year) <= nearby) return true;
}
@@ -381,7 +386,7 @@ export function datedLedgerAnchor(
if (!domain) return null;
let best: { year: number; month: number | null; label: string } | null = null;
for (const item of evidence ?? []) {
if (!isConfirmedDated(item) || item.domain !== domain) continue;
if (!isConfirmedDated(item) || !sameCollectDomain(item.domain, domain)) continue;
const year = evidenceYear(item);
if (year === null) continue;
const raw = item.occurredFrom || item.occurredTo || "";
@@ -408,7 +413,7 @@ function nearbyLedgerHint(
const probeIndex = month && month >= 1 && month <= 12 ? year * 12 + month : null;
let best: { label: string; family: string; delta: number } | null = null;
for (const item of evidence ?? []) {
if (!isConfirmedDated(item) || item.domain === domain) continue;
if (!isConfirmedDated(item) || sameCollectDomain(item.domain, domain)) continue;
const otherYear = evidenceYear(item);
if (otherYear === null) continue;
const raw = item.occurredFrom || item.occurredTo || "";
@@ -447,7 +452,7 @@ function probeYearAlreadyCovered(
if (item.status !== "confirmed" && item.status !== "draft" && item.status !== "pending_confirmation") {
return false;
}
if (item.domain !== domain) return false;
if (!sameCollectDomain(item.domain, domain)) return false;
const itemYear = evidenceYear(item);
if (itemYear === null) return false;
return Math.abs(itemYear - year) <= nearby;
@@ -503,11 +508,11 @@ export function datedMethodCollectOpen(
}
function hasConfirmedHealth(evidence: readonly MethodFollowupEvidence[]): boolean {
return hasConfirmedDomain(evidence, "health_pressure") || hasConfirmedDomain(evidence, "health");
return hasConfirmedDomain(evidence, "health_pressure");
}
function declinedHealth(declined: ReadonlySet<string>): boolean {
return declined.has("health") || declined.has("health_pressure");
return declinedHasDomain(declined, "health_pressure");
}
function topicDomain(topic: Readonly<Record<string, unknown>>): string | null {
@@ -553,7 +558,8 @@ function declinedDomains(
if (intent && intent !== "collect_method_evidence") continue;
if (!includeSkipped && topicStatus(topic) === "skipped") continue;
const domain = topicDomain(topic);
if (domain) domains.add(domain);
const canonical = canonicalCollectDomain(domain, topicQuestionId(topic));
if (canonical) domains.add(canonical);
}
return domains;
}
@@ -766,7 +772,7 @@ export function remainingReverseVerifyProbes(
&& probe.role === "distinguish";
if ((probe.source === "known_event_quality" && !anchoredQuality) || probe.role === "clarify" || probe.phase === "event_clarification") continue;
if (probe.role === "collect" || probe.phase === "evidence_collection") continue;
if (declined.has(probe.domain)) continue;
if (declinedHasDomain(declined, probe.domain)) continue;
if (!anchoredQuality && probeYearAlreadyCovered(evidence, probe.domain, probe.year)) continue;
if (probeBelowAdultFloor(probe, birthDate)) continue;
if (reverseVerifyProbeAsked(probe, askedKeys)) continue;
@@ -809,7 +815,7 @@ function datedCollectionProbe(
probes: readonly DiscriminatingEventProbe[] | undefined,
domain: string,
): DiscriminatingEventProbe | null {
const rows = (probes ?? []).filter((item) => item.domain === domain && Number(item.year) > 0);
const rows = (probes ?? []).filter((item) => sameCollectDomain(item.domain, domain) && Number(item.year) > 0);
return rows.sort((left, right) => (right.information_gain ?? 0) - (left.information_gain ?? 0))[0] ?? null;
}
@@ -825,7 +831,7 @@ function collectionYearFields(probe: DiscriminatingEventProbe | null): Pick<
};
}
function remainingConflictProbes(
export function remainingConflictProbes(
probes: readonly DiscriminatingEventProbe[] | undefined,
evidence: readonly MethodFollowupEvidence[],
declined: ReadonlySet<string>,
@@ -837,7 +843,7 @@ function remainingConflictProbes(
if (!CONFLICT_PROBE_SOURCES.has(probe.source)) continue;
if (probe.source === "known_event_quality" || probe.role === "clarify") continue;
if (!isValidDistinguishProbe({ ...probe, role: "distinguish" })) continue;
if (declined.has(probe.domain)) continue;
if (declinedHasDomain(declined, probe.domain)) continue;
if (probeYearAlreadyCovered(evidence, probe.domain, probe.year)) continue;
if (probeBelowAdultFloor(probe, birthDate)) continue;
const semantic = probe.semantic_key ?? `${probe.domain}.${probe.year}`;
@@ -1179,7 +1185,7 @@ function rankRenderableDiscriminators(input: {
birthDate?: string | null;
}): { locked: RankedDiscriminator[]; personality: RankedDiscriminator[]; yearless: RankedDiscriminator[]; dropped: DroppedProbe[] } {
const top = input.topCandidateTimes ?? [];
const provided = new Set(input.providedDomains ?? []);
const provided = new Set((input.providedDomains ?? []).map((item) => canonicalCollectDomain(item)));
const mentioned = input.mentionedKeys ?? new Set();
const rows: RankedDiscriminator[] = [];
const dropped: DroppedProbe[] = [];
@@ -1213,7 +1219,7 @@ function rankRenderableDiscriminators(input: {
continue;
}
}
if (!isStructuredDiscriminator(probe) && probe.domain && provided.has(probe.domain)) {
if (!isStructuredDiscriminator(probe) && probe.domain && provided.has(canonicalCollectDomain(probe.domain))) {
const year = probe.year ?? 0;
if (year <= 0) continue;
if (input.evidence && probeYearAlreadyCovered(input.evidence, probe.domain, year)) continue;
@@ -1320,15 +1326,9 @@ function datedCollectDomainBlocked(
declined: ReadonlySet<string>,
answeredYes: ReadonlySet<string> = new Set(),
): boolean {
if (domain === "health_pressure") {
return declinedHealth(declined)
|| hasConfirmedHealth(evidence)
|| answeredYes.has("health_pressure")
|| answeredYes.has("health");
}
return declined.has(domain)
return declinedHasDomain(declined, domain)
|| hasConfirmedDomain(evidence, domain)
|| answeredYes.has(domain);
|| [...answeredYes].some((item) => sameCollectDomain(item, domain));
}
export function datedCollectAlreadyAsked(
@@ -1535,16 +1535,11 @@ export function remainingEvidenceCollectStillOpen(
}
function holdoutOccupiedDomains(evidence: readonly MethodFollowupEvidence[]): Set<string> {
const occupied = new Set(
return new Set(
evidence
.filter((item) => isConfirmedDated(item) && evidenceYear(item) != null)
.map((item) => item.domain),
.map((item) => canonicalCollectDomain(item.domain)),
);
if (hasConfirmedHealth(evidence) || occupied.has("health") || occupied.has("health_pressure")) {
occupied.add("health");
occupied.add("health_pressure");
}
return occupied;
}
export function exhaustionSpokenCollectFollowup(input: {
@@ -1924,12 +1919,7 @@ function holdoutAskFields(
}
function declinedForHoldout(declined: ReadonlySet<string>): Set<string> {
const next = new Set(declined);
if (declinedHealth(declined)) {
next.add("health");
next.add("health_pressure");
}
return next;
return new Set([...declined].map((item) => canonicalCollectDomain(item)));
}
export function holdoutFollowupFor(
@@ -1943,12 +1933,14 @@ export function holdoutFollowupFor(
if (!meetsAcceptanceEventQuality(input.evidence)) return null;
const occupied = holdoutOccupiedDomains(input.evidence);
const blocked = declinedForHoldout(declined);
const prompt = (input.oosBlindPrompts ?? []).find((item) => (
Boolean(item.domain) && !occupied.has(item.domain) && !blocked.has(item.domain)
)) ?? null;
const reserved = (input.holdoutEvents ?? []).find((item) => (
item.year != null && !occupied.has(item.domain) && !blocked.has(item.domain)
)) ?? null;
const prompt = (input.oosBlindPrompts ?? []).find((item) => {
const domain = canonicalCollectDomain(item.domain);
return Boolean(domain) && !occupied.has(domain) && !blocked.has(domain);
}) ?? null;
const reserved = (input.holdoutEvents ?? []).find((item) => {
const domain = canonicalCollectDomain(item.domain);
return item.year != null && Boolean(domain) && !occupied.has(domain) && !blocked.has(domain);
}) ?? null;
return holdoutAskFields(prompt, reserved);
}