"""Reviewed publications. A generated digest is not one of these records.""" from datetime import datetime from pathlib import Path from .store import all_records, get, put FIELDS = {'schema_version', 'title', 'reviewer', 'reviewed_at', 'claims', 'supersedes'} CLAIM_FIELDS = {'statement', 'disposition', 'capture_id', 'exclusion_reason'} def publish(root, value): root = Path(root) record = _record(root, value) ident = put(root, 'publications', record) _render(root, ident, record) return ident def verify(root): root = Path(root) directory = root / 'data' / 'publications' if not directory.exists(): return 0 reviewed = (root / 'digests' / 'reviewed').resolve() count = 0 for ident, record in all_records(root, 'publications'): _record(root, record, existing=ident) path = (reviewed / f'{ident}.md').resolve() if path.parent != reviewed or not path.is_file(): raise ValueError(f'reviewed publication file missing: {ident}') text = path.read_text(encoding='utf-8') if not text.startswith('# Reviewed publication\n') or ident not in text: raise ValueError(f'reviewed publication rendering mismatch: {ident}') count += 1 return count def _record(root, value, existing=None): if not isinstance(value, dict) or set(value) - FIELDS or FIELDS - {'supersedes'} - set(value): raise ValueError('publication has missing or unknown fields') if type(value['schema_version']) is not int or value['schema_version'] != 1: raise ValueError('schema_version must be 1') for key in ('title', 'reviewer'): if not isinstance(value[key], str) or not value[key].strip(): raise ValueError(f'{key} must be nonempty text') reviewed = datetime.fromisoformat(value['reviewed_at'].replace('Z', '+00:00')) if reviewed.tzinfo is None: raise ValueError('reviewed_at requires a timezone') if not isinstance(value['claims'], list) or not value['claims']: raise ValueError('publication requires at least one claim') record = {'schema_version': 1, 'title': value['title'].strip(), 'reviewer': value['reviewer'].strip(), 'reviewed_at': value['reviewed_at'], 'claims': [_claim(root, claim) for claim in value['claims']]} if 'supersedes' in value: parent = value['supersedes'] if not isinstance(parent, str) or parent == existing: raise ValueError('supersedes must name an earlier publication') _chain(root, parent, set()) record['supersedes'] = parent return record def _claim(root, claim): if not isinstance(claim, dict) or set(claim) - CLAIM_FIELDS or {'statement', 'disposition'} - set(claim): raise ValueError('invalid publication claim') if not isinstance(claim['statement'], str) or not claim['statement'].strip(): raise ValueError('claim statement must be nonempty text') if claim['disposition'] not in {'included', 'excluded'}: raise ValueError('claim disposition must be included or excluded') item = {'statement': claim['statement'].strip(), 'disposition': claim['disposition']} if 'capture_id' in claim: get(root, 'captures', claim['capture_id']) item['capture_id'] = claim['capture_id'] if claim['disposition'] == 'included': if 'capture_id' not in item or 'exclusion_reason' in claim: raise ValueError('an included claim requires a capture and no exclusion reason') else: reason = claim.get('exclusion_reason') if not isinstance(reason, str) or not reason.strip(): raise ValueError('an excluded claim requires an exclusion reason') item['exclusion_reason'] = reason.strip() return item def _chain(root, ident, seen): if ident in seen: raise ValueError('publication correction cycle') seen.add(ident) try: record = get(root, 'publications', ident) except OSError as exc: raise ValueError('supersedes must name an earlier publication') from exc if 'supersedes' in record: _chain(root, record['supersedes'], seen) def _render(root, ident, record): lines = ['# Reviewed publication', '', 'This file is a reviewed publication. It is not a generated daily, weekly, or external draft.', '', f'- Revision: `{ident}`', f"- Title: {record['title']}", f"- Reviewer: {record['reviewer']}", f"- Reviewed: {record['reviewed_at']}"] if 'supersedes' in record: lines.append(f"- Supersedes: `{record['supersedes']}`") lines += ['', '## Claims', ''] for claim in record['claims']: lines.append(f"- {claim['disposition']}: {claim['statement']}") if 'capture_id' in claim: lines.append(f" - Capture: `{claim['capture_id']}`") if 'exclusion_reason' in claim: lines.append(f" - Excluded because: {claim['exclusion_reason']}") path = Path(root) / 'digests' / 'reviewed' / f'{ident}.md' path.parent.mkdir(parents=True, exist_ok=True) path.write_text('\n'.join(lines) + '\n', encoding='utf-8')