{"record":{"id":"71e6ffc2cc3e3477","repo":"sansan0/TrendRadar","slug":"period-period-key-start-end-period","errorCode":null,"errorMessage":"period '{period_key}' 的 start 与 end 不能相同: {period['start']}","messagePattern":"period '(.+?)' 的 start 与 end 不能相同: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"trendradar/core/scheduler.py","lineNumber":354,"sourceCode":"\n        # period 引用完整性\n        for plan_key, plan in timeline[\"day_plans\"].items():\n            for period_key in plan.get(\"periods\", []):\n                if period_key not in timeline[\"periods\"]:\n                    raise ValueError(\n                        f\"day_plan[{plan_key}] 引用了不存在的 period: {period_key}\"\n                    )\n\n        # 时间格式校验\n        for period_key, period in timeline[\"periods\"].items():\n            if \"start\" not in period or \"end\" not in period:\n                raise ValueError(\n                    f\"period '{period_key}' 缺少 start 或 end 字段\"\n                )\n            self._validate_hhmm(period[\"start\"], f\"{period_key}.start\")\n            self._validate_hhmm(period[\"end\"], f\"{period_key}.end\")\n            if period[\"start\"] == period[\"end\"]:\n                raise ValueError(\n                    f\"period '{period_key}' 的 start 与 end 不能相同: {period['start']}\"\n                )\n\n        # 检查冲突策略下的重叠\n        policy = timeline.get(\"overlap\", {}).get(\"policy\", \"error_on_overlap\")\n        if policy == \"error_on_overlap\":\n            self._check_period_overlaps(timeline)\n\n    def _check_period_overlaps(self, timeline: Dict[str, Any]) -> None:\n        \"\"\"\n        检查每个日计划中的时间段是否存在重叠\n\n        仅在 overlap.policy == \"error_on_overlap\" 时调用\n        \"\"\"\n        periods = timeline.get(\"periods\", {})\n\n        for plan_key, plan in timeline[\"day_plans\"].items():\n            period_keys = plan.get(\"periods\", [])","sourceCodeStart":336,"sourceCodeEnd":372,"githubUrl":"https://github.com/sansan0/TrendRadar/blob/8ee26026ba6c11dec41a95fb3895a7162876caa1/trendradar/core/scheduler.py#L336-L372","documentation":"Raised by _check_period_overlaps during startup validation when two periods within the same day_plan have overlapping time ranges and overlap.policy is 'error_on_overlap' (default). This is the eager, whole-timeline version of the runtime conflict error at scheduler.py:220: every day plan's periods are pairwise-checked (cross-midnight ranges supported via minute conversion) before the scheduler ever runs.","triggerScenarios":"A day_plan listing morning 08:00-12:00 and noon 11:30-13:30; overlap.policy unset or 'error_on_overlap'. Triggered at validation time regardless of current clock, unlike the runtime variant.","commonSituations":"Same as the runtime overlap case: hand-edited boundaries, periods copied between plans, cross-midnight spans overlapping morning periods (e.g. 22:00-02:00 vs 01:00-03:00).","solutions":["Make the listed ranges disjoint (adjust start/end so no pair overlaps).","If overlap is intended with the later period winning, set overlap: {policy: last_wins} — validation is then skipped and the runtime path logs warnings instead.","Audit every day_plan, not just one: the check iterates all plans and all period pairs."],"exampleFix":"# before (policy default)\nday_plans:\n  work: {periods: [morning, noon]}\nperiods:\n  morning: {start: \"08:00\", end: \"12:00\"}\n  noon:    {start: \"11:30\", end: \"13:30\"}\n# after\nperiods:\n  morning: {start: \"08:00\", end: \"11:30\"}\n  noon:    {start: \"11:30\", end: \"13:30\"}\n# or add: overlap: {policy: last_wins}","handlingStrategy":"validation","validationCode":"def to_min(t): return int(t[:2]) * 60 + int(t[3:])\n\ndef overlaps(s1, e1, s2, e2):\n    a1, b1, a2, b2 = to_min(s1), to_min(e1), to_min(s2), to_min(e2)\n    if a1 == b1 or a2 == b2:\n        return False\n    spans = []\n    for a, b in ((a1, b1), (a2, b2)):\n        if a < b:\n            spans.append((a, b))\n        else:  # cross-midnight\n            spans.append((a, 24 * 60))\n            spans.append((0, b))\n    # naive pairwise check on split intervals is enough for a lint\n    flat = sorted(spans)\n    for i in range(1, len(flat)):\n        if flat[i][0] < flat[i - 1][1]:\n            return True\n    return False\n\npolicy = timeline.get(\"overlap\", {}).get(\"policy\", \"error_on_overlap\")\nif policy == \"error_on_overlap\":\n    for plan_key, plan in timeline[\"day_plans\"].items():\n        keys = plan.get(\"periods\", [])\n        for i in range(len(keys)):\n            for j in range(i + 1, len(keys)):\n                pi, pj = timeline[\"periods\"][keys[i]], timeline[\"periods\"][keys[j]]\n                if overlaps(pi[\"start\"], pi[\"end\"], pj[\"start\"], pj[\"end\"]):\n                    raise ValueError(f\"{plan_key}: {keys[i]} overlaps {keys[j]}\")","typeGuard":null,"tryCatchPattern":"try:\n    scheduler = Scheduler(schedule_config, timeline_data)\nexcept ValueError as e:\n    if \"存在重叠\" in str(e):\n        raise SystemExit(f\"timeline has overlapping periods: {e}\")\n    raise","preventionTips":["Run an overlap lint on every timeline edit (or in CI).","Declare overlap.policy: last_wins explicitly when overlap is intended."],"tags":["scheduler","config","overlap","validation"],"backgroundTag":null,"analyzedSha":"8ee26026ba6c11dec41a95fb3895a7162876caa1","analyzedAt":"2026-08-15T01:42:18.084Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}