diff --git a/scripts/daybook-ensure.py b/scripts/daybook-ensure.py index 4842f9e..42d2ca7 100755 --- a/scripts/daybook-ensure.py +++ b/scripts/daybook-ensure.py @@ -19,11 +19,27 @@ import datetime, os, sys VAULT = os.path.expanduser( "~/Library/Mobile Documents/iCloud~md~obsidian/Documents/David, root-and-branch") DAILY = os.path.join(VAULT, "01. Daily") +# The steward's own folders and formats, in use since 2025 — 01. Weekly/YYYY-Www.md and +# 02. Monthly/YYYY.MM.md (dots, not dashes). Reused rather than reinvented: the vault already +# holds 2025-W15..W21 and 2025.05, and [[2025-W15]]-style links already resolve against them. +WEEKLY = os.path.join(VAULT, "02. Reviews & Planning", "01. Weekly") +MONTHLY = os.path.join(VAULT, "02. Reviews & Planning", "02. Monthly") + + +def week_id(day): + y, w, _ = day.isocalendar() + return f"{y}-W{w:02d}" + + +def month_id(day): + return day.strftime("%Y.%m") SKELETON = """--- title: "{date} — {dayname}" type: daily date: {date} +week: {week} +month: {month} status: active tags: - daily @@ -32,6 +48,8 @@ tags: # {date} — {dayname} +**{nav}** + *A plain-language record of the day's work, so the history is readable without reading `git log`. Project work sits under its own heading; decisions, insights and open threads are collected at the end.* @@ -62,7 +80,16 @@ def ensure(day=None, daily_dir=DAILY): return "no-dir", p if os.path.exists(p): return "exists", p - body = SKELETON.format(date=day.isoformat(), dayname=day.strftime("%A")) + # Upward links only. A "tomorrow" link on every note manufactures ~365 unresolved + # links a year, and an unresolved-link census on 2026-08-23 found 4,799 already — + # 56% of notes isolated. The week note is created below, so [[week]] always resolves; + # yesterday is linked only if it actually exists. + nav = [f"[[{week_id(day)}|Week]]", f"[[{month_id(day)}|Month]]", "[[Daily index]]"] + prev = day - datetime.timedelta(days=1) + if os.path.exists(note_path(prev, daily_dir)): + nav.insert(0, f"[[{prev.isoformat()}|Previous]]") + body = SKELETON.format(date=day.isoformat(), dayname=day.strftime("%A"), + week=week_id(day), month=month_id(day), nav=" · ".join(nav)) # x-mode: refuse to clobber if something raced us between the check and the write. with open(p, "x", encoding="utf-8") as fh: fh.write(body) @@ -73,6 +100,88 @@ def ensure(day=None, daily_dir=DAILY): return f"error:{e.__class__.__name__}", p +WEEK_SKELETON = """--- +type: weekly +week: {week} +start_date: {start} +status: active +tags: + - weekly + - review +--- + +# Weekly Review — {week} + +*Week of {start} to {end}.* + +> The daily notes for this week link up to here, so Obsidian's **backlinks** pane below lists +> exactly the days that have one. Nothing is hard-linked downward on purpose: a link to a day that +> never happened is an unresolved link, and the vault already carries thousands of those. + +## Work — the week in review + +*Written at the end of the week, from the dailies. What moved, what was decided, what is still open. +Plain language, and shorter than the sum of its days — a week's worth of perspective, not a digest.* + +## Threads that carried across days + +*The point of the weekly view: what appeared on more than one day. A problem that kept returning, a +question that stayed open, an idea that grew. These are invisible from inside a single day.* + +## Personal review + +*The 2025 weekly practice lived here — intentions, patterns, task migration. Kept as its own +heading; the work rollup above does not displace it.* +""" + +MONTH_SKELETON = """--- +type: monthly +month: {month} +status: active +tags: + - monthly + - review +--- + +# Monthly Review — {label} + +## Work — the month in perspective + +*Written from the weeks, not from the days. The question this answers is the one no daily can: +**did the month go anywhere?** Name the two or three things that actually advanced, and what +consumed time without advancing.* + +## Threads that carried across weeks + +## Personal review +""" + + +def rollup(day, kind, weekly_dir=None, monthly_dir=None): + """-> (status, path) for the week or month note. Same create-only contract as the daily.""" + try: + if kind == "week": + d, name = (weekly_dir or WEEKLY), week_id(day) + start = day - datetime.timedelta(days=day.isoweekday() - 1) + body = WEEK_SKELETON.format(week=name, start=start.isoformat(), + end=(start + datetime.timedelta(days=6)).isoformat()) + else: + d, name = (monthly_dir or MONTHLY), month_id(day) + body = MONTH_SKELETON.format(month=name, label=day.strftime("%B %Y")) + if not os.path.isdir(d): + return "no-dir", os.path.join(d, name + ".md") + p = os.path.join(d, name + ".md") + if os.path.exists(p): + return "exists", p + with open(p, "x", encoding="utf-8") as fh: + fh.write(body) + return "created", p + except FileExistsError: + return "exists", p + except OSError as e: + return f"error:{e.__class__.__name__}", os.path.join(d, name + ".md") + + def selftest(): import tempfile, shutil ok = fail = 0 @@ -99,6 +208,47 @@ def selftest(): st2 == "exists" and open(p, encoding="utf-8").read() == "PROSE THE EXECUTOR WROTE\n") chk("reports 'exists' rather than 'created' on the second run", st2 == "exists") + # --- upward-link controls. The census that motivated them: 4,799 unresolved link + # targets, 56% of notes isolated. A nav line that manufactures links is the disease. + day2 = datetime.date(2026, 8, 24) # the day AFTER one that now exists + st5, p5 = ensure(day2, d) + txt2 = open(p5, encoding="utf-8").read() + chk("links Previous when yesterday's note EXISTS", "[[2026-08-23|Previous]]" in txt2) + chk("never links tomorrow [no manufactured unresolved link]", "2026-08-25" not in txt2) + chk("links up to the week and month, which the hook creates", + "[[2026-W35|Week]]" in txt2 and "[[2026.08|Month]]" in txt2) + chk("carries the steward's own week/month frontmatter vocabulary", + "week: 2026-W35" in txt2 and "month: 2026.08" in txt2) + + d3 = tempfile.mkdtemp() + st6, p6 = ensure(datetime.date(2026, 9, 1), d3) # no previous note in a fresh dir + chk("OMITS Previous when yesterday's note does NOT exist [negative control — proves" + " the Previous check reads the disk, not the calendar]", + "Previous" not in open(p6, encoding="utf-8").read()) + shutil.rmtree(d3, ignore_errors=True) + + # --- rollups: same create-only contract, the steward's own folders and formats + wd, md = tempfile.mkdtemp(), tempfile.mkdtemp() + try: + stw, pw = rollup(day, "week", wd, md) + chk("creates the weekly note as YYYY-Www.md [his 2025 format]", + stw == "created" and os.path.basename(pw) == "2026-W34.md") + stm, pm = rollup(day, "month", wd, md) + chk("creates the monthly note as YYYY.MM.md [dots, as his 2025.05]", + stm == "created" and os.path.basename(pm) == "2026.08.md") + wtxt = open(pw, encoding="utf-8").read() + chk("weekly carries a Work heading AND keeps his personal-review heading", + "## Work — the week in review" in wtxt and "## Personal review" in wtxt) + chk("weekly hard-links NO daily notes [backlinks instead; zero manufactured links]", + "2026-08-2" not in wtxt.split("start_date")[1].split("## Personal")[0] + .replace("2026-08-23", "").replace("2026-08-17", "")) + open(pw, "w", encoding="utf-8").write("STEWARD PROSE\n") + stw2, _ = rollup(day, "week", wd, md) + chk("does NOT overwrite an existing weekly note", + stw2 == "exists" and open(pw, encoding="utf-8").read() == "STEWARD PROSE\n") + finally: + shutil.rmtree(wd, ignore_errors=True); shutil.rmtree(md, ignore_errors=True) + missing = os.path.join(d, "nope") st3, _ = ensure(day, missing) chk("reports a missing directory instead of creating one [no silent mkdir]", @@ -118,11 +268,16 @@ def selftest(): if __name__ == "__main__": if "--selftest" in sys.argv: sys.exit(selftest()) - status, path = ensure() - if status == "created": - print(f"DAYBOOK — created {os.path.basename(path)}") - elif status.startswith("error") or status == "no-dir": + made, failed = [], [] + for st, pth in (ensure(), rollup(datetime.date.today(), "week"), + rollup(datetime.date.today(), "month")): + if st == "created": + made.append(os.path.basename(pth)) + elif st != "exists": + failed.append(f"{os.path.basename(pth)} ({st})") + if made: + print("DAYBOOK — created " + ", ".join(made)) + if failed: # Loud on purpose. The predecessor's silence cost five months. - print(f"DAYBOOK — COULD NOT WRITE the day's note ({status}). " - f"Expected at: {path}") + print("DAYBOOK — COULD NOT WRITE: " + "; ".join(failed)) sys.exit(0)