fix(rectification): bump scoring identity to scoring-14 for the #166 rulings (BUG-1583..1588)

78f98eba changes the same-input rectification scores: Shadbala Sthana and Drik
feed the Shadbala auxiliary (BPHS Saptavargaja values, Moolatrikona 3 / 15 deg,
whole-sign Kendradi, BPHS 26 special aspects, Mercury with any malefic) and
jaimini.calc_arudha_padas feeds the event engine and feature facts. Per
BUG-1181 the algorithm identity moves rectification-v5-matrix-scoring-13 ->
-14. Policy v4, input contract v5 and the dated window contract (generation
>= 9) are unchanged; the SQL guard accepts scoring-(9|[1-9][0-9]+), so no
migration.

- Memoization golden v7 written by write_golden (source_commit 78f98eba); v6
  stays byte-identical and is pinned by sha256. For this public request the
  candidate scores are unchanged (12:00 8.5336, 12:01 8.5242, 12:02 8.088);
  the Shadbala fingerprints, feature hash and candidate UUID moved.
- New scoring-14 cross-midnight golden from midnight_date_anchor_regression.py
  --golden (baseline = this tree; independent-process A/B 3 x 7 fields equal);
  scoring-13 golden kept as history and opens read-only with recompare.
- Dated-transitions scan unchanged (test passes without regeneration).
- v9EngineVersion() default and the identity tests follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
This commit is contained in:
2026-11-04 21:10:43 +08:00
co-authored by Claude Opus 5.5
parent 78f98eba93
commit 1ce4eec53c
11 changed files with 5386 additions and 22 deletions
@@ -1095,7 +1095,7 @@ export async function runV9Diagnostics(input: {
// Keep the receipt default aligned with Python's ALGORITHM_VERSION; persisted
// receipts retain their original identity (this is not a Skill/open gate).
export const v9EngineVersion = (): string =>
process.env.RECTIFICATION_ENGINE_VERSION?.trim() || "rectification-v5-matrix-scoring-13";
process.env.RECTIFICATION_ENGINE_VERSION?.trim() || "rectification-v5-matrix-scoring-14";
export type V9RangeReading = Readonly<{
stableThemes: readonly string[];
@@ -134,7 +134,7 @@ test(`dated adoption ${declaration.time} persists date/spec atomically and rejec
if sqlerrm <> 'agentic_rectification_candidate_state_inconsistent' then raise; end if;
end;
end $$; select 'rejected';`);
for (const version of ["rectification-v5-matrix-scoring-10", "rectification-v5-matrix-scoring-11", "rectification-v5-matrix-scoring-12", "rectification-v5-matrix-scoring-13"]) {
for (const version of ["rectification-v5-matrix-scoring-10", "rectification-v5-matrix-scoring-11", "rectification-v5-matrix-scoring-12", "rectification-v5-matrix-scoring-13", "rectification-v5-matrix-scoring-14"]) {
setVersion(version);
assert.match(rejects(), /rejected/, `${version} must not pass as legacy`);
}
File diff suppressed because it is too large Load Diff
@@ -13,7 +13,7 @@ import { dossierFixture } from "./rectification-v9-test-support.ts";
test("dated contract is a generation threshold, not one literal", () => {
assert.equal(FIRST_DATED_SCORING_GENERATION, 9);
for (const version of ["rectification-v5-matrix-scoring-9", "rectification-v5-matrix-scoring-10", "rectification-v5-matrix-scoring-11", "rectification-v5-matrix-scoring-12", "rectification-v5-matrix-scoring-13", "rectification-v5-matrix-scoring-100"]) {
for (const version of ["rectification-v5-matrix-scoring-9", "rectification-v5-matrix-scoring-10", "rectification-v5-matrix-scoring-11", "rectification-v5-matrix-scoring-12", "rectification-v5-matrix-scoring-13", "rectification-v5-matrix-scoring-14", "rectification-v5-matrix-scoring-100"]) {
assert.equal(isDatedScoringAlgorithmVersion(version), true, version);
}
for (const version of ["rectification-v5-matrix-scoring-8", "rectification-v5-matrix-scoring-1", "rectification-v5-matrix-scoring-09",
@@ -22,14 +22,15 @@ test("dated contract is a generation threshold, not one literal", () => {
}
});
test("receipt default and Python identity agree on scoring-13", (t) => {
test("receipt default and Python identity agree on scoring-14", (t) => {
const saved = process.env.RECTIFICATION_ENGINE_VERSION;
delete process.env.RECTIFICATION_ENGINE_VERSION;
t.after(() => { if (saved === undefined) delete process.env.RECTIFICATION_ENGINE_VERSION; else process.env.RECTIFICATION_ENGINE_VERSION = saved; });
// 原值: rectification-v5-matrix-scoring-12;新值: ...-scoring-13;原因: D30 偶数星座按 BPHS、白羊 / 天蝎金星功能凶改变同输入分数(BUG-1560 / BUG-1570)。
assert.equal(v9EngineVersion(), "rectification-v5-matrix-scoring-13");
// 原值: ...-scoring-13;新值: ...-scoring-14;原因: #166 占星师裁定(Shadbala Sthana / Drik、jaimini.py Arudha 例外,BUG-1583~1588)改变同输入分数。
assert.equal(v9EngineVersion(), "rectification-v5-matrix-scoring-14");
const python = readFileSync(new URL("../../scripts/rectification/scoring_service.py", import.meta.url), "utf8");
assert.match(python, /^ALGORITHM_VERSION = "rectification-v5-matrix-scoring-13"$/m);
assert.match(python, /^ALGORITHM_VERSION = "rectification-v5-matrix-scoring-14"$/m);
});
test("no source file compares the algorithm identity with a scoring-N literal", () => {
@@ -117,3 +118,16 @@ test("a scoring-12 history opens read-only under scoring-13 and offers a recompa
assert.deepEqual([underThirteen.read_only, underThirteen.can_recompare, underThirteen.algorithm_version], [true, true, "rectification-v5-matrix-scoring-12"]);
assert.ok(dossier.latestResult!.candidates.every((candidate) => typeof candidate.candidate_date === "string"));
});
test("a scoring-13 history opens read-only under scoring-14 and offers a recompare (BUG-621)", () => {
const historical = JSON.parse(readFileSync(new URL("./fixtures/rectification-midnight-date-anchor.delivery.scoring-13.native.json", import.meta.url), "utf8")).response;
assert.equal(historical.algorithm_version, "rectification-v5-matrix-scoring-13");
const latest = { ...historical, candidates: historical.candidate_decisions };
const dossier = parseV9CaseDossier(dossierFixture({ stage: "minute", latestResult: latest }));
assert.ok(dossier);
assert.equal(dossier.latestResult?.algorithmVersion, "rectification-v5-matrix-scoring-13");
const policyVersion = historical.decision_policy_version;
const underFourteen = resultIdentityView(dossier, { algorithmVersion: "rectification-v5-matrix-scoring-14", policyVersion });
assert.deepEqual([underFourteen.read_only, underFourteen.can_recompare, underFourteen.algorithm_version], [true, true, "rectification-v5-matrix-scoring-13"]);
assert.ok(dossier.latestResult!.candidates.every((candidate) => typeof candidate.candidate_date === "string"));
});
@@ -34,11 +34,15 @@ const PREVIOUS = "rectification-v5-matrix-scoring-7";
// 原值: LIVE_CURRENT = "rectification-v5-matrix-scoring-12";新值: "...-scoring-13";
// scoring-12 dated golden 留作历史,当前版本另有真实引擎生成的 scoring-13 golden。
// 原因: D30 偶数星座按 BPHS、白羊 / 天蝎金星功能凶改变同输入分数(BUG-1560 / BUG-1570),按 BUG-1181 升算法身份。
const LIVE_CURRENT = "rectification-v5-matrix-scoring-13";
// 原值: LIVE_CURRENT = "rectification-v5-matrix-scoring-13";新值: "...-scoring-14";
// scoring-13 dated golden 留作历史,当前版本另有真实引擎生成的 scoring-14 golden。
// 原因: #166 占星师裁定(Shadbala Sthana / Drik、jaimini.py Arudha 例外,BUG-1583~1588)改变同输入分数,按 BUG-1181 升算法身份。
const LIVE_CURRENT = "rectification-v5-matrix-scoring-14";
const DATED_HISTORICAL = "rectification-v5-matrix-scoring-9";
const SCORING_10 = "rectification-v5-matrix-scoring-10";
const SCORING_11 = "rectification-v5-matrix-scoring-11";
const SCORING_12 = "rectification-v5-matrix-scoring-12";
const SCORING_13 = "rectification-v5-matrix-scoring-13";
const liveGolden = JSON.parse(readFileSync(new URL(
"./fixtures/rectification-midnight-date-anchor.delivery.native.json", import.meta.url,
), "utf8"));
@@ -51,9 +55,12 @@ const scoring11Golden = JSON.parse(readFileSync(new URL(
const scoring12Golden = JSON.parse(readFileSync(new URL(
"./fixtures/rectification-midnight-date-anchor.delivery.scoring-12.native.json", import.meta.url,
), "utf8"));
const currentGolden = JSON.parse(readFileSync(new URL(
const scoring13Golden = JSON.parse(readFileSync(new URL(
"./fixtures/rectification-midnight-date-anchor.delivery.scoring-13.native.json", import.meta.url,
), "utf8"));
const currentGolden = JSON.parse(readFileSync(new URL(
"./fixtures/rectification-midnight-date-anchor.delivery.scoring-14.native.json", import.meta.url,
), "utf8"));
const envKeys = [
"RECTIFICATION_ENGINE_VERSION", "RECTIFICATION_ALGORITHM_VERSION",
"RECTIFICATION_DECISION_POLICY_VERSION",
@@ -80,11 +87,12 @@ test("receipt default matches the cross-midnight native scoring version without
assert.equal(golden.score.algorithm_version, CURRENT);
assert.equal(golden.versions.algorithm_version, CURRENT);
const python = readFileSync(new URL("../../scripts/rectification/scoring_service.py", import.meta.url), "utf8");
assert.match(python, /ALGORITHM_VERSION = "rectification-v5-matrix-scoring-13"/);
assert.match(python, /ALGORITHM_VERSION = "rectification-v5-matrix-scoring-14"/);
assert.equal(liveGolden.response.algorithm_version, DATED_HISTORICAL);
assert.equal(scoring10Golden.response.algorithm_version, SCORING_10);
assert.equal(scoring11Golden.response.algorithm_version, SCORING_11);
assert.equal(scoring12Golden.response.algorithm_version, SCORING_12);
assert.equal(scoring13Golden.response.algorithm_version, SCORING_13);
assert.equal(currentGolden.response.algorithm_version, LIVE_CURRENT);
const fingerprints = { evidenceLedgerFingerprint: "e".repeat(64), candidateRangeFingerprint: "c".repeat(64) };
const live = {
@@ -93,11 +101,12 @@ test("receipt default matches the cross-midnight native scoring version without
};
const historical = { ...fingerprints, algorithmVersion: CURRENT, policyVersion: golden.versions.decision_policy_version };
assert.equal(cachedEngineScoreIsReusable(historical, fingerprints, live), false);
// BUG-1181 / BUG-1260 / BUG-1275 / BUG-1560: scoring-9 to scoring-12 caches are not current under scoring-13.
// BUG-1181 / BUG-1260 / BUG-1275 / BUG-1560 / #166: scoring-9 to scoring-13 caches are not current under scoring-14.
assert.equal(cachedEngineScoreIsReusable({ ...historical, algorithmVersion: DATED_HISTORICAL }, fingerprints, live), false);
assert.equal(cachedEngineScoreIsReusable({ ...historical, algorithmVersion: SCORING_10 }, fingerprints, live), false);
assert.equal(cachedEngineScoreIsReusable({ ...historical, algorithmVersion: SCORING_11 }, fingerprints, live), false);
assert.equal(cachedEngineScoreIsReusable({ ...historical, algorithmVersion: SCORING_12 }, fingerprints, live), false);
assert.equal(cachedEngineScoreIsReusable({ ...historical, algorithmVersion: SCORING_13 }, fingerprints, live), false);
// 原值: 只把 algorithmVersion 改成 LIVE_CURRENT 即可复用(当时 golden 的策略与旧 fixture 相同)
// 新值: 复用还要带上当前 golden 的 policyVersion
// 原因: scoring-11 回执的策略是 policy-v4,旧跨午夜 fixture 的策略不是这一版(BUG-1260)
@@ -99,8 +99,9 @@ for (const option of ["A", "B", "C", "skip_probe"] as const) test(`D1 initial no
// BUG-1181: scoring-9 and later goldens must parse as dated.
// 原值: native + scoring-10 + scoring-11;新值: 再加 scoring-12;原因: 章动修正升到 scoring-12(BUG-1275),旧文件仍要按日期窗打开。
// 原值: 到 scoring-13;新值: 再加 scoring-14;原因: #166 占星师裁定(Shadbala Sthana / Drik、jaimini.py Arudha 例外,BUG-1583~1588)改变同输入分数,旧文件仍要按日期窗打开。
// 原值: 到 scoring-12;新值: 再加 scoring-13;原因: D30 / 金星功能凶升到 scoring-13(BUG-1560 / BUG-1570),旧文件仍要按日期窗打开。
for (const fixture of ["rectification-midnight-date-anchor.delivery.native.json", "rectification-midnight-date-anchor.delivery.scoring-10.native.json", "rectification-midnight-date-anchor.delivery.scoring-11.native.json", "rectification-midnight-date-anchor.delivery.scoring-12.native.json", "rectification-midnight-date-anchor.delivery.scoring-13.native.json"]) test(`native golden crosses engine parsing, inference and dated delivery without reanchoring (${fixture})`, async () => {
for (const fixture of ["rectification-midnight-date-anchor.delivery.native.json", "rectification-midnight-date-anchor.delivery.scoring-10.native.json", "rectification-midnight-date-anchor.delivery.scoring-11.native.json", "rectification-midnight-date-anchor.delivery.scoring-12.native.json", "rectification-midnight-date-anchor.delivery.scoring-13.native.json", "rectification-midnight-date-anchor.delivery.scoring-14.native.json"]) test(`native golden crosses engine parsing, inference and dated delivery without reanchoring (${fixture})`, async () => {
const golden = JSON.parse(readFileSync(new URL(`./fixtures/${fixture}`, import.meta.url), "utf8"));
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(JSON.stringify(golden.response), { status: 200 })) as typeof fetch;
+6 -1
View File
@@ -26,7 +26,12 @@ from scripts.rectification.case_holdout import holdout_event_ids
# even-sign D30 follows BPHS ch. 6 (health_pressure, feature facts v4) and
# Venus is a functional malefic for Aries / Scorpio (BPHS 34.20 / 34.35), so
# the same input scores differently. Policy and the dated contract unchanged.
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-13"
# scoring-14 (2026-11-04, #166 astrologer rulings, product decision; BUG-1583..1588):
# Shadbala Sthana (BPHS Saptavargaja values, Moolatrikona Moon 3 / Mercury 15,
# whole-sign Kendradi) and Drik (BPHS 26 special aspects, Mercury with any malefic)
# feed the Shadbala auxiliary; jaimini.py Arudha 1/7 exception counts from the
# first-count pada. Same input scores differently. Policy and dated contract unchanged.
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-14"
INPUT_CONTRACT_VERSION = "rectification-calculation-spec-v5"
PRECISION_WEIGHTS = {
"day": 1.0,
File diff suppressed because it is too large Load Diff
@@ -35,7 +35,8 @@ def test_fixed_scoring_identity_is_exposed_without_changing_input_contract():
# (BUG-1275),按 BUG-1181 升算法身份;输入合同 v5 与日期窗合同不变。
# 原值: "rectification-v5-matrix-scoring-12";新值: "...-scoring-13";原因: varga.py 偶数星座 D30 按 BPHS 改正、
# 白羊 / 天蝎上升金星判功能凶(BUG-1560 / BUG-1570,产品 2026-11-04 决定),同输入分数变了,按 BUG-1181 升算法身份。
assert engine_scoring_versions()["algorithm_version"] == ALGORITHM_VERSION == "rectification-v5-matrix-scoring-13"
# 原值: "rectification-v5-matrix-scoring-13";新值: "...-scoring-14";原因: 同输入分数变了(#166 占星师裁定:Shadbala Sthana / Drik 按具名 BPHS 口径、jaimini.py Arudha 例外从初算落点数,BUG-1583~1588),按 BUG-1181 升算法身份。
assert engine_scoring_versions()["algorithm_version"] == ALGORITHM_VERSION == "rectification-v5-matrix-scoring-14"
assert INPUT_CONTRACT_VERSION == "rectification-calculation-spec-v5"
request = {"birth_date": "2000-03-01", "start_time": "00:00", "end_time": "00:02",
"lat": 0.0, "lon": 0.0, "tz": 0.0, "events": []}
+35 -8
View File
@@ -42,7 +42,7 @@ longitudes included nutation. The same request moves (12:00 8.5289 -> 8.5336).
The algorithm is rectification-v5-matrix-scoring-12. Policy stays v4.
v4 stays byte-for-byte and is pinned below. v5 is now frozen the same way.
Current golden tests/golden/rectification_engine_memoization_v6.json
Golden tests/golden/rectification_engine_memoization_v6.json
(2026-11-04, BUG-1560 / BUG-1570, product decision) was written by
``write_golden`` after varga.py's even-sign D30 followed BPHS ch. 6 and Venus
became a functional malefic for Aries / Scorpio. This request's candidate
@@ -50,7 +50,16 @@ scores are unchanged (12:00 8.5336, 12:01 8.5242, 12:02 8.088; the chart is
not Aries / Scorpio rising), but the D30 feature (Capricorn -> Pisces), its
window transition and the feature hashes moved; other inputs score
differently, hence rectification-v5-matrix-scoring-13. Policy stays v4. v5
stays byte-for-byte and is pinned below.
stays byte-for-byte and is pinned below. v6 is now frozen the same way.
Current golden tests/golden/rectification_engine_memoization_v7.json
(2026-11-04, #166 astrologer rulings, BUG-1583..1588, product decision) was
written by ``write_golden`` after Shadbala Sthana / Drik followed the named
BPHS version (Saptavargaja values, Moolatrikona 3 / 15 deg, whole-sign
Kendradi, BPHS 26 special aspects, Mercury with any malefic) and the
jaimini.py Arudha 1/7 exception counted from the first-count pada. Both feed
the rectification score, hence rectification-v5-matrix-scoring-14. Policy
stays v4. v6 stays byte-for-byte and is pinned below.
Do not compare that payload with a whole-structure ``==``. Cross-machine
libm / pyswisseph rounding already drifted ``margin_percent`` by 1.1e-3
@@ -90,7 +99,10 @@ import scripts.rectification.scoring_service as scoring_service
import shadbala
ROOT = Path(__file__).resolve().parents[1]
GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v6.json"
GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v7.json"
V6_GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v6.json"
V6_GOLDEN_SHA256 = "f8d63b807293464eb20585ecf8c2408f0cbf310bad4bf71f3e61671eac0e1f4b"
V6_SOURCE_COMMIT = "18c685c428a06f4530e43218588c03ca9ce1dea3"
V5_GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v5.json"
V5_GOLDEN_SHA256 = "92efd8fdbae43f553b0006cfe28e929d5255fe02f3c36e616475fa149a356212"
V5_SOURCE_COMMIT = "e5f2a75183381c4a027014c7f1e89ddcb8393bd3"
@@ -107,14 +119,15 @@ HISTORICAL_GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoi
HISTORICAL_GOLDEN_SHA256 = "6266e448d7dad204b264cbe8f9bcf4c36e02286794764a94edb9e2ce4d0e3015"
# 原值: rectification-v5-matrix-scoring-11;新值: ...-scoring-12;原因: 章动修正改变同输入分数(BUG-1275)。
# 原值: rectification-v5-matrix-scoring-12;新值: ...-scoring-13;原因: D30 偶数星座与白羊 / 天蝎金星功能凶改变同输入分数(BUG-1560 / BUG-1570)。
CURRENT_ALGORITHM_VERSION = "rectification-v5-matrix-scoring-13"
# 原值: rectification-v5-matrix-scoring-13;新值: ...-scoring-14;原因: #166 占星师裁定改了 Shadbala Sthana / Drik 与 jaimini.py Arudha 例外,同输入分数变(BUG-1583~1588)。
CURRENT_ALGORITHM_VERSION = "rectification-v5-matrix-scoring-14"
FROZEN_TODAY = date(2026, 9, 16)
TIMING_KEYS = frozenset({"column_compare_ms"})
HISTORICAL_SOURCE_COMMIT = "a8d29d1b6cc37ff865ddec6c8bccdf9aa889ee53"
CURRENT_POLICY_VERSION = "rectification-candidate-policy-v4"
# D30 / Venus-role fix commit that moves these scores (BUG-1560 / BUG-1570).
# v5 keeps V5_SOURCE_COMMIT.
SOURCE_COMMIT = "18c685c428a06f4530e43218588c03ca9ce1dea3"
# #166 rulings commit that moves these scores (BUG-1583..1588).
# v6 keeps V6_SOURCE_COMMIT.
SOURCE_COMMIT = "78f98eba93034ad3de8ea348c53f9e4b02241d4c"
# 原值: 4 个缓存键(ashtakavarga / shadbala / vimshottari / narayana_periods)
# 新值: 再加 functional_profile、narayana_antardasha_by_md,共 6 个
# 原因: BUG-1324。这两项只依赖候选分钟,放在静态上下文顶层;置 None 仍现算,输出应相同
@@ -303,7 +316,7 @@ def _contexts_with_layer_cache_cleared(contexts: list[dict[str, Any]]) -> list[d
def _assert_dated_payload_against_golden(actual: dict[str, Any], expected: dict[str, Any]) -> None:
"""Compare with the scoring-13 / policy-v4 golden; identity is spelled out, not trusted."""
"""Compare with the scoring-14 / policy-v4 golden; identity is spelled out, not trusted."""
metadata = {
"candidate_window_contract": "dated-v1",
"candidate_intervals": [{"start_at": "1990-01-01T12:00", "end_at": "1990-01-01T12:02"}],
@@ -413,6 +426,20 @@ def test_scoring_12_golden_is_frozen_and_scores_moved() -> None:
_assert_memoization_payloads(current, previous)
def test_scoring_13_golden_is_frozen_and_scores_moved() -> None:
"""#166 (BUG-1583..1588): v6 stays byte-identical; this request moved, hence scoring-14."""
raw = V6_GOLDEN_PATH.read_bytes()
assert hashlib.sha256(raw).hexdigest() == V6_GOLDEN_SHA256
previous = json.loads(raw)
current = json.loads(GOLDEN_PATH.read_text(encoding="utf-8"))
assert previous["source_commit"] == V6_SOURCE_COMMIT
assert previous["candidate_feature_snapshot"]["algorithm_version"] == "rectification-v5-matrix-scoring-13"
assert current["candidate_feature_snapshot"]["algorithm_version"] == CURRENT_ALGORITHM_VERSION
assert current["source_commit"] == SOURCE_COMMIT
with pytest.raises(AssertionError):
_assert_memoization_payloads(current, previous)
def test_score_candidates_matches_baseline_golden() -> None:
expected = json.loads(GOLDEN_PATH.read_text(encoding="utf-8"))
actual = json.loads(json.dumps(_golden_payload(), ensure_ascii=True))
+2 -1
View File
@@ -23,7 +23,8 @@ class RelativeSupportScaleTest(unittest.TestCase):
# Explicit date windows: scoring identity -8 -> -9, not a prior/policy change.
# 原值: -11;新值: -12;原因: 章动修正改变同输入分数(BUG-1275),策略 v4 与相对支持口径不变。
# 原值: -12;新值: -13;原因: D30 偶数星座与白羊 / 天蝎金星功能凶改变同输入分数(BUG-1560 / BUG-1570),策略 v4 与相对支持口径不变。
self.assertEqual(ALGORITHM_VERSION, "rectification-v5-matrix-scoring-13")
# 原值: -13;新值: -14;原因: 同输入分数变了(#166 占星师裁定:Shadbala Sthana / Drik 按具名 BPHS 口径、jaimini.py Arudha 例外从初算落点数,BUG-1583~1588),策略 v4 与相对支持口径不变。
self.assertEqual(ALGORITHM_VERSION, "rectification-v5-matrix-scoring-14")
self.assertEqual(RELATIVE_SUPPORT_MODE, "proportional")
def test_offset_top_two_lead_is_at_least_proportional(self) -> None: