# ---------- LinkedIn の柱と枠(2026-11-09 エイイチが採用) ---------- # LinkedIn は平日 17:00 に1日1本。曜日ごとに柱(投稿の種類)を決める。 # 月・木=課題地図、火=研究の記録、水=業界の読み解き、金=ひとりとAI(なければ業界の読み解き)。 # マンガ・英語・研究の外側の作業の記録は LinkedIn に出さない(X だけ)。docs/sns_autopost.md「LinkedIn の柱と枠」 LINKEDIN_RULE_FROM = dt.datetime(2026, 11, 10, tzinfo=JST) LINKEDIN_SLOT = (17, 0) LINKEDIN_PILLARS = {"issue": (0, 3), "research": (1,), "reading": (2, 4), "ai": (4,)} def next_linkedin_slot(schedule, pillar, after, exclude_id=None): """after より後で、その柱の曜日の 17:00 のうち、LinkedIn の予約がまだない最初の日時(JST の ISO 文字列)。""" days = LINKEDIN_PILLARS[pillar] used = {parse_time(e["post_at"]).astimezone(JST).date() for e in schedule.get("posts", []) if e.get("platform") == "linkedin" and e.get("id") != exclude_id} start = max(after, LINKEDIN_RULE_FROM).astimezone(JST) d = start.date() for _ in range(800): t = dt.datetime(d.year, d.month, d.day, *LINKEDIN_SLOT, tzinfo=JST) if t > start and d.weekday() in days and d not in used: return t.isoformat() d += dt.timedelta(days=1) raise RuntimeError("LinkedIn の空いている枠が見つかりません") def linkedin_rule_errors(schedule): errs, per_day = [], {} for e in schedule.get("posts", []): if e.get("platform") != "linkedin": continue try: t = parse_time(e["post_at"]).astimezone(JST) except (KeyError, ValueError): continue if t < LINKEDIN_RULE_FROM: continue where = f"{e['id']}(LinkedIn の決まり)" per_day.setdefault(t.date(), []).append(e["id"]) if lang_of(e) != "ja": errs.append(f"{where}: LinkedIn は日本語だけです") if os.path.basename(e.get("source", "")).startswith(("MANGA_", "SHORT_")): errs.append(f"{where}: マンガとショートは LinkedIn に出しません") p = e.get("pillar") if p not in LINKEDIN_PILLARS: errs.append(f"{where}: pillar(issue・research・reading・ai のどれか)がありません") continue if (t.hour, t.minute) != LINKEDIN_SLOT or t.weekday() not in LINKEDIN_PILLARS[p]: errs.append(f"{where}: {p} の枠({'・'.join('月火水木金土日'[w] for w in LINKEDIN_PILLARS[p])}曜 17:00)ではありません: {e['post_at']}") for d, ids in sorted(per_day.items()): if len(ids) > 1: errs.append(f"{d}: LinkedIn が1日に{len(ids)}本あります(1日1本まで): {', '.join(ids)}") return errs def check_schedule(schedule): errs = linkedin_rule_errors(schedule) ids = set() for i, e in enumerate(schedule.get("posts", [])): where = f"posts[{i}] {e.get('id', '(id なし)')}" for k in ("id", "source", "platform", "post_at"): if k not in e: errs.append(f"{where}: {k} がありません") if any(k not in e for k in ("id", "source", "platform", "post_at")): continue if e["id"] in ids: errs.append(f"{where}: id が重複しています") ids.add(e["id"]) try: parse_time(e["post_at"]) except ValueError as ex: errs.append(f"{where}: {ex}") if lang_of(e) not in LANGS: errs.append(f"{where}: lang は ja か en です") continue if e["platform"] not in PLATFORMS: errs.append(f"{where}: platform は x か linkedin です") continue try: src = load_source(e) except (OSError, json.JSONDecodeError) as ex: errs.append(f"{where}: 投稿文のファイルが読めません({ex})") continue errs += [f"{where}: {m}" for m in validate(e, src)] return errs