feat(rectification): range delivery needs 4 dated events across 3 domains (R1, BUG-1193)

Product decision 2026-10-02 (TASK-upstream-sync5 R1): a time range is offered
only with at least 4 dated, primary-scoreable events covering 3 domains,
counted on all of them (training + reserved holdout). Was 3 training events /
2 domains in three TS copies and the Python acceptance gate while the policy
file already said 4/3.

- One definition: references/rectification_policy.v1.json
  (minConfirmationEvents / minConfirmationDomains). TS core/types MIN_DATED_*,
  rectification-decision MIN_STANDALONE_*, evidence-model MIN_ACCEPTANCE_*,
  the convergence evaluator and the post-inference trainingGateOpen all read
  it; Python decision_policy MIN_ACCEPTANCE_* alias MIN_CONFIRMATION_*.
- Python receipt counts all scoreable events / domains for event_quality and
  domain_diversity; decision policy identity v3 -> v4 (candidate UUIDs carry
  it). Candidate scores unchanged (77 v5 cases A/B identical), so the
  algorithm stays rectification-v5-matrix-scoring-10.
- Memoization golden v3 written by write_golden; v2 frozen by sha256 with a
  test that its scores equal v3 and only the receipt policy moved.
- Collect gap copy names the exact gap ("再来两件……其中至少一件不是……")
  instead of always "再来一件"; VOICE.md updated. Legacy life-events form copy
  4/3 as well.
- 30 frontend test files, 4 Python tests: fixtures extended to the same
  scenario at 4/3, or assertions changed with 原值/新值/原因 notes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-10-03 00:08:00 +08:00
co-authored by Claude Opus 5.5
parent 7fb165ecf6
commit 758fee954b
37 changed files with 2270 additions and 97 deletions
+40 -5
View File
@@ -20,6 +20,16 @@ input (candidate 12:00: 8.6274 -> 8.4977), which is why the algorithm identity
was bumped; it already carries the dated-v1 receipt metadata. Never rewrite it
in place: a future scoring change adds a v3 file and freezes this one.
Current golden tests/golden/rectification_engine_memoization_v3.json
(2026-10-02, BUG-1193, upstream sync 5) was written by ``write_golden`` after
R1 raised the range floor to 4 dated events / 3 domains. Candidate scores are
identical to v2 (the ported combustion / Chara Karaka / Yogini / Shodhana
changes do not feed the event matrix; 77 v5 cases A/B byte-identical), so the
algorithm stays scoring-10. Only the decision receipt moved: policy
rectification-candidate-policy-v3 -> v4, event_quality / domain_diversity
minimum 3/2 -> 4/3, and the representative candidate UUID (its namespace
carries the policy version). v2 is frozen by sha256 below.
Do not compare that payload with a whole-structure ``==``. Cross-machine
libm / pyswisseph rounding already drifted ``margin_percent`` by 1.1e-3
(TASK-rectification-engine-memoization-fix-20260915). Equivalence of the
@@ -58,15 +68,20 @@ import scripts.rectification.scoring_service as scoring_service
import shadbala
ROOT = Path(__file__).resolve().parents[1]
GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v2.json"
GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v3.json"
PREVIOUS_GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v2.json"
PREVIOUS_GOLDEN_SHA256 = "abb15efae4d86387d930cd463b82a22fe95ccd1779e85ca6cc4cdf07a063cb73"
PREVIOUS_SOURCE_COMMIT = "57782aea8ae28f5dd165a08d221ec27dbef391f8"
HISTORICAL_GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v1.json"
HISTORICAL_GOLDEN_SHA256 = "6266e448d7dad204b264cbe8f9bcf4c36e02286794764a94edb9e2ce4d0e3015"
CURRENT_ALGORITHM_VERSION = "rectification-v5-matrix-scoring-10"
FROZEN_TODAY = date(2026, 9, 16)
TIMING_KEYS = frozenset({"column_compare_ms"})
HISTORICAL_SOURCE_COMMIT = "a8d29d1b6cc37ff865ddec6c8bccdf9aa889ee53"
# Engine code of the v2 golden; the scoring-10 label is the BUG-1181 commit on top.
SOURCE_COMMIT = "57782aea8ae28f5dd165a08d221ec27dbef391f8"
CURRENT_POLICY_VERSION = "rectification-candidate-policy-v4"
# Engine code of the v3 golden: upstream-sync5 tip before R1; the policy-v4
# receipt (R1, BUG-1193) is the commit that adds the v3 file on top.
SOURCE_COMMIT = "7fb165ecf66ba1726f2fc74ffa2353fadc65ba39"
CACHE_LAYER_KEYS = (
"ashtakavarga_result",
"shadbala_result",
@@ -250,7 +265,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-10 golden; identity is spelled out, not trusted."""
"""Compare with the scoring-10 / 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"}],
@@ -276,7 +291,7 @@ def _assert_dated_payload_against_golden(actual: dict[str, Any], expected: dict[
).encode()).hexdigest()
result_id = uuid5(NAMESPACE_URL, f"{CURRENT_ALGORITHM_VERSION}:{fingerprint}")
representative_time = golden_receipt["representative_time"]
candidate_id = uuid5(NAMESPACE_URL, f"rectification-candidate-policy-v3:{result_id}:{representative_time}")
candidate_id = uuid5(NAMESPACE_URL, f"{CURRENT_POLICY_VERSION}:{result_id}:{representative_time}")
assert golden_receipt["representative_candidate_id"] == str(candidate_id)
assert receipt["representative_candidate_id"] == str(candidate_id)
# BUG-733/985: fingerprints hash unrounded floats; do not turn the
@@ -297,6 +312,26 @@ def test_historical_scoring_8_golden_is_frozen_and_no_longer_current() -> None:
_assert_memoization_payloads(current, historical)
def test_previous_policy_v3_golden_is_frozen_with_identical_scores() -> None:
"""BUG-1193: v2 stays byte-identical; scores equal v3 (no scoring-11), only the receipt policy moved."""
raw = PREVIOUS_GOLDEN_PATH.read_bytes()
assert hashlib.sha256(raw).hexdigest() == PREVIOUS_GOLDEN_SHA256
previous = json.loads(raw)
current = json.loads(GOLDEN_PATH.read_text(encoding="utf-8"))
assert previous["source_commit"] == PREVIOUS_SOURCE_COMMIT
assert previous["candidate_feature_snapshot"]["algorithm_version"] == CURRENT_ALGORITHM_VERSION
assert current["candidate_feature_snapshot"]["algorithm_version"] == CURRENT_ALGORITHM_VERSION
_assert_tiered_equal(current["candidate_scores"], previous["candidate_scores"], path="candidate_scores")
assert previous["decision_receipt"]["policy_version"] == "rectification-candidate-policy-v3"
assert current["decision_receipt"]["policy_version"] == CURRENT_POLICY_VERSION
assert previous["decision_receipt"]["gates"]["event_quality"]["minimum"] == 3
assert current["decision_receipt"]["gates"]["event_quality"]["minimum"] == 4
assert previous["decision_receipt"]["gates"]["domain_diversity"]["minimum"] == 2
assert current["decision_receipt"]["gates"]["domain_diversity"]["minimum"] == 3
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))