Round one of the p2-4 review found the behaviour sound and the record wrong
in eight places. All eight are fixed here, each against a measurement:
D1 "compare_digest" was written as if it covered holder tokens. It does not:
the store keeps only a sha256 digest, so verify_token is SQL equality;
compare_digest guards the operator's plaintext (api_auth.py:54, :110).
D2 register_minimal is called at memory.py:94, not :120.
D3 the compiler cache key never had an "as_holder or ''" step — the old key
had no identity component at all, which is a stronger defect than the one
I described.
D4 browse.py's own comment said four copies, two here. There are five, three
here (:52, :66, :93), which is what test_visibility_rule asserts.
D5 the store_search / vector_index line pointers moved with the as_of work.
D6 my review prompt said 33 routes; measured 34 decorators / 31 distinct
paths at the parent, 38 / 35 at the reviewed commit.
D7 the semgrep claim cannot be re-run in this container (neither semgrep nor
opengrep is installed), so it is recorded as an observation from the p0-1
gate run, and the anti-drift job is given to the test that does not need
that tool.
D8 "a holder can only write as itself, now guaranteed by the token" is the
opposite of what runs: only the operator writes, attribution comes from
default_holder, and the real guarantee is that RememberRequest has no
holder field.
Four precision notes are recorded too: a revoked token is a hard 401 rather
than a downgrade to anonymous, an anonymous caller passing an empty ?holder=
gets 403, the capability card also carries newest_memory timestamps and
token_issued, and leg_check's commit line is the cwd's HEAD.
The p2-3 entry gains the as_of gap the key test exposed, and the final-accept
entry now carries the seven measured scenarios with the two reasons it stays
open: scenario 7 unmeasured, and recall@10 0.863 against a 0.90 bar.
109 lines
4.6 KiB
Python
109 lines
4.6 KiB
Python
"""Page through the store the way a person browses a shelf: newest first, filtered by who and what."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from .config import Settings
|
|
from .db import Database, row_to_atom
|
|
from .retrieval import decode_cursor, encode_cursor
|
|
from .schemas import AtomPage, Modality
|
|
from .storage import ObjectStore
|
|
|
|
# The visibility rule is spelled out in five literal statements — three here, one in store_search, one in vector_index — because the SQL gate rejects a concatenated statement and a view cannot take a parameter; backend/tests/test_visibility_rule.py fails if the copies disagree.
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Picture:
|
|
"""The stored bytes behind an atom and how to label them when serving."""
|
|
|
|
path: Path
|
|
mime: str
|
|
|
|
|
|
def browse(
|
|
settings: Settings,
|
|
*,
|
|
limit: int,
|
|
cursor: str | None = None,
|
|
modality: Modality | None = None,
|
|
source_id: str | None = None,
|
|
holder: str | None = None,
|
|
as_holder: str | None = None,
|
|
) -> AtomPage:
|
|
"""One page plus the total, so a list can say what it left out instead of looking endless.
|
|
|
|
`holder` narrows the shelf to one holder's memories; `as_holder` is whose eyes the page is
|
|
read through: a holder id, the empty string for a caller who is nobody (only what nobody owns
|
|
and what is marked for everyone), or None to read the whole library as the operator does.
|
|
"""
|
|
offset = decode_cursor(cursor)
|
|
wanted = (modality.value if modality else None, source_id, holder)
|
|
filters = tuple(value for bound in wanted for value in (bound, bound))
|
|
eyes = (as_holder, as_holder, as_holder, as_holder)
|
|
database = Database(settings)
|
|
with database.connect() as connection:
|
|
total = int(
|
|
connection.execute(
|
|
"SELECT COUNT(*) FROM v_atoms_current"
|
|
" WHERE (? IS NULL OR modality = ?) AND (? IS NULL OR source_id = ?) AND (? IS NULL OR holder_id = ?)"
|
|
" AND (? IS NULL"
|
|
" OR holder_id = ''"
|
|
" OR json_extract(visibility_json, '$.scope') = 'everyone'"
|
|
" OR (? <> '' AND (holder_id = ?"
|
|
" OR EXISTS ("
|
|
" SELECT 1 FROM json_each(json_extract(visibility_json, '$.holders'))"
|
|
" WHERE value = ?"
|
|
" ))))",
|
|
(*filters, *eyes),
|
|
).fetchone()[0]
|
|
)
|
|
rows = connection.execute(
|
|
"SELECT * FROM v_atoms_current"
|
|
" WHERE (? IS NULL OR modality = ?) AND (? IS NULL OR source_id = ?) AND (? IS NULL OR holder_id = ?)"
|
|
" AND (? IS NULL"
|
|
" OR holder_id = ''"
|
|
" OR json_extract(visibility_json, '$.scope') = 'everyone'"
|
|
" OR (? <> '' AND (holder_id = ?"
|
|
" OR EXISTS ("
|
|
" SELECT 1 FROM json_each(json_extract(visibility_json, '$.holders'))"
|
|
" WHERE value = ?"
|
|
" ))))"
|
|
" ORDER BY ingested_at DESC, atom_id DESC LIMIT ? OFFSET ?",
|
|
(*filters, *eyes, limit + 1, offset),
|
|
).fetchall()
|
|
atoms = [row_to_atom(row) for row in rows[:limit]]
|
|
more = len(rows) > limit or offset + limit < total
|
|
return AtomPage(
|
|
atoms=atoms,
|
|
total=total,
|
|
next_cursor=encode_cursor(offset + limit) if atoms and more else None,
|
|
)
|
|
|
|
|
|
def picture(settings: Settings, atom_id: str, as_holder: str | None = None) -> Picture | None:
|
|
"""The stored bytes behind one atom; None means there is no picture to show, for any of the three reasons."""
|
|
database = Database(settings)
|
|
with database.connect() as connection:
|
|
row = connection.execute(
|
|
"SELECT image_ref, mime FROM v_atoms_current JOIN sources USING (source_id)"
|
|
" WHERE atom_id = ?"
|
|
" AND (? IS NULL"
|
|
" OR holder_id = ''"
|
|
" OR json_extract(visibility_json, '$.scope') = 'everyone'"
|
|
" OR (? <> '' AND (holder_id = ?"
|
|
" OR EXISTS ("
|
|
" SELECT 1 FROM json_each(json_extract(visibility_json, '$.holders'))"
|
|
" WHERE value = ?"
|
|
" ))))",
|
|
(atom_id, as_holder, as_holder, as_holder, as_holder),
|
|
).fetchone()
|
|
if row is None or not row["image_ref"]:
|
|
return None
|
|
path = ObjectStore(settings).resolve(str(row["image_ref"]))
|
|
if not path.exists():
|
|
return None
|
|
mime = str(row["mime"]) if row["mime"] else "image/png"
|
|
return Picture(path=path, mime=mime)
|