#!/usr/bin/env python3 # -*- coding: utf-8 -*- """List the PDF library against its ledger. python tools/library.py Stage 1 of `knowledge/library/RUNBOOK.md`, and nothing more. It answers three questions and refuses to grow a fourth: what is in the drop folder that the ledger has never seen, what has changed under a name the ledger already knows, and how many documents are sitting in each status. The identity is the CONTENT HASH, not the filename. A file that moved is the same document; a second copy of a hash already listed is a no-op, which is what makes a re-run cheap -- and it has already saved one 929-page manual from being extracted twice. This prints filenames from a gitignored folder, so its OUTPUT can carry a producer's name even though this file never does. Do not paste a line of it into a tracked file; `CLAUDE.md` section 3 has the placeholders. Host CPython. The pipeline is discipline, not machinery: if this ever needs a second screen of code, delete the addition and follow the runbook by hand. """ from __future__ import annotations import hashlib import os import re import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) LIBRARY = os.path.join(ROOT, 'knowledge', 'library') LEDGER = os.path.join(LIBRARY, 'LEDGER.md') # A ledger index row: | id | `sha256` | `file` | kind | pages | status | _ROW = re.compile( r'^\|[^|]*\|\s*`([0-9a-f]{64})`\s*\|\s*`([^`]+)`\s*\|' r'([^|]*)\|([^|]*)\|([^|]*)\|') def sha256(path: str) -> str: """Streamed, because the manual is 122 MB and a read() of it is not free.""" h = hashlib.sha256() with open(path, 'rb') as f: for block in iter(lambda: f.read(1 << 20), b''): h.update(block) return h.hexdigest() def read_ledger(): """Index rows as (hash, filename, status). Missing file is not an error.""" if not os.path.exists(LEDGER): return [] rows = [] with open(LEDGER, encoding='utf-8') as f: for line in f: m = _ROW.match(line.strip()) if m: rows.append((m.group(1), m.group(2).strip(), m.group(5).strip().strip('`'))) return rows def dropped_pdfs(): """Every PDF actually sitting in the drop folder, repo-relative.""" found = [] for base, dirs, files in os.walk(LIBRARY): dirs[:] = [d for d in dirs if not d.startswith('.')] for name in sorted(files): if name.lower().endswith('.pdf'): found.append(os.path.join(base, name)) return found def main() -> int: if not os.path.isdir(LIBRARY): print('no library folder at knowledge/library/ -- nothing to list') return 0 rows = read_ledger() by_hash = {h: (name, status) for h, name, status in rows} by_name = {} for h, name, _ in rows: by_name.setdefault(os.path.basename(name), set()).add(h) print('ledger: {0} row(s), {1} distinct document(s)' .format(len(rows), len(by_hash))) new, changed, known = [], [], 0 for path in dropped_pdfs(): rel = os.path.relpath(path, ROOT).replace('\\', '/') digest = sha256(path) if digest in by_hash: known += 1 elif os.path.basename(path) in by_name: changed.append((rel, digest)) else: new.append((rel, digest)) print('drop folder: {0} PDF(s) -- {1} already in the ledger' .format(known + len(new) + len(changed), known)) if new: print('\nNEW -- inventory these (runbook stage 1):') for rel, digest in new: print(' {0} {1}'.format(digest[:16], rel)) if changed: print('\nHASH CHANGED -- a known filename holding new content. This is ' 'a NEW document: reset its row to `inventoried` and re-extract.') for rel, digest in changed: print(' {0} {1}'.format(digest[:16], rel)) elsewhere = [n for h, n, _ in rows if not os.path.exists(os.path.join(LIBRARY, os.path.basename(n)))] if elsewhere: print('\nrecorded at another location (not in the drop folder), {0}: ' '{1}'.format(len(elsewhere), ', '.join(sorted(set(elsewhere))))) counts = {} for _, _, status in rows: counts[status] = counts.get(status, 0) + 1 if counts: print('\nstatus: ' + ' | '.join( '{0} {1}'.format(k, v) for k, v in sorted(counts.items()))) debt = counts.get('extracted', 0) if debt: print('\n{0} document(s) EXTRACTED AND NOT LANDED. That is the debt the ' 'library exists to drain, not a stage on the way to done -- ' 'runbook stage 3.'.format(debt)) return 0 if __name__ == '__main__': sys.exit(main())