fix(rectification): downgrade yearless personality probes to tie-breakers (BUG-629)
Ask dated dasha probes first; D9/D10 and nakshatra wait until that pool is empty, score at half weight, and never eliminate. Skill 10.0.21. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
type TransitionSignLookup,
|
||||
} from "./sign-from-transitions.ts";
|
||||
import {
|
||||
PROBE_WEIGHT,
|
||||
SCORE_DELTA,
|
||||
STRONG_CONFLICT_ELIMINATION_COUNT,
|
||||
type AnswerClass,
|
||||
@@ -16,11 +17,17 @@ import {
|
||||
export type ProbeApplyResult = Readonly<{
|
||||
scores: Readonly<Record<string, number>>;
|
||||
eliminated_ids: readonly string[];
|
||||
kind: "informative" | "low_information";
|
||||
kind: "informative" | "low_information" | "tie_break";
|
||||
deltas: Readonly<Record<string, number>>;
|
||||
strong_conflict_counts: Readonly<Record<string, number>>;
|
||||
}>;
|
||||
|
||||
export function isYearlessPersonalityProbe(
|
||||
probe: Pick<ConflictProbe, "choice_kind" | "source">,
|
||||
): boolean {
|
||||
return probe.choice_kind === "varga_style" || probe.source === "nakshatra_boundary";
|
||||
}
|
||||
|
||||
export type ProbeDirectionContext = Readonly<{
|
||||
probe?: ConflictProbe;
|
||||
transitions?: readonly TransitionSignLookup[];
|
||||
@@ -181,15 +188,19 @@ export function applyProbeOutcome(
|
||||
};
|
||||
}
|
||||
const context: ProbeDirectionContext = { probe, transitions: options.transitions };
|
||||
const yearless = isYearlessPersonalityProbe(probe);
|
||||
const weight = yearless ? PROBE_WEIGHT.yearless : PROBE_WEIGHT.dated;
|
||||
for (const [id, score] of Object.entries(scores)) {
|
||||
const direction = directionFor(id, outcome, probe.choice_kind, context);
|
||||
if (direction === "conflict") conflictCounts[id] = (conflictCounts[id] ?? 0) + 1;
|
||||
if (direction === "conflict" && !yearless) {
|
||||
conflictCounts[id] = (conflictCounts[id] ?? 0) + 1;
|
||||
}
|
||||
if (eliminated.has(id)) {
|
||||
next[id] = score;
|
||||
deltas[id] = 0;
|
||||
continue;
|
||||
}
|
||||
const delta = SCORE_DELTA[direction];
|
||||
const delta = SCORE_DELTA[direction] * weight;
|
||||
deltas[id] = delta;
|
||||
next[id] = score + delta;
|
||||
}
|
||||
@@ -210,7 +221,7 @@ export function applyProbeOutcome(
|
||||
return {
|
||||
scores: next,
|
||||
eliminated_ids: [...eliminated],
|
||||
kind: changed ? "informative" : "low_information",
|
||||
kind: yearless ? "tie_break" : (changed ? "informative" : "low_information"),
|
||||
deltas,
|
||||
strong_conflict_counts: conflictCounts,
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ export type InferenceTransitionSnapshot = Readonly<{
|
||||
entropyBefore?: number | null;
|
||||
entropyAfter?: number | null;
|
||||
eliminatedCandidateIds?: readonly string[];
|
||||
roundKind?: "informative" | "low_information" | null;
|
||||
roundKind?: "informative" | "low_information" | "tie_break" | null;
|
||||
}>;
|
||||
|
||||
const CLOCK = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
||||
@@ -34,7 +34,7 @@ const EVENT_USAGES = new Set(["training", "holdout", "unused"]);
|
||||
const ANSWER_CLASSES = new Set(["yes", "weak_yes", "no", "unsure"]);
|
||||
const PROBE_CHOICE_KINDS = new Set(["existence", "varga_style", "event_quality"]);
|
||||
const ANSWER_SOURCES = new Set(["choice", "evidence", "declined"]);
|
||||
const ROUND_KINDS = new Set(["informative", "low_information"]);
|
||||
const ROUND_KINDS = new Set(["informative", "low_information", "tie_break"]);
|
||||
const SCORE_DIRECTIONS = new Set(["support", "weak_support", "neutral", "weak_conflict", "conflict"]);
|
||||
|
||||
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
||||
|
||||
@@ -47,6 +47,12 @@ export const SCORE_DELTA: Readonly<Record<ScoreDirection, number>> = {
|
||||
conflict: -2,
|
||||
};
|
||||
|
||||
/** Dated distinguish probes keep full SCORE_DELTA. Yearless personality is tie-break only. */
|
||||
export const PROBE_WEIGHT = {
|
||||
dated: 1,
|
||||
yearless: 0.5,
|
||||
} as const;
|
||||
|
||||
export type InferenceCandidate = Readonly<{
|
||||
id: string;
|
||||
time: string;
|
||||
@@ -116,7 +122,7 @@ export type RoundTrace = Readonly<{
|
||||
entropy_after: number;
|
||||
eliminated_ids: readonly string[];
|
||||
winner_id: string | null;
|
||||
kind: "informative" | "low_information";
|
||||
kind: "informative" | "low_information" | "tie_break";
|
||||
}>;
|
||||
|
||||
export type InferenceState = Readonly<{
|
||||
|
||||
@@ -89,4 +89,4 @@ export function evidenceWritesAllowed(
|
||||
export const MAX_RESUMABLE_CASES_PER_USER = 1;
|
||||
|
||||
export const RECTIFICATION_SKILL_NAME = "jyotish-birth-time-rectification";
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.20";
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.21";
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type HoldoutValidationStatus,
|
||||
type RectificationDecision,
|
||||
} from "../core/rectification-decision.ts";
|
||||
import { evaluateCandidateSeparation } from "../core/candidate-separation.ts";
|
||||
import type { ConflictProbe, InferenceState } from "../core/types.ts";
|
||||
import {
|
||||
askedDiscriminatorKeys,
|
||||
@@ -277,6 +278,11 @@ export function contrastPacketFromDossier(dossier: DecisionDossier): CandidateCo
|
||||
return contrastPacketFromLatestResult(dossier.latestResult, dossier.evidence);
|
||||
}
|
||||
|
||||
function dossierCandidatesSeparated(dossier: DecisionDossier): boolean {
|
||||
const separation = evaluateCandidateSeparation(candidateScoresFromDossier(dossier.latestResult));
|
||||
return separation.status === "separated" || separation.status === "sole_candidate";
|
||||
}
|
||||
|
||||
export function rectificationFollowupCatalog(
|
||||
latest: DecisionDossier["latestResult"] | undefined,
|
||||
evidence: DecisionDossier["evidence"] = [],
|
||||
@@ -474,7 +480,7 @@ function discriminatorProbeIfFollowupCanAsk(input: {
|
||||
...(input.contrastPacket ? { contrastPacket: input.contrastPacket } : {}),
|
||||
...(input.askedKeys ? { askedProbeKeys: input.askedKeys } : {}),
|
||||
...(input.birthDate ? { birthDate: input.birthDate } : {}),
|
||||
candidatesSeparated: false,
|
||||
candidatesSeparated: dossierCandidatesSeparated(input.dossier),
|
||||
});
|
||||
const dropped = mergeDroppedProbes(input.inspected.dropped, plan.dropped_probes);
|
||||
if (!followupAsksRenderableDiscriminator(plan.next_followup)) {
|
||||
@@ -544,7 +550,7 @@ function nakshatraProbeIfFollowupCanAsk(input: {
|
||||
oosBlindPrompts: catalog.oosBlindPrompts,
|
||||
holdoutEvents: catalog.holdoutEvents,
|
||||
...(input.birthDate ? { birthDate: input.birthDate } : {}),
|
||||
candidatesSeparated: false,
|
||||
candidatesSeparated: dossierCandidatesSeparated(input.dossier),
|
||||
});
|
||||
if (!followupAsksRenderableDiscriminator(plan.next_followup)) {
|
||||
return { probe: null, dropped: mergeDroppedProbes(inspected.dropped, plan.dropped_probes) };
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
* Scoring A/B/C/D reverse-inference needs the engine year/month.
|
||||
* Yearless varga splits do not borrow a ledger year. Pick the next
|
||||
* dated discriminator, or collect a dated event in that domain.
|
||||
* Remaining chart discriminators (D9/D10 style and dated dasha
|
||||
* probes) stay in the pool; the highest information-gain renderable
|
||||
* probe is asked next, with no preferred domain.
|
||||
* Remaining dated dasha distinguish probes are asked first. Yearless
|
||||
* D9/D10 style and nakshatra_boundary wait until that pool is empty and
|
||||
* at least two active candidates are still unseparated (BUG-629).
|
||||
* If holdout is already reserved but training is still short,
|
||||
* keep collecting a dated event instead of discriminating.
|
||||
* Once blocking methods are covered, move into candidate discrimination.
|
||||
@@ -1082,11 +1082,35 @@ function discriminatorChoiceKind(row: RankedDiscriminator): string {
|
||||
}
|
||||
|
||||
function discriminatorLocksScoringPeriod(row: RankedDiscriminator): boolean {
|
||||
if (discriminatorChoiceKind(row) === "varga_style") return true;
|
||||
if (discriminatorChoiceKind(row) === "varga_style") return false;
|
||||
const year = row.eventProbe?.year ?? row.contrastProbe?.year ?? 0;
|
||||
return year > 0;
|
||||
}
|
||||
|
||||
function isYearlessPersonalityRow(row: RankedDiscriminator): boolean {
|
||||
return discriminatorChoiceKind(row) === "varga_style";
|
||||
}
|
||||
|
||||
function yearlessPersonalityCanAsk(input: {
|
||||
datedCount: number;
|
||||
candidatesSeparated: boolean;
|
||||
topCandidateTimes?: readonly string[];
|
||||
}): boolean {
|
||||
if (input.datedCount > 0 || input.candidatesSeparated) return false;
|
||||
const times = input.topCandidateTimes;
|
||||
if (times !== undefined && times.length < 2) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function emptyRankedCatalog(): {
|
||||
locked: RankedDiscriminator[];
|
||||
personality: RankedDiscriminator[];
|
||||
yearless: RankedDiscriminator[];
|
||||
dropped: DroppedProbe[];
|
||||
} {
|
||||
return { locked: [], personality: [], yearless: [], dropped: [] };
|
||||
}
|
||||
|
||||
function rankRenderableDiscriminators(input: {
|
||||
eventProbes: readonly DiscriminatingEventProbe[];
|
||||
contrastProbes: readonly CandidateDiscriminatorProbe[];
|
||||
@@ -1096,7 +1120,7 @@ function rankRenderableDiscriminators(input: {
|
||||
providedDomains?: readonly string[];
|
||||
evidence?: readonly MethodFollowupEvidence[];
|
||||
birthDate?: string | null;
|
||||
}): { locked: RankedDiscriminator[]; yearless: RankedDiscriminator[]; dropped: DroppedProbe[] } {
|
||||
}): { locked: RankedDiscriminator[]; personality: RankedDiscriminator[]; yearless: RankedDiscriminator[]; dropped: DroppedProbe[] } {
|
||||
const top = input.topCandidateTimes ?? [];
|
||||
const provided = new Set(input.providedDomains ?? []);
|
||||
const mentioned = input.mentionedKeys ?? new Set();
|
||||
@@ -1146,7 +1170,8 @@ function rankRenderableDiscriminators(input: {
|
||||
const sorted = rows.sort((left, right) => right.score - left.score || (right.eventProbe?.information_gain ?? right.contrastProbe?.informationGain ?? 0) - (left.eventProbe?.information_gain ?? left.contrastProbe?.informationGain ?? 0));
|
||||
return {
|
||||
locked: sorted.filter((row) => discriminatorLocksScoringPeriod(row)),
|
||||
yearless: sorted.filter((row) => !discriminatorLocksScoringPeriod(row) && discriminatorChoiceKind(row) !== "varga_style"),
|
||||
personality: sorted.filter((row) => isYearlessPersonalityRow(row)),
|
||||
yearless: sorted.filter((row) => !discriminatorLocksScoringPeriod(row) && !isYearlessPersonalityRow(row)),
|
||||
dropped,
|
||||
};
|
||||
}
|
||||
@@ -1992,7 +2017,7 @@ export function buildMethodFollowupPlan(input: {
|
||||
...(input.askedProbeKeys ?? []),
|
||||
]);
|
||||
const rankedCatalog = input.caseStage === "block_scan"
|
||||
? { locked: [] as RankedDiscriminator[], yearless: [] as RankedDiscriminator[], dropped: [] as DroppedProbe[] }
|
||||
? emptyRankedCatalog()
|
||||
: dashaCovered && meetsAcceptanceEventQuality(input.evidence)
|
||||
? rankRenderableDiscriminators({
|
||||
eventProbes: remainingConflictProbes(input.eventProbes, input.evidence, declined, askedKeys, input.birthDate),
|
||||
@@ -2004,8 +2029,9 @@ export function buildMethodFollowupPlan(input: {
|
||||
evidence: input.evidence,
|
||||
birthDate: input.birthDate,
|
||||
})
|
||||
: { locked: [] as RankedDiscriminator[], yearless: [] as RankedDiscriminator[], dropped: [] as DroppedProbe[] };
|
||||
: emptyRankedCatalog();
|
||||
const rankedDiscriminators = rankedCatalog.locked;
|
||||
const personalityDiscriminators = rankedCatalog.personality;
|
||||
const yearlessDiscriminators = rankedCatalog.yearless;
|
||||
const bestDiscriminator = rankedDiscriminators[0] ?? null;
|
||||
const catalogWinnerKey = rankedDiscriminatorKey(bestDiscriminator);
|
||||
@@ -2298,6 +2324,30 @@ export function buildMethodFollowupPlan(input: {
|
||||
}
|
||||
}
|
||||
}
|
||||
const personalityAllowed = yearlessPersonalityCanAsk({
|
||||
datedCount: rankedDiscriminators.length,
|
||||
candidatesSeparated,
|
||||
topCandidateTimes: input.topCandidateTimes,
|
||||
});
|
||||
if (dashaCovered && personalityAllowed && !next) {
|
||||
const allowLowGainDiscriminator = !coverageComplete || !candidatesSeparated;
|
||||
for (const ranked of personalityDiscriminators) {
|
||||
if (!allowLowGainDiscriminator && ranked.score < 0.08) continue;
|
||||
const candidate = followupFromRanked(ranked);
|
||||
if (candidate.choice_frame) {
|
||||
next = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const ranked of personalityDiscriminators) {
|
||||
extraDropped.push(droppedFromProbe(
|
||||
ranked.eventProbe?.semantic_key ?? ranked.contrastProbe?.semanticKey ?? "varga_style",
|
||||
ranked.eventProbe?.information_gain ?? ranked.contrastProbe?.informationGain ?? ranked.score,
|
||||
"yearless_deferred",
|
||||
));
|
||||
}
|
||||
}
|
||||
if (!next && !dashaCovered) {
|
||||
next = makeFollowup({
|
||||
method_id: "dasha_events",
|
||||
@@ -2341,6 +2391,7 @@ export function buildMethodFollowupPlan(input: {
|
||||
}
|
||||
if (
|
||||
!next
|
||||
&& personalityAllowed
|
||||
&& input.holdoutValidation !== "not_started"
|
||||
&& !datedMethodCollectOpen(methods)
|
||||
&& input.nakshatraProbe
|
||||
@@ -2373,6 +2424,12 @@ export function buildMethodFollowupPlan(input: {
|
||||
reason: "not_renderable",
|
||||
});
|
||||
}
|
||||
} else if (input.nakshatraProbe && !personalityAllowed) {
|
||||
extraDropped.push(droppedFromProbe(
|
||||
input.nakshatraProbe.semantic_key,
|
||||
input.nakshatraProbe.information_gain,
|
||||
"yearless_deferred",
|
||||
));
|
||||
}
|
||||
const sameDomainYearlessCard = (domain: string): MethodFollowup | null => {
|
||||
const ranked = yearlessDiscriminators.find((row) => (
|
||||
|
||||
@@ -36,6 +36,7 @@ export type ProbeRejectReason =
|
||||
| "yearless_ungrounded_contrast"
|
||||
| "no_split_among_active"
|
||||
| "unanchored_varga_style"
|
||||
| "yearless_deferred"
|
||||
| "frameless_distinguish"
|
||||
| "same_year_asked"
|
||||
| "below_adult_floor";
|
||||
|
||||
@@ -129,21 +129,38 @@ export function buildSkillVerificationPacket(input: SkillVerificationReportInput
|
||||
const d10Table = d10TableSigns.length > 0
|
||||
? ["| D10上升 | 事业特质 | 职业类型 | 工作风格 |", "|---|---|---|---|", ...d10TableSigns.map((sign) => typeRow(sign, "d10"))]
|
||||
: ["当前窗还没有可对照的 D10 上升名,类型表待候选换升后填写。"];
|
||||
const PERSONALITY_AUDIT_NOTE = "性格自评,只作排序参考,不参与淘汰";
|
||||
|
||||
function isPersonalityAuditTechnique(name: string): boolean {
|
||||
return /D9\s*\/\s*D10|类型对照|月宿/.test(name);
|
||||
}
|
||||
|
||||
const audit = (input.techniqueAuditTable ?? []).slice(0, 16);
|
||||
const mappedAudit = audit.map((row) => {
|
||||
const technique = row.technique ?? "技法";
|
||||
if (isPersonalityAuditTechnique(technique)) {
|
||||
return `| ${technique} | reference | ${PERSONALITY_AUDIT_NOTE} |`;
|
||||
}
|
||||
const executed = row.status === "executed" && Boolean(row.calculation_result_id);
|
||||
const status = executed ? "executed" : (row.status === "executed" ? "input_covered" : (row.status ?? "blocked"));
|
||||
const note = executed
|
||||
? (row.note ?? "")
|
||||
: (row.note || "该方法所需资料已覆盖,没有可追溯的 calculationResultId,不能写成已执行。");
|
||||
return `| ${technique} | ${status} | ${note} |`;
|
||||
});
|
||||
const auditText = mappedAudit.join("\n");
|
||||
const withPersonalityRows = [
|
||||
mappedAudit,
|
||||
auditText.includes("D9 / D10 类型对照") ? [] : [`| D9 / D10 类型对照 | reference | ${PERSONALITY_AUDIT_NOTE} |`],
|
||||
auditText.includes("月宿边界") ? [] : [`| 月宿边界 | reference | ${PERSONALITY_AUDIT_NOTE} |`],
|
||||
].flat();
|
||||
const auditRows = audit.length > 0
|
||||
? audit.map((row) => {
|
||||
const executed = row.status === "executed" && Boolean(row.calculation_result_id);
|
||||
const status = executed ? "executed" : (row.status === "executed" ? "input_covered" : (row.status ?? "blocked"));
|
||||
const note = executed
|
||||
? (row.note ?? "")
|
||||
: (row.note || "该方法所需资料已覆盖,没有可追溯的 calculationResultId,不能写成已执行。");
|
||||
return `| ${row.technique ?? "技法"} | ${status} | ${note} |`;
|
||||
})
|
||||
? withPersonalityRows
|
||||
: [
|
||||
"| Vimshottari + 受控行运 | input_covered | 没有 calculationResultId 时不能写成已执行 |",
|
||||
"| D9/D10 分盘 | input_covered | 类型表是校时方法;差异要进入区分探针 |",
|
||||
`| D9 / D10 类型对照 | reference | ${PERSONALITY_AUDIT_NOTE} |`,
|
||||
"| 宫位 / 六亲六步 | input_covered | 后三步方法论层 |",
|
||||
"| Nakshatra Pada | observation_only | 不确认唯一分钟 |",
|
||||
`| 月宿边界 | reference | ${PERSONALITY_AUDIT_NOTE} |`,
|
||||
"| KP / D60 / Gulika | observation_only | 不进提出门或确认门计分 |",
|
||||
"| 占问 | observation_only | AI 暂不支持独立占问;有问起时间则观察 |",
|
||||
];
|
||||
|
||||
@@ -1679,7 +1679,9 @@ function parseTransitionSnapshot(value: unknown): InferenceTransitionSnapshot |
|
||||
eliminatedCandidateIds: Array.isArray(row.eliminated_candidate_ids)
|
||||
? row.eliminated_candidate_ids.filter((item): item is string => typeof item === "string")
|
||||
: [],
|
||||
roundKind: row.round_kind === "informative" || row.round_kind === "low_information"
|
||||
roundKind: row.round_kind === "informative"
|
||||
|| row.round_kind === "low_information"
|
||||
|| row.round_kind === "tie_break"
|
||||
? row.round_kind
|
||||
: null,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user