IR-WP-0007-T03 can now record a capture-linked publication separately from generated drafts. The field-outcome design is written, and credential, billing, and website-pull gates were rechecked and left waiting. Assistant: grok Assistant-Session: 01a106a5-fc07-78c2-b64f-8e04cea25daf
113 lines
5 KiB
Python
113 lines
5 KiB
Python
"""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')
|