fix(rectification): split D4/D5 stages and recast after adopt
Keep family on D12+D7 instead of mixing it into D4, score D5 on education, and show a natal recast plus technique audit after adopt without unique-minute claims. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -153,3 +153,53 @@ test("reads dasha ledger and change minutes without scores or D9 labels", () =>
|
||||
assert.doesNotMatch(result?.eventDashaLedger[0]?.user_meaning ?? "", /points/);
|
||||
});
|
||||
|
||||
function twelveHouses(sign: string, occupant?: string) {
|
||||
return Array.from({ length: 12 }, (_, index) => ({
|
||||
house: index + 1,
|
||||
sign,
|
||||
occupants: index === 0 && occupant ? [occupant] : [],
|
||||
}));
|
||||
}
|
||||
|
||||
test("adopted minute recasts the house table and keeps technique audit collapsed-safe", () => {
|
||||
const result = parseRectificationCandidateResult({
|
||||
...camelCaseSnapshot,
|
||||
representativeTime: "05:07",
|
||||
selectedTime: "05:08",
|
||||
decisionReceipt: {
|
||||
house_table: { time: "05:07", lagna: "金牛座", houses: twelveHouses("金牛座", "太阳") },
|
||||
house_tables_by_time: {
|
||||
"05:07": { time: "05:07", lagna: "金牛座", houses: twelveHouses("金牛座", "太阳") },
|
||||
"05:08": { time: "05:08", lagna: "双子座", houses: twelveHouses("双子座", "月亮") },
|
||||
},
|
||||
natal_recast: {
|
||||
time: "05:07",
|
||||
lagna: "金牛座",
|
||||
user_meaning: "本命宫位已按 05:07 重算(上升 金牛座)。下面是本轮实际执行的技法,不能当作唯一分钟确认。",
|
||||
unique_minute_claim: false,
|
||||
confirmation_allowed: false,
|
||||
},
|
||||
technique_audit_table: [
|
||||
{ technique: "D1 本命盘", status: "executed", note: "本轮已按该分钟重算本命宫位。" },
|
||||
{ technique: "KP 宫头", status: "blocked", note: "KP 宫头本轮未计算。" },
|
||||
{ technique: "唯一分钟确认", status: "blocked", note: "采用不等于确认唯一分钟。" },
|
||||
],
|
||||
precision_stage: {
|
||||
current: "d4_refine",
|
||||
can_stop: true,
|
||||
user_meaning: "居所盘仍会换升。",
|
||||
unique_minute_claim: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(result?.houseTable?.time, "05:08");
|
||||
assert.equal(result?.houseTable?.lagna, "双子座");
|
||||
assert.equal(result?.houseTablesByTime["05:08"]?.lagna, "双子座");
|
||||
assert.equal(result?.natalRecast?.time, "05:08");
|
||||
assert.equal(result?.natalRecast?.lagna, "双子座");
|
||||
assert.equal(result?.natalRecast?.unique_minute_claim, false);
|
||||
assert.equal(result?.techniqueAudit.length, 3);
|
||||
assert.equal(result?.techniqueAudit[2]?.status, "blocked");
|
||||
assert.equal(result?.precisionStage?.current, "d4_refine");
|
||||
});
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ test("holdout not_ready forbids unique-minute copy and still blocks confirm", as
|
||||
assert.match(agentSource, /session_outcome=adopt_representative/);
|
||||
assert.doesNotMatch(agentSource, /±2 分钟/);
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 13);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.4");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.5");
|
||||
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
|
||||
@@ -255,6 +255,9 @@ test("D9 differ becomes an internal ask theme without sign labels", () => {
|
||||
assert.deepEqual(observations, [
|
||||
{ layer: "d9", candidates_differ: true, ask_theme: "relationship_style" },
|
||||
{ layer: "d10", candidates_differ: false, ask_theme: null },
|
||||
{ layer: "d4", candidates_differ: false, ask_theme: null },
|
||||
{ layer: "d5", candidates_differ: false, ask_theme: null },
|
||||
{ layer: "d7", candidates_differ: false, ask_theme: null },
|
||||
{ layer: "d12", candidates_differ: false, ask_theme: null },
|
||||
]);
|
||||
assert.doesNotMatch(JSON.stringify({ scan, observations }), FORBIDDEN_LABELS);
|
||||
@@ -501,9 +504,9 @@ test("rescore failure does not fail the evidence write", async () => {
|
||||
assert.ok(result.rescore.error_code);
|
||||
});
|
||||
|
||||
test("public tool surface stays at 13 and new cases bind 10.0.4", () => {
|
||||
test("public tool surface stays at 13 and new cases bind 10.0.5", () => {
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 13);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.4");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.5");
|
||||
const deprecated = resolveExactSkillPackage(
|
||||
"jyotish-birth-time-rectification",
|
||||
"10.0.2",
|
||||
@@ -569,6 +572,41 @@ test("precision stage lagna_frame asks another dated event instead of rotating d
|
||||
assert.doesNotMatch(JSON.stringify(plan), FORBIDDEN_LABELS);
|
||||
});
|
||||
|
||||
test("precision stage d4 asks home change not family, and d5 asks education", () => {
|
||||
const d4 = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
|
||||
],
|
||||
precisionStage: "d4_refine",
|
||||
});
|
||||
assert.equal(d4.next_followup?.source, "precision_stage");
|
||||
assert.equal(d4.next_followup?.method_id, "d4_home");
|
||||
assert.equal(d4.next_followup?.domain, "relocation");
|
||||
assert.equal(d4.next_followup?.ask_theme, "home_change");
|
||||
assert.doesNotMatch(d4.next_followup?.user_prompt_hint ?? "", /家人/);
|
||||
const legacyTheme = buildMethodFollowupPlan({
|
||||
evidence: d4.next_followup ? [
|
||||
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
|
||||
] : [],
|
||||
precisionStage: "theme_refine",
|
||||
});
|
||||
assert.equal(legacyTheme.next_followup?.ask_theme, "home_change");
|
||||
const d5 = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
|
||||
],
|
||||
precisionStage: "d5_refine",
|
||||
});
|
||||
assert.equal(d5.next_followup?.method_id, "d5_education");
|
||||
assert.equal(d5.next_followup?.domain, "education");
|
||||
assert.doesNotMatch(JSON.stringify(d4), FORBIDDEN_LABELS);
|
||||
});
|
||||
|
||||
test("accepted representative time hands off to consultation", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [{
|
||||
|
||||
@@ -202,10 +202,20 @@ test("read-case evidence context keeps day labels and confirm does not rewrite d
|
||||
assert.equal("p_occurred_from" in confirmCall.args, false);
|
||||
});
|
||||
|
||||
test("new-case skill identity is 10.0.4 and the prompt prefers batch ingest", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.4");
|
||||
assert.match(skill, /^version: 10\.0\.4$/m);
|
||||
test("new-case skill identity is 10.0.5 and the prompt prefers batch ingest", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.5");
|
||||
assert.match(skill, /^version: 10\.0\.5$/m);
|
||||
assert.match(skill, /不要对同一句用户消息里的多件事件逐条 propose\+confirm/);
|
||||
assert.match(agentSource, /当前轮新事件一律走 rectification-record-evidence-batch/);
|
||||
assert.doesNotMatch(agentSource, /分别调用 rectification-propose-evidence 和 rectification-confirm-evidence/);
|
||||
});
|
||||
|
||||
test("receipt allowlist includes D5 and D7 public methods", () => {
|
||||
const d5d7Migration = readFileSync(
|
||||
new URL("../supabase/migrations/20260820020000_rectification_d5_d7_precision.sql", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(d5d7Migration, /'d5-panchamsha'/);
|
||||
assert.match(d5d7Migration, /'d7-saptamsha'/);
|
||||
assert.match(d5d7Migration, /'d12-dwadashamsha'/);
|
||||
});
|
||||
|
||||
@@ -76,11 +76,11 @@ test("system prompt carries only high-priority boundaries, never the method copy
|
||||
test("agent pins the dedicated rectification skill and its fixed version", () => {
|
||||
assert.equal(RECTIFICATION_V9_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(basename(RECTIFICATION_V9_SKILL_PATH), RECTIFICATION_V9_SKILL_NAME);
|
||||
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.4"));
|
||||
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.5"));
|
||||
assert.notEqual(RECTIFICATION_V9_SKILL_PATH, RECTIFICATION_V9_PACKAGE_PATH);
|
||||
assert.equal(realpathSync(RECTIFICATION_V9_SKILL_PATH), RECTIFICATION_V9_PACKAGE_PATH);
|
||||
assert.equal(RECTIFICATION_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.4");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.5");
|
||||
});
|
||||
|
||||
test("step budgets are bounded per action with a hard ceiling", () => {
|
||||
|
||||
@@ -92,9 +92,9 @@ test("terminal transitions are one-way and evidence writes stop at terminal", ()
|
||||
|
||||
test("the active rectification skill pins the v10 identity and lives in the right directory", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.4");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.5");
|
||||
assert.match(skill, /^---\nname: jyotish-birth-time-rectification/m);
|
||||
assert.match(skill, /^version: 10\.0\.4$/m);
|
||||
assert.match(skill, /^version: 10\.0\.5$/m);
|
||||
for (const reference of references) {
|
||||
const content = readFileSync(`${skillDirectory}/references/${reference}`, "utf8");
|
||||
assert.ok(content.length > 0, `${reference} must be non-empty`);
|
||||
|
||||
@@ -209,7 +209,7 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
|
||||
session_id: SESSION_ID,
|
||||
status: "draft",
|
||||
should_start_opening: true,
|
||||
skill_version: "10.0.4",
|
||||
skill_version: "10.0.5",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -247,11 +247,11 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
|
||||
});
|
||||
assert.equal(response.disposition, "created");
|
||||
assert.equal(response.shouldStartOpening, true);
|
||||
assert.equal(response.skillVersion, "10.0.4");
|
||||
assert.equal(response.skillVersion, "10.0.5");
|
||||
const openCall = accounting.calls.find((call) => call.fn === "open_agentic_rectification_case_v2");
|
||||
assert.ok(openCall);
|
||||
assert.equal(openCall.args.p_skill_name, "jyotish-birth-time-rectification");
|
||||
assert.equal(openCall.args.p_skill_version, "10.0.4");
|
||||
assert.equal(openCall.args.p_skill_version, "10.0.5");
|
||||
assert.equal(openCall.args.p_user_id, "user-1");
|
||||
// The server derives the baseline; the request never carries it from the browser.
|
||||
assert.equal("birth_date" in openCall.args, false);
|
||||
|
||||
@@ -85,8 +85,8 @@ test("checked-in registry verifies hashed product packages and leaves consult on
|
||||
[
|
||||
{
|
||||
name: "jyotish-birth-time-rectification",
|
||||
version: "10.0.4",
|
||||
sha256: "1a032b1af54d8ca593ad37da8dee116668b71524d37c7b77ec0e3e44f8c7624c",
|
||||
version: "10.0.5",
|
||||
sha256: "47cd353b5f7a08695007ce52d77560584300a1097ed97248f3a81372eeaca9c6",
|
||||
},
|
||||
{
|
||||
name: "jyotish-personal-report",
|
||||
|
||||
Reference in New Issue
Block a user