1818from pathlib import Path
1919
2020_REPO_ROOT = Path (__file__ ).resolve ().parents [1 ]
21- _PLANS_DIR = _REPO_ROOT / " docs" / " plans"
21+ _PLANS_DIR = _REPO_ROOT / ' docs' / ' plans'
2222
2323# Patterns for date markers
2424_DATE_LAST_UPDATED = re .compile (
25- r"Last (?:updated|reviewed):\s*(\d{4}-\d{2}-\d{2})" , re .IGNORECASE
26- )
27- _DATE_AS_OF = re .compile (
28- r"#\s+(?:As of|)\s*(\d{4}-\d{2}-\d{2})" , re .IGNORECASE
29- )
30- _DATE_TITLE = re .compile (
31- r"^#\s+.*?(\d{4}-\d{2}-\d{2})" , re .IGNORECASE
25+ r'Last (?:updated|reviewed):\s*(\d{4}-\d{2}-\d{2})' , re .IGNORECASE
3226)
27+ _DATE_AS_OF = re .compile (r'#\s+(?:As of|)\s*(\d{4}-\d{2}-\d{2})' , re .IGNORECASE )
28+ _DATE_TITLE = re .compile (r'^#\s+.*?(\d{4}-\d{2}-\d{2})' , re .IGNORECASE )
3329_DATE_FRONTMATTER = re .compile (
34- r"last_(?:audit|reviewed|updated):\s*(\d{4}-\d{2}-\d{2})" , re .IGNORECASE
35- )
36- _DATE_INLINE = re .compile (
37- r"^Date:\s*(\d{4}-\d{2}-\d{2})" , re .IGNORECASE | re .MULTILINE
38- )
39- _DATE_IN_FILENAME = re .compile (
40- r"(\d{4}-\d{2}-\d{2})\.md$"
30+ r'last_(?:audit|reviewed|updated):\s*(\d{4}-\d{2}-\d{2})' , re .IGNORECASE
4131)
32+ _DATE_INLINE = re .compile (r'^Date:\s*(\d{4}-\d{2}-\d{2})' , re .IGNORECASE | re .MULTILINE )
33+ _DATE_IN_FILENAME = re .compile (r'(\d{4}-\d{2}-\d{2})\.md$' )
4234_SUPERSESSION_NOTE = re .compile (
43- r"Supersession note[,:]?\s*(\d{4}-\d{2}-\d{2})" , re .IGNORECASE
44- )
45- _SUPERSEDED_KEYWORD = re .compile (
46- r"\b[sS]upersed(?:ed|es)\b"
47- )
48- _HISTORICAL_KEYWORD = re .compile (
49- r"\bhistorical(?: evidence|)\b" , re .IGNORECASE
35+ r'Supersession note[,:]?\s*(\d{4}-\d{2}-\d{2})' , re .IGNORECASE
5036)
37+ _SUPERSEDED_KEYWORD = re .compile (r'\b[sS]upersed(?:ed|es)\b' )
38+ _HISTORICAL_KEYWORD = re .compile (r'\bhistorical(?: evidence|)\b' , re .IGNORECASE )
5139
5240STALE_DAYS = 90
5341
@@ -56,10 +44,17 @@ def _extract_date(text: str) -> datetime | None:
5644 """Extract the most recent date from text using known patterns."""
5745 dates : list [datetime ] = []
5846
59- for pattern in (_DATE_LAST_UPDATED , _DATE_AS_OF , _DATE_TITLE , _DATE_FRONTMATTER , _DATE_INLINE , _SUPERSESSION_NOTE ):
47+ for pattern in (
48+ _DATE_LAST_UPDATED ,
49+ _DATE_AS_OF ,
50+ _DATE_TITLE ,
51+ _DATE_FRONTMATTER ,
52+ _DATE_INLINE ,
53+ _SUPERSESSION_NOTE ,
54+ ):
6055 for match in pattern .finditer (text ):
6156 try :
62- dt = datetime .strptime (match .group (1 ), " %Y-%m-%d" ).replace (
57+ dt = datetime .strptime (match .group (1 ), ' %Y-%m-%d' ).replace (
6358 tzinfo = timezone .utc
6459 )
6560 dates .append (dt )
@@ -87,18 +82,18 @@ def scan_plans(
8782 now = datetime .now (timezone .utc )
8883 cutoff = now - timedelta (days = stale_days )
8984
90- md_files = sorted (plans_dir .rglob (" *.md" ))
85+ md_files = sorted (plans_dir .rglob (' *.md' ))
9186
9287 for path in md_files :
9388 rel = path .relative_to (_REPO_ROOT )
9489 try :
95- text = path .read_text (encoding = " utf-8" )
90+ text = path .read_text (encoding = ' utf-8' )
9691 except Exception :
97- issues .append (f" { rel } : cannot read file" )
92+ issues .append (f' { rel } : cannot read file' )
9893 continue
9994
10095 # Skip ticket-plans directory (task execution ledgers, not strategic plans)
101- if " ticket-plans" in str (path ):
96+ if ' ticket-plans' in str (path ):
10297 continue
10398
10499 date = _extract_date (text )
@@ -107,7 +102,7 @@ def scan_plans(
107102 fn_match = _DATE_IN_FILENAME .search (path .name )
108103 if fn_match :
109104 with suppress (ValueError ):
110- date = datetime .strptime (fn_match .group (1 ), " %Y-%m-%d" ).replace (
105+ date = datetime .strptime (fn_match .group (1 ), ' %Y-%m-%d' ).replace (
111106 tzinfo = timezone .utc
112107 )
113108 has_supersession = _has_supersession_note (text )
@@ -116,9 +111,9 @@ def scan_plans(
116111 if date is None and not has_supersession and not is_historical :
117112 # Undated file
118113 issues .append (
119- f" { rel } : no date marker found (Last updated/reviewed, "
120- f" supersession note, or historical label). Add a date or "
121- f" mark as superseded."
114+ f' { rel } : no date marker found (Last updated/reviewed, '
115+ f' supersession note, or historical label). Add a date or '
116+ f' mark as superseded.'
122117 )
123118 elif date is not None and date < cutoff :
124119 # Stale file
@@ -130,44 +125,42 @@ def scan_plans(
130125 pass
131126 else :
132127 issues .append (
133- f" { rel } : last updated { date .strftime (' %Y-%m-%d' )} "
134- f" ({ (now - date ).days } days ago, threshold { stale_days } d). "
135- f" Add a supersession note or update the document."
128+ f' { rel } : last updated { date .strftime (" %Y-%m-%d" )} '
129+ f' ({ (now - date ).days } days ago, threshold { stale_days } d). '
130+ f' Add a supersession note or update the document.'
136131 )
137132
138133 if check_review_dates and date and date < (now - timedelta (days = 180 )):
139134 issues .append (
140- f" { rel } : review date { date .strftime (' %Y-%m-%d' )} is "
141- f" older than 180 days. Refresh or add supersession note."
135+ f' { rel } : review date { date .strftime (" %Y-%m-%d" )} is '
136+ f' older than 180 days. Refresh or add supersession note.'
142137 )
143138
144139 return issues
145140
146141
147142def main () -> int :
148- parser = argparse .ArgumentParser (
149- description = "Detect stale plans in docs/plans/"
150- )
143+ parser = argparse .ArgumentParser (description = 'Detect stale plans in docs/plans/' )
151144 parser .add_argument (
152- " --plans-dir" ,
145+ ' --plans-dir' ,
153146 default = str (_PLANS_DIR ),
154- help = " Directory to scan for plan files" ,
147+ help = ' Directory to scan for plan files' ,
155148 )
156149 parser .add_argument (
157- " --stale-days" ,
150+ ' --stale-days' ,
158151 type = int ,
159152 default = STALE_DAYS ,
160- help = f" Days after which a plan is considered stale (default: { STALE_DAYS } )" ,
153+ help = f' Days after which a plan is considered stale (default: { STALE_DAYS } )' ,
161154 )
162155 parser .add_argument (
163- " --check-review-dates" ,
164- action = " store_true" ,
165- help = " Also flag files with review dates older than 180 days" ,
156+ ' --check-review-dates' ,
157+ action = ' store_true' ,
158+ help = ' Also flag files with review dates older than 180 days' ,
166159 )
167160 parser .add_argument (
168- " --verbose" ,
169- action = " store_true" ,
170- help = " Show informational messages for superseded-but-stale files" ,
161+ ' --verbose' ,
162+ action = ' store_true' ,
163+ help = ' Show informational messages for superseded-but-stale files' ,
171164 )
172165
173166 args = parser .parse_args ()
@@ -179,15 +172,15 @@ def main() -> int:
179172 )
180173
181174 if not issues :
182- print (" No stale plans detected." )
175+ print (' No stale plans detected.' )
183176 return 0
184177
185178 for issue in issues :
186- print (f" STALE: { issue } " )
179+ print (f' STALE: { issue } ' )
187180
188- print (f" \n { len (issues )} stale plan(s) detected." )
181+ print (f' \n { len (issues )} stale plan(s) detected.' )
189182 return 1
190183
191184
192- if __name__ == " __main__" :
185+ if __name__ == ' __main__' :
193186 raise SystemExit (main ())
0 commit comments