ILIB-WP-0002-T01..T06: append-only item store, read-only IMAP intake with quarantine, file-backed registries, idempotent hub/pull/digest notices, run-due with health and the systemd user timer. 14 tests pass. Registers the intelligence topics and the intelligence-radar subscription. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 1170398@bnt-lap001 Assistant-Session: 622cd705-cb31-4322-ba08-dd42e44fa583
141 lines
4.5 KiB
Python
141 lines
4.5 KiB
Python
"""Append-only, content-addressed item store.
|
|
|
|
Original bytes live under data/items/<sha256> and are never overwritten or
|
|
deleted. Each delivery of those bytes is its own immutable record. Derived text
|
|
is rebuildable and may be replaced.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
SCHEMA = 'inbound-library/v1'
|
|
HEX = set('0123456789abcdef')
|
|
|
|
|
|
def canonical(value):
|
|
return json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode()
|
|
|
|
|
|
def sha256(data):
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def is_id(ident):
|
|
return isinstance(ident, str) and len(ident) == 64 and set(ident) <= HEX
|
|
|
|
|
|
def atomic(path, data, immutable=False):
|
|
"""Write via a temp file. With immutable, an existing path is never replaced."""
|
|
path = Path(path)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
fd, name = tempfile.mkstemp(dir=path.parent, prefix='.write-')
|
|
try:
|
|
with os.fdopen(fd, 'wb') as stream:
|
|
stream.write(data)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
if immutable:
|
|
try:
|
|
os.link(name, path)
|
|
except FileExistsError:
|
|
if path.read_bytes() != data:
|
|
raise ValueError(f'refusing to overwrite {path}')
|
|
return
|
|
os.replace(name, path)
|
|
finally:
|
|
if os.path.exists(name):
|
|
os.unlink(name)
|
|
|
|
|
|
def data_dir(root):
|
|
return Path(root) / 'data'
|
|
|
|
|
|
def put_record(root, kind, value):
|
|
"""Store an immutable JSON record keyed by the hash of its canonical form."""
|
|
content = canonical(value)
|
|
ident = sha256(content)
|
|
atomic(data_dir(root) / kind / f'{ident}.json', content + b'\n', immutable=True)
|
|
return ident
|
|
|
|
|
|
def get_record(root, kind, ident):
|
|
if not is_id(ident):
|
|
raise ValueError('invalid content ID')
|
|
value = json.loads((data_dir(root) / kind / f'{ident}.json').read_text())
|
|
if sha256(canonical(value)) != ident:
|
|
raise ValueError(f'integrity mismatch: {kind}/{ident}')
|
|
return value
|
|
|
|
|
|
def records(root, kind):
|
|
directory = data_dir(root) / kind
|
|
if not directory.is_dir():
|
|
return []
|
|
return [get_record(root, kind, p.stem) for p in sorted(directory.glob('*.json'))]
|
|
|
|
|
|
def put_item(root, data):
|
|
"""Retain original bytes. Returns (item_id, newly_stored)."""
|
|
ident = sha256(data)
|
|
path = data_dir(root) / 'items' / ident
|
|
existed = path.exists()
|
|
atomic(path, data, immutable=True)
|
|
return ident, not existed
|
|
|
|
|
|
def get_item(root, ident):
|
|
if not is_id(ident):
|
|
raise ValueError('invalid item ID')
|
|
data = (data_dir(root) / 'items' / ident).read_bytes()
|
|
if sha256(data) != ident:
|
|
raise ValueError(f'integrity mismatch: items/{ident}')
|
|
return data
|
|
|
|
|
|
def record_delivery(root, item, provenance):
|
|
"""Record one delivery of an item. Without a source it is quarantined."""
|
|
if not is_id(item):
|
|
raise ValueError('invalid item ID')
|
|
delivery = {'schema': SCHEMA, 'item': item, **provenance}
|
|
delivery['status'] = 'routed' if provenance.get('source') else 'quarantined'
|
|
return put_record(root, 'deliveries', delivery)
|
|
|
|
|
|
def put_derived(root, item, derived):
|
|
"""Derived text and metadata are rebuildable, so they may be replaced."""
|
|
atomic(data_dir(root) / 'derived' / f'{item}.json',
|
|
canonical({'schema': SCHEMA, 'item': item, **derived}) + b'\n')
|
|
|
|
|
|
def get_derived(root, item):
|
|
path = data_dir(root) / 'derived' / f'{item}.json'
|
|
return json.loads(path.read_text()) if path.exists() else None
|
|
|
|
|
|
def verify(root):
|
|
"""Rehash every item and record. Returns a list of problems."""
|
|
problems = []
|
|
base = data_dir(root)
|
|
items = base / 'items'
|
|
if items.is_dir():
|
|
for path in sorted(items.iterdir()):
|
|
if path.name.startswith('.'):
|
|
continue
|
|
if not is_id(path.name) or sha256(path.read_bytes()) != path.name:
|
|
problems.append(f'item integrity mismatch: {path.name}')
|
|
for kind in ('deliveries', 'notices'):
|
|
directory = base / kind
|
|
if not directory.is_dir():
|
|
continue
|
|
for path in sorted(directory.glob('*.json')):
|
|
try:
|
|
value = get_record(root, kind, path.stem)
|
|
except (ValueError, json.JSONDecodeError):
|
|
problems.append(f'record integrity mismatch: {kind}/{path.stem}')
|
|
continue
|
|
if kind == 'deliveries' and not (items / value.get('item', '')).is_file():
|
|
problems.append(f'delivery {path.stem} refers to missing item')
|
|
return problems
|