fix(rectification): anchor candidate windows to civil dates across midnight
Carry explicit local date intervals instead of inferring the day from clock order. Cluster width, delivery, adoption, and reports keep the actual civil date; adopted date is stored separately from the reported birth_date. Algorithm identity is scoring-9 / spec-v5. Scoring weights, confirmation thresholds, and Skill version are unchanged. Isolated Linux final-3 gates passed; four pre-existing Python failures remain. This is not a production release.
This commit is contained in:
@@ -98,8 +98,12 @@ def _features(built: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
continue
|
||||
time = _feature_time(feature)
|
||||
if time:
|
||||
rows.append(feature)
|
||||
rows.sort(key=lambda item: _clock(str(_feature_time(item))))
|
||||
if "candidate_intervals" in built:
|
||||
position = {key: context[key] for key in ("candidate_date", "window_index", "window_offset_minutes", "segment_index")}
|
||||
rows.append({**feature, **position})
|
||||
else:
|
||||
rows.append(feature)
|
||||
rows.sort(key=lambda item: item.get("window_index", _clock(str(_feature_time(item)))))
|
||||
return rows
|
||||
|
||||
|
||||
@@ -224,27 +228,43 @@ def window_scan(
|
||||
counts: dict[str, set[int]] = {layer: set() for layer in _LAYER_LABEL}
|
||||
transitions: list[dict[str, Any]] = []
|
||||
previous: dict[str, int | None] | None = None
|
||||
for feature in _features(built):
|
||||
previous_segment: int | None = None
|
||||
features = _features(built)
|
||||
if start_minute is not None and end_minute is not None:
|
||||
features = [feature for feature in features
|
||||
if start_minute <= _clock(str(_feature_time(feature))) <= end_minute]
|
||||
segment_bounds: dict[int, tuple[str, str]] = {}
|
||||
for feature in features:
|
||||
if "window_index" in feature:
|
||||
segment = feature["segment_index"]
|
||||
stamp = f'{feature["candidate_date"]}T{_feature_time(feature)}'
|
||||
segment_bounds[segment] = (segment_bounds.get(segment, (stamp, stamp))[0], stamp)
|
||||
for feature in features:
|
||||
time = _feature_time(feature)
|
||||
if time and start_minute is not None and end_minute is not None:
|
||||
clock = _clock(time)
|
||||
if clock < start_minute or clock > end_minute:
|
||||
continue
|
||||
current = {layer: _scan_layer_value(feature, layer) for layer in _LAYER_LABEL}
|
||||
for layer, bucket in counts.items():
|
||||
value = current[layer]
|
||||
if isinstance(value, int):
|
||||
bucket.add(value)
|
||||
if previous and time:
|
||||
dated = "window_index" in feature
|
||||
segment_start = dated and feature["segment_index"] != previous_segment
|
||||
if (previous or segment_start) and time:
|
||||
for layer, label in _LAYER_LABEL.items():
|
||||
before = previous[layer]
|
||||
after = current[layer]
|
||||
if isinstance(before, int) and isinstance(after, int) and before != after:
|
||||
before = after if segment_start else previous[layer]
|
||||
if isinstance(before, int) and isinstance(after, int) and (before != after or segment_start):
|
||||
row = {
|
||||
"layer": layer,
|
||||
"at": time,
|
||||
"user_meaning": f"{label} 在 {time} 发生变化",
|
||||
}
|
||||
if dated:
|
||||
row.update({key: feature[key] for key in ("candidate_date", "window_index", "window_offset_minutes", "segment_index")})
|
||||
start_at, end_at = segment_bounds[feature["segment_index"]]
|
||||
row.update(segment_start_at=start_at, segment_end_at=end_at)
|
||||
if segment_start:
|
||||
row["segment_start"] = True
|
||||
row["user_meaning"] = f"{label} 在这一段起点的状态"
|
||||
from_sign = _sign_name(before)
|
||||
to_sign = _sign_name(after)
|
||||
if from_sign and to_sign:
|
||||
@@ -252,6 +272,7 @@ def window_scan(
|
||||
row["to_sign"] = to_sign
|
||||
transitions.append(row)
|
||||
previous = current
|
||||
previous_segment = feature.get("segment_index")
|
||||
payload: dict[str, Any] = {
|
||||
"scanned": True,
|
||||
"confirmation_allowed": False,
|
||||
@@ -472,8 +493,10 @@ def lagna_contrast(built: dict[str, Any]) -> dict[str, Any] | None:
|
||||
index = feature.get("ascendant_sign_index")
|
||||
if not time or not isinstance(index, int) or index < 0 or index > 11:
|
||||
continue
|
||||
if current and current["d1_lagna_index"] == index:
|
||||
if current and current["d1_lagna_index"] == index and current.get("segment_index") == feature.get("segment_index"):
|
||||
current["end"] = time
|
||||
if "candidate_date" in feature:
|
||||
current["end_at"] = f'{feature["candidate_date"]}T{time}'
|
||||
continue
|
||||
if current:
|
||||
intervals.append(current)
|
||||
@@ -482,6 +505,8 @@ def lagna_contrast(built: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"start": time,
|
||||
"end": time,
|
||||
"d1_lagna_index": index,
|
||||
**({"segment_index": feature["segment_index"], "start_at": f'{feature["candidate_date"]}T{time}',
|
||||
"end_at": f'{feature["candidate_date"]}T{time}'} if "candidate_date" in feature else {}),
|
||||
"lagna": sign,
|
||||
"lords": {
|
||||
"l1": _house_lord_zh(index, 1),
|
||||
@@ -495,11 +520,16 @@ def lagna_contrast(built: dict[str, Any]) -> dict[str, Any] | None:
|
||||
if len(intervals) < 2:
|
||||
return None
|
||||
left, right = intervals[0], intervals[1]
|
||||
def interval_label(interval: dict[str, Any]) -> str:
|
||||
if "start_at" in interval:
|
||||
return f"{interval['start_at'].replace('T', ' ')}–{interval['end_at'].replace('T', ' ')}"
|
||||
return f"{interval['start']}-{interval['end']}"
|
||||
|
||||
return {
|
||||
"intervals": intervals[:3],
|
||||
"user_meaning": (
|
||||
f"窗口里出现两段本命上升:{left['start']}-{left['end']} 为{left['lagna']},"
|
||||
f"{right['start']}-{right['end']} 为{right['lagna']}。"
|
||||
f"窗口里出现两段本命上升:{interval_label(left)} 为{left['lagna']},"
|
||||
f"{interval_label(right)} 为{right['lagna']}。"
|
||||
"可并列 D9/D10 类型表作校时方法,不是命运承诺,也不能确认唯一分钟。"
|
||||
),
|
||||
"unique_minute_claim": False,
|
||||
@@ -618,6 +648,24 @@ def cluster_scan(
|
||||
width_minutes: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Scan only the indistinguishable candidate cluster, not the full declared range."""
|
||||
if "candidate_intervals" in built:
|
||||
wanted = {str(value)[:5] for value in [*candidate_times, representative_time] if value}
|
||||
features = _features(built)
|
||||
selected: dict[int, list[int]] = {}
|
||||
for feature in features:
|
||||
if _feature_time(feature) in wanted:
|
||||
selected.setdefault(feature["segment_index"], []).append(feature["window_offset_minutes"])
|
||||
if not selected:
|
||||
return window_scan(built)
|
||||
bounds = {segment: (min(points), max(points)) for segment, points in selected.items()}
|
||||
if len(bounds) == 1:
|
||||
segment, (lo, hi) = next(iter(bounds.items()))
|
||||
extra = max(int(width_minutes or 0) - (hi - lo + 1), 0)
|
||||
bounds[segment] = (lo - extra // 2, hi + extra - extra // 2)
|
||||
contexts = [row for row in built.get("static_contexts") or []
|
||||
if row.get("segment_index") in bounds
|
||||
and bounds[row["segment_index"]][0] <= row["window_offset_minutes"] <= bounds[row["segment_index"]][1]]
|
||||
return window_scan({**built, "static_contexts": contexts})
|
||||
clocks: list[int] = []
|
||||
for raw in [*candidate_times, representative_time]:
|
||||
if isinstance(raw, str) and len(raw) >= 5:
|
||||
|
||||
Reference in New Issue
Block a user