feat(rectification): 少问几道就出卡,交付卡以范围为主(D1–D4)

- D1 引导窗口题每个校正最多 2 道(前端 GUIDED_WINDOW_CASE_LIMIT;引擎
  GUIDED_COLLECT_LIMIT 未动:event_probes.py 属冻结评分身份,改它需重新冻结)
- D2 七条定向线与跳过线重问问完即出卡,没问到的引导窗口不再挡卡,出卡后也不再挂窗口题
- D3 卡头加副标题「最可能 HH:MM」
- D4 前两列相差 ≥5 个百分点才显示相对可能性,否则一句「这几个时刻目前区分不开……」
- 离线回放 scripts/research/fewer_probes_card_replay.py:真值不降、宽度中位 ±1 分钟、提问 11.4→7.8
- Skill 10.0.29 → 10.0.30;DESIGN / VOICE / CHANGELOG / PROGRESS / 真机清单

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-26 14:27:12 +08:00
co-authored by Claude Opus 5.5
parent 7ddce2c96a
commit 4e6e8d87ef
48 changed files with 7885 additions and 54 deletions
@@ -465,7 +465,8 @@ test("skill version is 10.0.26 after the targeted-collect-cards bump", () => {
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
});
test("revision 5 with uncovered relatives asks the dated family collect, not a yearless D12 card", () => {
@@ -382,7 +382,8 @@ test("holdout not_ready forbids unique-minute copy and still blocks confirm", as
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
const accounting = fakeAccounting({
...receiptHandlers,
@@ -259,14 +259,16 @@ test("skill 10.0.26 forbids computing varga signs from transition times", () =>
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
// 原值: "10.0.25"
// 原值: "10.0.26"
// 新值: "10.0.27"
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: /^version: 10\.0\.28$/ / 新值: /^version: 10\.0\.29$/ / 原因: 年月阶段改口述并禁止追问本人
assert.match(skill, /^version: 10\.0\.29$/m);
// 原值: /^version: 10\.0\.29$/ / 新值: /^version: 10\.0\.30$/ / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.match(skill, /^version: 10\.0\.30$/m);
assert.match(skill, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.match(comparison, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
});
@@ -1456,7 +1456,8 @@ test("public tool surface stays at 14 and new cases bind 10.0.26", () => {
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
const deprecated = resolveExactSkillPackage(
"jyotish-birth-time-rectification",
"10.0.2",
@@ -603,7 +603,8 @@ test("skill version is 10.0.26 after the targeted-collect-cards bump", () => {
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
});
test("USER_COLLECT_QUESTION no longer has an other fallback", () => {
@@ -0,0 +1,301 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { INFERENCE_ALGORITHM_VERSION, type InferenceCandidate, type InferenceState } from "../src/lib/rectification-agentic/core/types.ts";
import { buildRangeDelivery } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
import {
RANGE_DELIVERY_PERCENT_MIN_GAP,
RECTIFICATION_USER_COPY,
listUserVisibleCopy,
rangeDeliveryMostLikely,
rangeDeliveryShowsPercents,
} from "../src/lib/rectification-agentic/user-copy.ts";
import {
GUIDED_WINDOW_CASE_LIMIT,
cardHoldingLinesExhausted,
guidedCollectExhausted,
guidedWindowPool,
type CollectionEvidence,
type GuidedCollectWindow,
} from "../src/lib/rectification-agentic/v9/collection-question-pool.ts";
import { buildMethodFollowupPlan, targetedCollectFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { RectificationRangeDelivery } from "../src/components/rectification-range-delivery.tsx";
import type { RectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts";
import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts";
// Fictional data only. Three columns inside 05:00–05:15.
function cand(time: string, probability: number): InferenceCandidate {
return {
id: time,
time,
cluster_range: [time, time],
prior_score: probability * 100,
posterior_score: probability * 100,
probability,
status: "active",
rank: 1,
strong_conflict_count: 0,
};
}
function inference(probabilities: readonly [number, number, number]): InferenceState {
return {
algorithm_version: INFERENCE_ALGORITHM_VERSION,
candidate_set_id: "05:00-05:15:05:02,05:07,05:13",
revision: 1,
phase: "discrimination",
result_status: "credible_range",
range_start: "05:00",
range_end: "05:15",
candidates: [
cand("05:07", probabilities[0]),
cand("05:02", probabilities[1]),
cand("05:13", probabilities[2]),
],
events: [
{ id: "e1", domain: "career", year: 2018, precision: "month", usage: "training" },
{ id: "e2", domain: "education", year: 2012, precision: "month", usage: "training" },
],
probes: [],
answered_probes: [],
rounds: [],
entropy: 1,
representative_time: "05:07",
credible_range: ["05:00", "05:15"],
};
}
const PUBLIC = [
{ candidateId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1", time: "05:07" },
{ candidateId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2", time: "05:02" },
{ candidateId: "cccccccc-cccc-4ccc-8ccc-ccccccccccc3", time: "05:13" },
];
function result(probabilities: readonly [number, number, number]): RectificationCandidateResult {
const delivery = buildRangeDelivery({
inference: inference(probabilities),
publicCandidates: PUBLIC,
credibleRange: ["05:00", "05:15"],
representativeTime: "05:07",
});
return {
resultId: "result-1",
candidates: delivery.columns.map((column, index) => ({
candidateId: column.candidate_id,
rank: index + 1,
time: column.time,
relativeSupport: 10,
tiedMinuteCount: 1,
})),
overallConfidence: "medium",
selectionAllowed: true,
canAdopt: true,
confirmationAllowed: false,
decisionReceipt: null,
representativeTime: delivery.representative_time,
selectedTime: null,
selectionKind: null,
houseTable: null,
houseTablesByTime: {},
natalRecast: null,
techniqueAudit: [],
windowTransitions: [],
eventDashaLedger: [],
dashaAgreement: null,
lagnaContrast: null,
nakshatraBoundary: null,
precisionStage: null,
oosBlindPrompts: [],
confirmationGate: { confirmation_allowed: false } as RectificationCandidateResult["confirmationGate"],
validated: false,
completionStatus: null,
sessionOutcome: "adopt_representative",
credibleRange: delivery.range,
rangeDelivery: delivery,
verificationReportMarkdown: null,
};
}
function render(probabilities: readonly [number, number, number]): string {
return renderToStaticMarkup(createElement(RectificationRangeDelivery, {
result: result(probabilities),
acceptingCandidateId: null,
readonly: false,
onAccept: () => undefined,
}));
}
test("D4 threshold is 5 points between the first and second column", () => {
assert.equal(RANGE_DELIVERY_PERCENT_MIN_GAP, 5);
assert.equal(rangeDeliveryShowsPercents([{ probability_percent: 40 }, { probability_percent: 35 }]), true);
assert.equal(rangeDeliveryShowsPercents([{ probability_percent: 30 }, { probability_percent: 26 }]), false);
assert.equal(rangeDeliveryShowsPercents([
{ probability_percent: 26 },
{ probability_percent: 26 },
{ probability_percent: 20 },
]), false);
// Order-independent: the gap is first − second after ranking.
assert.equal(rangeDeliveryShowsPercents([{ probability_percent: 30 }, { probability_percent: 45 }]), true);
// One column has no second place to be clearly ahead of.
assert.equal(rangeDeliveryShowsPercents([{ probability_percent: 100 }]), false);
});
test("D3 range is the title and the representative minute is the subtitle", () => {
const html = render([0.45, 0.3, 0.25]);
assert.match(html, /<strong>目前范围 05:00–05:15(对照了 2 件经历)<\/strong>/);
assert.match(html, /rectification-range-delivery__most-likely">最可能 05:07</);
assert.ok(html.indexOf("最可能 05:07") < html.indexOf("rectification-range-delivery__columns"));
assert.equal(rangeDeliveryMostLikely("05:07"), "最可能 05:07");
});
test("D4 gap ≥ 5 shows every column's percentage and no indistinct line", () => {
const html = render([0.45, 0.3, 0.25]);
assert.match(html, /相对可能性 45%/);
assert.match(html, /相对可能性 30%/);
assert.match(html, /相对可能性 25%/);
assert.doesNotMatch(html, new RegExp(RECTIFICATION_USER_COPY.rangeDeliveryIndistinct));
});
test("D4 gap < 5 shows no numbers and one indistinct sentence; columns and order unchanged", () => {
const html = render([0.35, 0.33, 0.32]);
assert.doesNotMatch(html, /相对可能性/);
assert.doesNotMatch(html, /\d+%/);
assert.equal(html.split(RECTIFICATION_USER_COPY.rangeDeliveryIndistinct).length - 1, 1);
assert.equal(
RECTIFICATION_USER_COPY.rangeDeliveryIndistinct,
"这几个时刻目前区分不开,补一件带年月的经历能帮助分开。",
);
assert.equal([...html.matchAll(/<button\b/g)].length, 3);
const order = ["05:07", "05:02", "05:13"].map((time) => html.indexOf(`__time">${time}`));
assert.deepEqual([...order].sort((left, right) => left - right), order);
assert.ok(order.every((index) => index > 0));
// The subtitle stays: it names the representative minute, not a confidence.
assert.match(html, /最可能 05:07/);
});
test("D3/D4 copy is registered as user-visible copy", () => {
const visible = listUserVisibleCopy();
assert.ok(visible.includes(RECTIFICATION_USER_COPY.rangeDeliveryIndistinct));
assert.ok(visible.includes(rangeDeliveryMostLikely("05:07")));
const voice = readFileSync(new URL("../docs/VOICE.md", import.meta.url), "utf8");
assert.ok(voice.includes(RECTIFICATION_USER_COPY.rangeDeliveryIndistinct));
assert.ok(voice.includes("最可能 HH:MM"));
});
const FRESH_WINDOWS: readonly GuidedCollectWindow[] = [
{ year: 2019, month_lo: 4, month_hi: 6, domain: "any", split: { left: 2, right: 1 } },
{ year: 2015, month_lo: 9, month_hi: 9, domain: "any", split: { left: 1, right: 2 } },
];
function askedWindow(year: number, lo: number, hi: number, status = "declined") {
return {
questionId: `collect:guided:window:${year}:${lo}:${hi}:any`,
target_domain: "any",
status,
intent: "collect_method_evidence",
target_kind: "guided:window:any",
};
}
test("D1 at most two guided windows per Case even when a new receipt brings fresh ones", () => {
assert.equal(GUIDED_WINDOW_CASE_LIMIT, 2);
const oneAsked = [askedWindow(2021, 1, 3)];
assert.equal(guidedWindowPool(FRESH_WINDOWS, [], oneAsked).length, 2);
const twoAsked = [askedWindow(2021, 1, 3), askedWindow(2017, 5, 5)];
assert.equal(guidedWindowPool(FRESH_WINDOWS, [], twoAsked).length, 0);
// The same window re-asked under `:next` is still one window.
const sameTwice = [askedWindow(2021, 1, 3), { ...askedWindow(2021, 1, 3), questionId: "collect:guided:window:2021:1:3:any:next" }];
assert.equal(guidedWindowPool(FRESH_WINDOWS, [], sameTwice).length, 2);
// An active (being asked) window does not count as asked yet.
assert.equal(guidedWindowPool(FRESH_WINDOWS, [], [askedWindow(2021, 1, 3), askedWindow(2017, 5, 5, "active")]).length, 2);
// With the cap reached the next question falls through to the targeted lines.
const followup = targetedCollectFollowup(["d9"], [], twoAsked, ["05:00", "05:15"], 3, ["05:00", "05:15"], FRESH_WINDOWS);
assert.ok(followup?.collection_key?.startsWith("collect:targeted:"), followup?.collection_key);
});
const ALL_SEVEN: CollectionEvidence[] = [
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2012-09-01", occurredTo: "2012-09-01" },
{ status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2018-07-01", occurredTo: "2018-07-01" },
{ status: "confirmed", domain: "relocation", datePrecision: "month", occurredFrom: "2016-08-01", occurredTo: "2016-08-01" },
{ status: "confirmed", domain: "relationship", datePrecision: "month", occurredFrom: "2021-08-01", occurredTo: "2021-08-01" },
{ status: "confirmed", domain: "family", datePrecision: "month", occurredFrom: "2019-01-01", occurredTo: "2019-01-01" },
{ status: "confirmed", domain: "finance", datePrecision: "month", occurredFrom: "2020-03-01", occurredTo: "2020-03-01" },
{ status: "confirmed", domain: "health_pressure", datePrecision: "month", occurredFrom: "2022-04-01", occurredTo: "2022-04-01" },
];
const LAYERS = ["d9", "d10", "d4", "d5", "d7", "d2", "d30"];
test("D2 unasked guided windows no longer hold the card once the targeted seven are asked", () => {
// The whole guided pool still reports a window left…
assert.equal(guidedCollectExhausted(LAYERS, ALL_SEVEN, [], FRESH_WINDOWS, ["05:00", "05:15"], 3), false);
// …but the lines that may hold the card are asked out.
assert.equal(cardHoldingLinesExhausted(LAYERS, ALL_SEVEN, [], ["05:00", "05:15"], 3), true);
});
test("D2 the skip re-ask and a pending year answer still hold the card", () => {
const skipped = [{
questionId: "collect:targeted:family",
target_domain: "family",
status: "skipped",
intent: "collect_method_evidence",
target_kind: "targeted:family",
}];
const withoutFamily = ALL_SEVEN.filter((row) => row.domain !== "family");
assert.equal(cardHoldingLinesExhausted(LAYERS, withoutFamily, skipped, ["05:00", "05:15"], 3), false);
const yesOnWindow = [askedWindow(2019, 4, 6, "resolved")];
assert.equal(cardHoldingLinesExhausted(LAYERS, ALL_SEVEN, yesOnWindow, ["05:00", "05:15"], 3), false);
// Targeted lines still open: keep asking.
assert.equal(cardHoldingLinesExhausted(LAYERS, ALL_SEVEN.slice(0, 3), [], ["05:00", "05:15"], 3), false);
});
test("D2 gate unmet with every card-holding line asked delivers; D5 range rules unchanged", () => {
const decision = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: [
{ time: "05:02", score: 13 },
{ time: "05:07", score: 13.4 },
{ time: "05:13", score: 13.1 },
],
holdoutValidation: "unavailable",
datedEventCount: 7,
datedDomainCount: 7,
discriminatorProbe: null,
targetedCollectExhausted: true,
refreshExhausted: true,
inferenceCredibleRange: ["05:00", "05:15"],
openingCandidateRange: ["04:45", "05:15"],
precisionGateMet: false,
guidedCollectExhausted: cardHoldingLinesExhausted(LAYERS, ALL_SEVEN, [], ["05:00", "05:15"], 3),
});
assert.equal(decision.canOfferRange, true);
assert.deepEqual(decision.credibleRange, ["05:00", "05:15"]);
assert.equal(decision.precisionGateMet, false);
assert.equal(decision.canConfirmExactMinute, false);
});
test("D2 a delivered card does not carry an unasked guided window as the next question", () => {
const base = {
evidence: ALL_SEVEN,
declinedTopics: [],
candidatesSeparated: false,
remainingLayers: LAYERS,
remainingSplitTimes: ["05:00", "05:15"] as const,
remainingCandidateCount: 3,
remainingCredibleRange: ["05:00", "05:15"] as const,
guidedWindows: FRESH_WINDOWS,
};
for (const sessionOutcome of ["completed_with_range", "provisional_range", "adopt_representative"] as const) {
const plan = buildMethodFollowupPlan({ ...base, sessionOutcome });
const key = plan.next_followup?.collection_key ?? "";
assert.equal(key.startsWith("collect:guided:window:"), false, `${sessionOutcome}: ${key}`);
}
// While still collecting, the same windows are asked (at most two per Case).
const collecting = buildMethodFollowupPlan({ ...base, sessionOutcome: "collect_evidence" });
assert.match(collecting.next_followup?.collection_key ?? "", /^collect:guided:window:/);
});
@@ -220,14 +220,16 @@ test("new-case skill identity is 10.0.26 and the prompt prefers batch ingest", (
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
// 原值: "10.0.25"
// 原值: "10.0.26"
// 新值: "10.0.27"
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: /^version: 10\.0\.28$/ / 新值: /^version: 10\.0\.29$/ / 原因: 年月阶段改口述并禁止追问本人
assert.match(skill, /^version: 10\.0\.29$/m);
// 原值: /^version: 10\.0\.29$/ / 新值: /^version: 10\.0\.30$/ / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.match(skill, /^version: 10\.0\.30$/m);
assert.match(skill, /不要对同一句用户消息里的多件事件逐条 propose\+confirm/);
assert.match(agentSource, /新事件走 rectification-record-evidence-batch/);
assert.doesNotMatch(agentSource, /分别调用 rectification-propose-evidence 和 rectification-confirm-evidence/);
@@ -191,7 +191,8 @@ test("skill version is 10.0.26 after the targeted-collect-cards bump", () => {
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
});
test("nineteen-row ledger opens the training gate with four scoreable domains", () => {
@@ -431,7 +431,8 @@ test("skill version is 10.0.26 after the targeted-collect-cards bump", () => {
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
});
test("pre-fix dual-exit constant is gone; range narration carries numbers and the disclaimer", () => {
@@ -464,7 +464,8 @@ test("skill version is 10.0.26", () => {
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
});
test("two education events do not spawn birth-year reverse questions", () => {
@@ -101,7 +101,8 @@ test("skill version is 10.0.26 after the targeted-collect-cards bump", () => {
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
});
test("cases current_question remains the submit contract, not a visual slot", () => {
@@ -197,7 +197,8 @@ test("Skill 10.0.26 lists the fourth targeted-collect skip option", () => {
const skill = readFileSync(new URL("../../skills/jyotish-birth-time-rectification/SKILL.md", import.meta.url), "utf8");
const strategy = readFileSync(new URL("../../skills/jyotish-birth-time-rectification/references/conversation-strategy.md", import.meta.url), "utf8");
// 原值: /^version: 10\.0\.28$/ / 新值: /^version: 10\.0\.29$/ / 原因: 年月阶段改口述并禁止追问本人
assert.match(skill, /^version: 10\.0\.29$/m);
// 原值: /^version: 10\.0\.29$/ / 新值: /^version: 10\.0\.30$/ / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.match(skill, /^version: 10\.0\.30$/m);
assert.match(skill, /已拒绝(没有发生过 \/ 这类事都没有过)的目标不得换词重问;跳过的按服务器计划最多重问一次/);
assert.match(strategy, /跳过的线按服务器计划最多换一种问法再问一次/);
});
@@ -104,7 +104,8 @@ test("agent pins the dedicated rectification skill and its fixed version", () =>
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.29"));
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.30"));
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");
@@ -114,7 +115,8 @@ test("agent pins the dedicated rectification skill and its fixed version", () =>
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
});
test("step budgets are bounded per action with a hard ceiling", () => {
@@ -101,7 +101,8 @@ test("the active rectification skill pins the v10 identity and lives in the righ
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
assert.match(skill, /^---\nname: jyotish-birth-time-rectification/m);
// 原值: "10.0.25"
// 原值: "10.0.26"
@@ -109,7 +110,8 @@ test("the active rectification skill pins the v10 identity and lives in the righ
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: /^version: 10\.0\.28$/ / 新值: /^version: 10\.0\.29$/ / 原因: 年月阶段改口述并禁止追问本人
assert.match(skill, /^version: 10\.0\.29$/m);
// 原值: /^version: 10\.0\.29$/ / 新值: /^version: 10\.0\.30$/ / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.match(skill, /^version: 10\.0\.30$/m);
assert.match(skill, /至多一个主问题且唯一来源:[\s\S]*不得自行提出、复述、改写或预告问题/);
for (const reference of references) {
const content = readFileSync(`${skillDirectory}/references/${reference}`, "utf8");
@@ -211,7 +211,8 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
should_start_opening: true,
// 原值: "10.0.25";新值: "10.0.26";原因: BUG-668 新案绑定现行 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
skill_version: "10.0.29",
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
skill_version: "10.0.30",
};
}
return null;
@@ -255,7 +256,8 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(response.skillVersion, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(response.skillVersion, "10.0.30");
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");
@@ -265,7 +267,8 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(openCall.args.p_skill_version, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(openCall.args.p_skill_version, "10.0.30");
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);
@@ -92,5 +92,6 @@ test("agent body cannot verbally accept a spoken birth window", () => {
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
});
@@ -167,7 +167,8 @@ test("SCORE_DELTA stays ±2/±1 and yearless weight is half", () => {
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.29");
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
});
test("D9 answer B moves scores by ±1 and does not count toward elimination", () => {
+5 -5
View File
@@ -85,11 +85,11 @@ test("checked-in registry verifies hashed product packages and leaves consult on
[
{
name: "jyotish-birth-time-rectification",
// 原值: 10.0.28 / f0bb8295…cffe
// 新值: 10.0.29 / 750a0d58…724c
// 原因: 年月阶段改口述、禁止追问本人
version: "10.0.29",
sha256: "750a0d58c6731c0dbdeb5c785cea1bf8a3f73aaf94a560a86397805b7037724c",
// 原值: 10.0.29 / 750a0d58…724c
// 新值: 10.0.30 / 926db1ab…3a13
// 原因: D2 出卡句(引导窗口题不挡出卡、一次校正最多两道)与 D4 卡上百分比规则写进 Skill
version: "10.0.30",
sha256: "926db1ab20e7990ca0585037b6595691a2db0da363b4ab433f2fb98b2d223a13",
},
{
name: "jyotish-personal-report",