fix(rectification): bump scoring identity to scoring-15 for #166 batch 2 (BUG-1590)

4b223eb8 makes Gemini Saturn (8, 9) and Virgo Saturn (5, 6) functionally
mixed; the *_functional_* rules read the functional layer, so the same input
scores differently for those ascendants. Per BUG-1181:
rectification-v5-matrix-scoring-14 -> -15. Policy v4, input contract v5 and
the dated window contract unchanged; the SQL guard already accepts it.

- Memoization golden v8 (source_commit 4b223eb8); v7 pinned by sha256. This
  public request's candidate scores are unchanged.
- scoring-15 cross-midnight golden (independent-process A/B 3 x 7 equal);
  scoring-14 golden kept as history and opens read-only with recompare.

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 22:29:06 +08:00
co-authored by Claude Opus 5.5
parent 4b223eb8ea
commit f61d279aec
11 changed files with 5370 additions and 21 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-14";
process.env.RECTIFICATION_ENGINE_VERSION?.trim() || "rectification-v5-matrix-scoring-15";
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", "rectification-v5-matrix-scoring-14"]) {
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", "rectification-v5-matrix-scoring-15"]) {
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-14", "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-15", "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,15 +22,16 @@ test("dated contract is a generation threshold, not one literal", () => {
}
});
test("receipt default and Python identity agree on scoring-14", (t) => {
test("receipt default and Python identity agree on scoring-15", (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)。
// 原值: ...-scoring-13;新值: ...-scoring-14;原因: #166 占星师裁定(Shadbala Sthana / Drik、jaimini.py Arudha 例外,BUG-1583~1588)改变同输入分数。
assert.equal(v9EngineVersion(), "rectification-v5-matrix-scoring-14");
// 原值: ...-scoring-14;新值: ...-scoring-15;原因: #166 第二批(第 4 条:双子 / 处女上升土星改为混合,BUG-1590)改变同输入分数。
assert.equal(v9EngineVersion(), "rectification-v5-matrix-scoring-15");
const python = readFileSync(new URL("../../scripts/rectification/scoring_service.py", import.meta.url), "utf8");
assert.match(python, /^ALGORITHM_VERSION = "rectification-v5-matrix-scoring-14"$/m);
assert.match(python, /^ALGORITHM_VERSION = "rectification-v5-matrix-scoring-15"$/m);
});
test("no source file compares the algorithm identity with a scoring-N literal", () => {
@@ -119,6 +120,16 @@ test("a scoring-12 history opens read-only under scoring-13 and offers a recompa
assert.ok(dossier.latestResult!.candidates.every((candidate) => typeof candidate.candidate_date === "string"));
});
test("a scoring-14 history opens read-only under scoring-15 and offers a recompare (BUG-621)", () => {
const historical = JSON.parse(readFileSync(new URL("./fixtures/rectification-midnight-date-anchor.delivery.scoring-14.native.json", import.meta.url), "utf8")).response;
assert.equal(historical.algorithm_version, "rectification-v5-matrix-scoring-14");
const latest = { ...historical, candidates: historical.candidate_decisions };
const dossier = parseV9CaseDossier(dossierFixture({ stage: "minute", latestResult: latest }));
assert.ok(dossier);
const view = resultIdentityView(dossier, { algorithmVersion: "rectification-v5-matrix-scoring-15", policyVersion: historical.decision_policy_version });
assert.deepEqual([view.read_only, view.can_recompare, view.algorithm_version], [true, true, "rectification-v5-matrix-scoring-14"]);
});
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");
@@ -37,12 +37,14 @@ const PREVIOUS = "rectification-v5-matrix-scoring-7";
// 原值: 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";
// 原值: "...-scoring-14";新值: "...-scoring-15";原因: #166 第二批(第 4 条:双子 / 处女上升土星改为混合,BUG-1590)改变同输入分数,按 BUG-1181 升算法身份;scoring-14 golden 留作历史。
const LIVE_CURRENT = "rectification-v5-matrix-scoring-15";
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 SCORING_14 = "rectification-v5-matrix-scoring-14";
const liveGolden = JSON.parse(readFileSync(new URL(
"./fixtures/rectification-midnight-date-anchor.delivery.native.json", import.meta.url,
), "utf8"));
@@ -58,9 +60,12 @@ const scoring12Golden = 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(
const scoring14Golden = JSON.parse(readFileSync(new URL(
"./fixtures/rectification-midnight-date-anchor.delivery.scoring-14.native.json", import.meta.url,
), "utf8"));
const currentGolden = JSON.parse(readFileSync(new URL(
"./fixtures/rectification-midnight-date-anchor.delivery.scoring-15.native.json", import.meta.url,
), "utf8"));
const envKeys = [
"RECTIFICATION_ENGINE_VERSION", "RECTIFICATION_ALGORITHM_VERSION",
"RECTIFICATION_DECISION_POLICY_VERSION",
@@ -87,12 +92,13 @@ 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-14"/);
assert.match(python, /ALGORITHM_VERSION = "rectification-v5-matrix-scoring-15"/);
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(scoring14Golden.response.algorithm_version, SCORING_14);
assert.equal(currentGolden.response.algorithm_version, LIVE_CURRENT);
const fingerprints = { evidenceLedgerFingerprint: "e".repeat(64), candidateRangeFingerprint: "c".repeat(64) };
const live = {
@@ -107,6 +113,7 @@ test("receipt default matches the cross-midnight native scoring version without
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);
assert.equal(cachedEngineScoreIsReusable({ ...historical, algorithmVersion: SCORING_14 }, fingerprints, live), false);
// 原值: 只把 algorithmVersion 改成 LIVE_CURRENT 即可复用(当时 golden 的策略与旧 fixture 相同)
// 新值: 复用还要带上当前 golden 的 policyVersion
// 原因: scoring-11 回执的策略是 policy-v4,旧跨午夜 fixture 的策略不是这一版(BUG-1260)
@@ -101,7 +101,7 @@ for (const option of ["A", "B", "C", "skip_probe"] as const) test(`D1 initial no
// 原值: 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", "rectification-midnight-date-anchor.delivery.scoring-14.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", "rectification-midnight-date-anchor.delivery.scoring-15.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;
+3 -1
View File
@@ -31,7 +31,9 @@ from scripts.rectification.case_holdout import holdout_event_ids
# 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"
# scoring-15 (2026-11-04, #166 batch 2, BUG-1590): functional roles make Gemini
# Saturn (8, 9) and Virgo Saturn (5, 6) mixed, which the *_functional_* rules read.
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-15"
INPUT_CONTRACT_VERSION = "rectification-calculation-spec-v5"
PRECISION_WEIGHTS = {
"day": 1.0,
File diff suppressed because it is too large Load Diff
@@ -36,7 +36,8 @@ def test_fixed_scoring_identity_is_exposed_without_changing_input_contract():
# 原值: "rectification-v5-matrix-scoring-12";新值: "...-scoring-13";原因: varga.py 偶数星座 D30 按 BPHS 改正、
# 白羊 / 天蝎上升金星判功能凶(BUG-1560 / BUG-1570,产品 2026-11-04 决定),同输入分数变了,按 BUG-1181 升算法身份。
# 原值: "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"
# 原值: "...-scoring-14";新值: "...-scoring-15";原因: #166 第二批(第 4 条:双子 / 处女上升土星改为混合,BUG-1590)改变同输入分数,按 BUG-1181 升算法身份。
assert engine_scoring_versions()["algorithm_version"] == ALGORITHM_VERSION == "rectification-v5-matrix-scoring-15"
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": []}
+29 -8
View File
@@ -52,14 +52,20 @@ 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. v6 is now frozen the same way.
Current golden tests/golden/rectification_engine_memoization_v7.json
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.
stays v4. v6 stays byte-for-byte and is pinned below. v7 is now frozen the same way.
Current golden tests/golden/rectification_engine_memoization_v8.json
(2026-11-04, #166 batch 2, BUG-1590) was written by ``write_golden`` after
the functional roles made Gemini Saturn (8, 9) and Virgo Saturn (5, 6) mixed,
which the *_functional_* rules read; hence rectification-v5-matrix-scoring-15.
Policy stays v4. v7 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
@@ -99,7 +105,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_v7.json"
GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v8.json"
V7_GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v7.json"
V7_GOLDEN_SHA256 = "708e084b4401fd2f516e15224ef794ac4a037b67e40e534473b5c701e8008626"
V7_SOURCE_COMMIT = "78f98eba93034ad3de8ea348c53f9e4b02241d4c"
V6_GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v6.json"
V6_GOLDEN_SHA256 = "f8d63b807293464eb20585ecf8c2408f0cbf310bad4bf71f3e61671eac0e1f4b"
V6_SOURCE_COMMIT = "18c685c428a06f4530e43218588c03ca9ce1dea3"
@@ -120,14 +129,14 @@ HISTORICAL_GOLDEN_SHA256 = "6266e448d7dad204b264cbe8f9bcf4c36e02286794764a94edb9
# 原值: rectification-v5-matrix-scoring-11;新值: ...-scoring-12;原因: 章动修正改变同输入分数(BUG-1275)。
# 原值: rectification-v5-matrix-scoring-12;新值: ...-scoring-13;原因: D30 偶数星座与白羊 / 天蝎金星功能凶改变同输入分数(BUG-1560 / BUG-1570)。
# 原值: 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"
# 原值: ...-scoring-14;新值: ...-scoring-15;原因: #166 第二批(第 4 条:双子 / 处女上升土星改为混合,BUG-1590)改变同输入分数。
CURRENT_ALGORITHM_VERSION = "rectification-v5-matrix-scoring-15"
FROZEN_TODAY = date(2026, 9, 16)
TIMING_KEYS = frozenset({"column_compare_ms"})
HISTORICAL_SOURCE_COMMIT = "a8d29d1b6cc37ff865ddec6c8bccdf9aa889ee53"
CURRENT_POLICY_VERSION = "rectification-candidate-policy-v4"
# #166 rulings commit that moves these scores (BUG-1583..1588).
# v6 keeps V6_SOURCE_COMMIT.
SOURCE_COMMIT = "78f98eba93034ad3de8ea348c53f9e4b02241d4c"
# #166 batch 2 commit that moves these scores (BUG-1590). v7 keeps V7_SOURCE_COMMIT.
SOURCE_COMMIT = "4b223eb8eaf28db7c22364b50c17af7941c1d606"
# 原值: 4 个缓存键(ashtakavarga / shadbala / vimshottari / narayana_periods)
# 新值: 再加 functional_profile、narayana_antardasha_by_md,共 6 个
# 原因: BUG-1324。这两项只依赖候选分钟,放在静态上下文顶层;置 None 仍现算,输出应相同
@@ -316,7 +325,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-14 / policy-v4 golden; identity is spelled out, not trusted."""
"""Compare with the scoring-15 / 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"}],
@@ -440,6 +449,18 @@ def test_scoring_13_golden_is_frozen_and_scores_moved() -> None:
_assert_memoization_payloads(current, previous)
def test_scoring_14_golden_is_frozen() -> None:
"""#166 batch 2 (BUG-1590): v7 stays byte-identical; the identity moved to scoring-15."""
raw = V7_GOLDEN_PATH.read_bytes()
assert hashlib.sha256(raw).hexdigest() == V7_GOLDEN_SHA256
previous = json.loads(raw)
current = json.loads(GOLDEN_PATH.read_text(encoding="utf-8"))
assert previous["source_commit"] == V7_SOURCE_COMMIT
assert previous["candidate_feature_snapshot"]["algorithm_version"] == "rectification-v5-matrix-scoring-14"
assert current["candidate_feature_snapshot"]["algorithm_version"] == CURRENT_ALGORITHM_VERSION
assert current["source_commit"] == SOURCE_COMMIT
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
@@ -24,7 +24,8 @@ class RelativeSupportScaleTest(unittest.TestCase):
# 原值: -11;新值: -12;原因: 章动修正改变同输入分数(BUG-1275),策略 v4 与相对支持口径不变。
# 原值: -12;新值: -13;原因: D30 偶数星座与白羊 / 天蝎金星功能凶改变同输入分数(BUG-1560 / BUG-1570),策略 v4 与相对支持口径不变。
# 原值: -13;新值: -14;原因: 同输入分数变了(#166 占星师裁定:Shadbala Sthana / Drik 按具名 BPHS 口径、jaimini.py Arudha 例外从初算落点数,BUG-1583~1588),策略 v4 与相对支持口径不变。
self.assertEqual(ALGORITHM_VERSION, "rectification-v5-matrix-scoring-14")
# 原值: -14;新值: -15;原因: #166 第二批(第 4 条:双子 / 处女上升土星改为混合,BUG-1590)改变同输入分数,策略 v4 不变。
self.assertEqual(ALGORITHM_VERSION, "rectification-v5-matrix-scoring-15")
self.assertEqual(RELATIVE_SUPPORT_MODE, "proportional")
def test_offset_top_two_lead_is_at_least_proportional(self) -> None: