Daily notes link up to week and month, both in the steward's own formats
The steward asked for links between the week's daily notes, weekly and monthly ids, and rollups that show progress. Three of those already existed in the vault and had lapsed: 01. Weekly/2025-W15..W21 (7 notes, ISO YYYY-Www), 02. Monthly/ 2025.05 (dots, not dashes), a quarterly, and templates for all of them. The old daily template already carried week: gggg-[W]WW and month: YYYY-MM fields. So nothing here is invented. The hook now writes his week/month frontmatter and creates the week and month notes in his folders, in his filename formats. LINK UPWARD ONLY, and this is the load-bearing design decision. An unresolved- link census of the vault the same day found 4,799 unresolved targets across 1,366 distinct names, with 56% of notes carrying no link in or out. A nav line with a "tomorrow" link manufactures ~365 more unresolved links a year, and a weekly note hard-linking its seven days manufactures ~5 a week for days that never happened. So: the week note always resolves because the hook creates it; Previous is written only when yesterday's note actually exists on disk; tomorrow is never written; and the weekly note hard-links no dailies at all — the dailies link up, so Obsidian's backlinks pane lists exactly the days that exist. Two controls carry that: Previous appears when yesterday exists, and is absent in a fresh directory — proving the check reads the disk rather than the calendar. The weekly note carries a Work heading and keeps a Personal review heading. The 2025 weekly practice was personal — intentions, patterns, task migration — and the work rollup sits beside it under its own heading rather than displacing it, which is the same instruction the steward gave for the daily note. 17 selftest checks, up from 7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQKeKY9T9d95KpvHwwok8T
This commit is contained in:
co-authored by
Claude Opus 5
parent
073ef8a482
commit
7226e0d806
+162
-7
@@ -19,11 +19,27 @@ import datetime, os, sys
|
|||||||
VAULT = os.path.expanduser(
|
VAULT = os.path.expanduser(
|
||||||
"~/Library/Mobile Documents/iCloud~md~obsidian/Documents/David, root-and-branch")
|
"~/Library/Mobile Documents/iCloud~md~obsidian/Documents/David, root-and-branch")
|
||||||
DAILY = os.path.join(VAULT, "01. Daily")
|
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 = """---
|
SKELETON = """---
|
||||||
title: "{date} — {dayname}"
|
title: "{date} — {dayname}"
|
||||||
type: daily
|
type: daily
|
||||||
date: {date}
|
date: {date}
|
||||||
|
week: {week}
|
||||||
|
month: {month}
|
||||||
status: active
|
status: active
|
||||||
tags:
|
tags:
|
||||||
- daily
|
- daily
|
||||||
@@ -32,6 +48,8 @@ tags:
|
|||||||
|
|
||||||
# {date} — {dayname}
|
# {date} — {dayname}
|
||||||
|
|
||||||
|
**{nav}**
|
||||||
|
|
||||||
*A plain-language record of the day's work, so the history is readable without reading `git log`.
|
*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.*
|
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
|
return "no-dir", p
|
||||||
if os.path.exists(p):
|
if os.path.exists(p):
|
||||||
return "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.
|
# x-mode: refuse to clobber if something raced us between the check and the write.
|
||||||
with open(p, "x", encoding="utf-8") as fh:
|
with open(p, "x", encoding="utf-8") as fh:
|
||||||
fh.write(body)
|
fh.write(body)
|
||||||
@@ -73,6 +100,88 @@ def ensure(day=None, daily_dir=DAILY):
|
|||||||
return f"error:{e.__class__.__name__}", p
|
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():
|
def selftest():
|
||||||
import tempfile, shutil
|
import tempfile, shutil
|
||||||
ok = fail = 0
|
ok = fail = 0
|
||||||
@@ -99,6 +208,47 @@ def selftest():
|
|||||||
st2 == "exists" and open(p, encoding="utf-8").read() == "PROSE THE EXECUTOR WROTE\n")
|
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")
|
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")
|
missing = os.path.join(d, "nope")
|
||||||
st3, _ = ensure(day, missing)
|
st3, _ = ensure(day, missing)
|
||||||
chk("reports a missing directory instead of creating one [no silent mkdir]",
|
chk("reports a missing directory instead of creating one [no silent mkdir]",
|
||||||
@@ -118,11 +268,16 @@ def selftest():
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if "--selftest" in sys.argv:
|
if "--selftest" in sys.argv:
|
||||||
sys.exit(selftest())
|
sys.exit(selftest())
|
||||||
status, path = ensure()
|
made, failed = [], []
|
||||||
if status == "created":
|
for st, pth in (ensure(), rollup(datetime.date.today(), "week"),
|
||||||
print(f"DAYBOOK — created {os.path.basename(path)}")
|
rollup(datetime.date.today(), "month")):
|
||||||
elif status.startswith("error") or status == "no-dir":
|
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.
|
# Loud on purpose. The predecessor's silence cost five months.
|
||||||
print(f"DAYBOOK — COULD NOT WRITE the day's note ({status}). "
|
print("DAYBOOK — COULD NOT WRITE: " + "; ".join(failed))
|
||||||
f"Expected at: {path}")
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user