feat(remote): a rebuildable collection index, and remote reindex master
M4. `_index.parquet` per collection plus `owi remote reindex`, shipped as one change because an index without a rebuild path is a liability rather than a cache. A listing goes from one GET per dataset to one LIST plus one range read -- on a 585-dataset collection, 585 requests replaced by 2. Most of the work is in the ways it refuses to be trusted, because this is the milestone that puts a derived artifact in the read path, and that is the exact place this codebase has repeatedly turned a failure into a confident wrong answer. - The sidecars stay authoritative. The index is derived from them and always rebuildable from them; if they disagree the sidecars win. Deleting every _index.parquet costs speed and nothing else. - A stale index is never served. The index stores a fingerprint over every sidecar's name, size and mtime, recomputed on read from one cheap ls. Chosen over a bare object count because it also catches a resized or re-uploaded sidecar at identical cost -- the listing already returns the details. - Absent, unreadable, stale, or written by a newer owilix: all four walk, and log why. read_index raises IndexUnusable rather than returning None so the one correct response is impossible to forget at the call site. - One unreadable sidecar does not cost the index, matching the tolerance list() gained in 5.9.0 rather than reintroducing the bug it fixed. - The fingerprint excludes _index.parquet itself; an index that invalidated itself by existing would never be usable. Parquet rather than SQLite: duckdb and pyarrow are already dependencies and DuckDB queries Parquet in place over HTTP range requests with predicate pushdown. SQLite over an object store has no locking, needs the whole file, and rewrites all of it per update -- on a medium with no byte-range writes, two writers silently lose each other's work. Flattened columns carry the predicates; metadata_json carries the sidecar, so a dataset from the index is identical to one from disk. Verified end to end rather than by unit test alone: walk and index return the same results; adding a dataset without reindexing gives 4 not 3; a corrupted index gives 4; a deleted index gives 4. Known limit, documented: the fingerprint cannot see an edit preserving both size and mtime. That is why reindex exists and why the sidecars stay authoritative. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017phbSd6D8u4iEQsCAPEw6s