feat(cli): stack llm index lists only unsealed/incomplete dockets, lazy Zotero snapshot (refs #615)

This commit is contained in:
kert
2026-09-08 14:07:43 -04:00
parent 74d72f7d93
commit bac63363c6
3 changed files with 203 additions and 58 deletions

View File

@@ -7,24 +7,26 @@ app = typer.Typer(no_args_is_help=True)
_COLLECTIONS = ("comments", "rules", "corpus") _COLLECTIONS = ("comments", "rules", "corpus")
def _docs_for(collection: str, store, docket: str, keys: tuple[str, ...]): def _refs_for(
collection: str, store, docket: str, keys: tuple[str, ...], skip_dockets: set[str]
):
from llm.source import ( from llm.source import (
ZoteroPdfIndex, ZoteroPdfIndex,
iter_comment_docs, iter_comment_refs,
iter_corpus_docs, iter_corpus_refs,
iter_rule_docs, iter_rule_refs,
) )
if collection == "comments": if collection == "comments":
return iter_comment_docs(store, docket=docket) return iter_comment_refs(store, docket=docket, skip_dockets=skip_dockets)
if collection == "rules": if collection == "rules":
return iter_rule_docs(store, keys=keys) return iter_rule_refs(store, keys=keys)
from conf import ROOT, path from conf import ROOT, path
zotero = ZoteroPdfIndex.snapshot( zotero = ZoteroPdfIndex.lazy(
path("db.zotero"), path("storage.zotero"), ROOT / ".state" / "llm" path("db.zotero"), path("storage.zotero"), ROOT / ".state" / "llm"
) )
return iter_corpus_docs(store, zotero=zotero) return iter_corpus_refs(store, zotero=zotero)
@app.command() @app.command()
@@ -47,7 +49,7 @@ def index(
from conf.connect import bib from conf.connect import bib
from llm import config as llm_config from llm import config as llm_config
from llm.index import index_docs from llm.index import _engine, docket_complete, index_refs
from llm.pool import HostPool from llm.pool import HostPool
targets = _COLLECTIONS if collection == "all" else (collection,) targets = _COLLECTIONS if collection == "all" else (collection,)
@@ -55,20 +57,35 @@ def index(
raise typer.BadParameter("collection must be comments, rules, corpus or all") raise typer.BadParameter("collection must be comments, rules, corpus or all")
cfg = llm_config.load() cfg = llm_config.load()
store = bib() store = bib()
sealed_all = {} if force else store.sealed_dockets()
for target in targets: for target in targets:
docs = _docs_for(target, store, docket, tuple(key)) complete = (
docket_complete(_engine(cfg), target)
if (sealed_all and target == "comments")
else {}
)
skip = {d for d, s in sealed_all.items() if complete.get(d) == s}
pending_seals = {d: s for d, s in sealed_all.items() if d not in skip}
refs = _refs_for(target, store, docket, tuple(key), skip)
if limit: if limit:
docs = itertools.islice(docs, limit) refs = itertools.islice(refs, limit)
stats = index_docs( stats = index_refs(
docs, refs,
collection=target, collection=target,
cfg=cfg, cfg=cfg,
pool=HostPool.from_config(cfg), pool=HostPool.from_config(cfg),
force=force, force=force,
sealed=pending_seals if target == "comments" else None,
mark_complete=not limit,
) )
if skip:
typer.echo(
f"{target}: {len(skip)} sealed docket(s) already complete — not listed"
)
typer.echo( typer.echo(
f"{target}: indexed={stats['indexed']} skipped={stats['skipped']} " f"{target}: indexed={stats['indexed']} skipped={stats['skipped']} chunks={stats['chunks']} "
f"chunks={stats['chunks']}" f"fp_skipped={stats['fingerprint_skipped']} hash_skipped={stats['hash_skipped']} "
f"docket_complete={stats['docket_complete']}"
) )

View File

@@ -10,34 +10,51 @@ from cli.llm import app
runner = CliRunner() runner = CliRunner()
_STATS = {"indexed": 3, "skipped": 1, "chunks": 7} _STATS = {
"indexed": 3,
"skipped": 1,
"chunks": 7,
"fingerprint_skipped": 0,
"hash_skipped": 0,
"docket_complete": 0,
}
class TestIndexComments: class TestIndexComments:
@patch("llm.source.iter_comment_docs") @patch("llm.index.docket_complete", return_value={})
@patch("llm.index._engine")
@patch("llm.source.iter_comment_refs")
@patch("llm.pool.HostPool.from_config") @patch("llm.pool.HostPool.from_config")
@patch("llm.index.index_docs") @patch("llm.index.index_refs")
@patch("conf.connect.bib") @patch("conf.connect.bib")
@patch("llm.config.load") @patch("llm.config.load")
def test_default_collection_is_comments( def test_default_collection_is_comments(
self, mock_load, mock_bib, mock_index_docs, mock_from_config, mock_iter self,
mock_load,
mock_bib,
mock_index_refs,
mock_from_config,
mock_iter,
_engine,
_complete,
): ):
cfg = MagicMock() cfg = MagicMock()
mock_load.return_value = cfg mock_load.return_value = cfg
store = MagicMock() store = MagicMock()
store.sealed_dockets.return_value = {}
mock_bib.return_value = store mock_bib.return_value = store
docs = iter(["doc1", "doc2"]) docs = iter(["doc1", "doc2"])
mock_iter.return_value = docs mock_iter.return_value = docs
pool = MagicMock() pool = MagicMock()
mock_from_config.return_value = pool mock_from_config.return_value = pool
mock_index_docs.return_value = _STATS mock_index_refs.return_value = _STATS
result = runner.invoke(app, ["index"]) result = runner.invoke(app, ["index"])
assert result.exit_code == 0 assert result.exit_code == 0, result.output
mock_iter.assert_called_once_with(store, docket="") mock_iter.assert_called_once_with(store, docket="", skip_dockets=set())
mock_index_docs.assert_called_once() mock_index_refs.assert_called_once()
kwargs = mock_index_docs.call_args.kwargs kwargs = mock_index_refs.call_args.kwargs
assert kwargs["collection"] == "comments" assert kwargs["collection"] == "comments"
assert kwargs["cfg"] is cfg assert kwargs["cfg"] is cfg
assert kwargs["pool"] is pool assert kwargs["pool"] is pool
@@ -46,71 +63,82 @@ class TestIndexComments:
class TestIndexCorpus: class TestIndexCorpus:
@patch("llm.source.ZoteroPdfIndex.snapshot") @patch("llm.index.docket_complete", return_value={})
@patch("llm.source.iter_corpus_docs") @patch("llm.index._engine")
@patch("llm.source.ZoteroPdfIndex.lazy")
@patch("llm.source.iter_corpus_refs")
@patch("llm.pool.HostPool.from_config") @patch("llm.pool.HostPool.from_config")
@patch("llm.index.index_docs") @patch("llm.index.index_refs")
@patch("conf.connect.bib") @patch("conf.connect.bib")
@patch("llm.config.load") @patch("llm.config.load")
def test_corpus_collection( def test_corpus_collection(
self, self,
mock_load, mock_load,
mock_bib, mock_bib,
mock_index_docs, mock_index_refs,
mock_from_config, mock_from_config,
mock_iter, mock_iter,
mock_snapshot, mock_lazy,
_engine,
_complete,
): ):
cfg = MagicMock() cfg = MagicMock()
mock_load.return_value = cfg mock_load.return_value = cfg
store = MagicMock() store = MagicMock()
store.sealed_dockets.return_value = {}
mock_bib.return_value = store mock_bib.return_value = store
mock_iter.return_value = iter(["doc1"]) mock_iter.return_value = iter(["doc1"])
mock_from_config.return_value = MagicMock() mock_from_config.return_value = MagicMock()
mock_index_docs.return_value = _STATS mock_index_refs.return_value = _STATS
zot = MagicMock() zot = MagicMock()
mock_snapshot.return_value = zot mock_lazy.return_value = zot
result = runner.invoke(app, ["index", "--collection", "corpus"]) result = runner.invoke(app, ["index", "--collection", "corpus"])
assert result.exit_code == 0 assert result.exit_code == 0, result.output
mock_iter.assert_called_once_with(store, zotero=zot) mock_iter.assert_called_once_with(store, zotero=zot)
kwargs = mock_index_docs.call_args.kwargs kwargs = mock_index_refs.call_args.kwargs
assert kwargs["collection"] == "corpus" assert kwargs["collection"] == "corpus"
assert "corpus: indexed=3 skipped=1 chunks=7" in result.output assert "corpus: indexed=3 skipped=1 chunks=7" in result.output
class TestIndexAll: class TestIndexAll:
@patch("llm.source.ZoteroPdfIndex.snapshot") @patch("llm.index.docket_complete", return_value={})
@patch("llm.source.iter_corpus_docs") @patch("llm.index._engine")
@patch("llm.source.iter_rule_docs") @patch("llm.source.ZoteroPdfIndex.lazy")
@patch("llm.source.iter_comment_docs") @patch("llm.source.iter_corpus_refs")
@patch("llm.source.iter_rule_refs")
@patch("llm.source.iter_comment_refs")
@patch("llm.pool.HostPool.from_config") @patch("llm.pool.HostPool.from_config")
@patch("llm.index.index_docs") @patch("llm.index.index_refs")
@patch("conf.connect.bib") @patch("conf.connect.bib")
@patch("llm.config.load") @patch("llm.config.load")
def test_all_runs_comments_rules_corpus_in_order( def test_all_runs_comments_rules_corpus_in_order(
self, self,
mock_load, mock_load,
mock_bib, mock_bib,
mock_index_docs, mock_index_refs,
mock_from_config, mock_from_config,
mock_comments, mock_comments,
mock_rules, mock_rules,
mock_corpus, mock_corpus,
mock_snapshot, mock_lazy,
_engine,
_complete,
): ):
mock_load.return_value = MagicMock() mock_load.return_value = MagicMock()
mock_bib.return_value = MagicMock() store = MagicMock()
store.sealed_dockets.return_value = {}
mock_bib.return_value = store
mock_from_config.return_value = MagicMock() mock_from_config.return_value = MagicMock()
mock_index_docs.return_value = _STATS mock_index_refs.return_value = _STATS
for m in (mock_comments, mock_rules, mock_corpus): for m in (mock_comments, mock_rules, mock_corpus):
m.return_value = iter(["d"]) m.return_value = iter(["d"])
result = runner.invoke(app, ["index", "--collection", "all"]) result = runner.invoke(app, ["index", "--collection", "all"])
assert result.exit_code == 0 assert result.exit_code == 0, result.output
assert [c.kwargs["collection"] for c in mock_index_docs.call_args_list] == [ assert [c.kwargs["collection"] for c in mock_index_refs.call_args_list] == [
"comments", "comments",
"rules", "rules",
"corpus", "corpus",
@@ -119,30 +147,40 @@ class TestIndexAll:
class TestIndexRules: class TestIndexRules:
@patch("llm.source.iter_rule_docs") @patch("llm.index.docket_complete", return_value={})
@patch("llm.index._engine")
@patch("llm.source.iter_rule_refs")
@patch("llm.pool.HostPool.from_config") @patch("llm.pool.HostPool.from_config")
@patch("llm.index.index_docs") @patch("llm.index.index_refs")
@patch("conf.connect.bib") @patch("conf.connect.bib")
@patch("llm.config.load") @patch("llm.config.load")
def test_rules_collection( def test_rules_collection(
self, mock_load, mock_bib, mock_index_docs, mock_from_config, mock_iter self,
mock_load,
mock_bib,
mock_index_refs,
mock_from_config,
mock_iter,
_engine,
_complete,
): ):
cfg = MagicMock() cfg = MagicMock()
mock_load.return_value = cfg mock_load.return_value = cfg
store = MagicMock() store = MagicMock()
store.sealed_dockets.return_value = {}
mock_bib.return_value = store mock_bib.return_value = store
mock_iter.return_value = iter(["doc1"]) mock_iter.return_value = iter(["doc1"])
mock_from_config.return_value = MagicMock() mock_from_config.return_value = MagicMock()
mock_index_docs.return_value = _STATS mock_index_refs.return_value = _STATS
result = runner.invoke( result = runner.invoke(
app, app,
["index", "--collection", "rules", "--key", "abc123", "--key", "def456"], ["index", "--collection", "rules", "--key", "abc123", "--key", "def456"],
) )
assert result.exit_code == 0 assert result.exit_code == 0, result.output
mock_iter.assert_called_once_with(store, keys=("abc123", "def456")) mock_iter.assert_called_once_with(store, keys=("abc123", "def456"))
kwargs = mock_index_docs.call_args.kwargs kwargs = mock_index_refs.call_args.kwargs
assert kwargs["collection"] == "rules" assert kwargs["collection"] == "rules"
assert "indexed=3 skipped=1 chunks=7" in result.output assert "indexed=3 skipped=1 chunks=7" in result.output
@@ -161,25 +199,37 @@ class TestIndexBadCollection:
class TestIndexLimit: class TestIndexLimit:
@patch("llm.source.iter_comment_docs") @patch("llm.index.docket_complete", return_value={})
@patch("llm.index._engine")
@patch("llm.source.iter_comment_refs")
@patch("llm.pool.HostPool.from_config") @patch("llm.pool.HostPool.from_config")
@patch("llm.index.index_docs") @patch("llm.index.index_refs")
@patch("conf.connect.bib") @patch("conf.connect.bib")
@patch("llm.config.load") @patch("llm.config.load")
def test_limit_truncates_docs( def test_limit_truncates_docs(
self, mock_load, mock_bib, mock_index_docs, mock_from_config, mock_iter self,
mock_load,
mock_bib,
mock_index_refs,
mock_from_config,
mock_iter,
_engine,
_complete,
): ):
mock_load.return_value = MagicMock() mock_load.return_value = MagicMock()
mock_bib.return_value = MagicMock() store = MagicMock()
store.sealed_dockets.return_value = {}
mock_bib.return_value = store
mock_iter.return_value = iter([f"doc{i}" for i in range(5)]) mock_iter.return_value = iter([f"doc{i}" for i in range(5)])
mock_from_config.return_value = MagicMock() mock_from_config.return_value = MagicMock()
mock_index_docs.return_value = _STATS mock_index_refs.return_value = _STATS
result = runner.invoke(app, ["index", "--limit", "2"]) result = runner.invoke(app, ["index", "--limit", "2"])
assert result.exit_code == 0 assert result.exit_code == 0, result.output
docs_arg = mock_index_docs.call_args.args[0] refs_arg = mock_index_refs.call_args.args[0]
assert list(docs_arg) == ["doc0", "doc1"] assert list(refs_arg) == ["doc0", "doc1"]
assert mock_index_refs.call_args.kwargs["mark_complete"] is False
class TestServe: class TestServe:

View File

@@ -0,0 +1,78 @@
"""Exercise cli/llm.py's `index` seal-skip wiring: sealed/complete dockets
are excluded from the comment listing, and only pending seals are passed
through to be re-marked."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from typer.testing import CliRunner
from cli.llm import app
runner = CliRunner()
_STATS = {
"indexed": 0,
"skipped": 0,
"chunks": 0,
"fingerprint_skipped": 5,
"hash_skipped": 0,
"docket_complete": 0,
}
@patch("llm.index._engine")
@patch(
"llm.index.docket_complete",
return_value={"CMS-2019-0111": "s1", "CMS-2020-0088": "old"},
)
@patch("llm.source.iter_comment_refs")
@patch("llm.pool.HostPool.from_config")
@patch("llm.index.index_refs")
@patch("conf.connect.bib")
@patch("llm.config.load")
def test_complete_sealed_dockets_are_not_listed(
mock_load, mock_bib, mock_index, mock_pool, mock_iter, mock_complete, _engine
):
mock_load.return_value = MagicMock()
store = MagicMock()
store.sealed_dockets.return_value = {"CMS-2019-0111": "s1", "CMS-2020-0088": "s2"}
mock_bib.return_value = store
mock_iter.return_value = iter([])
mock_index.return_value = _STATS
result = runner.invoke(app, ["index"])
assert result.exit_code == 0, result.output
kwargs = mock_iter.call_args.kwargs
assert kwargs["skip_dockets"] == {"CMS-2019-0111"} # seal matches → skipped
ikw = mock_index.call_args.kwargs
assert ikw["sealed"] == {
"CMS-2020-0088": "s2"
} # re-sealed docket will be re-marked
assert ikw["mark_complete"] is True
assert "fp_skipped=5" in result.output
@patch("llm.index._engine")
@patch("llm.index.docket_complete", return_value={})
@patch("llm.source.iter_comment_refs")
@patch("llm.pool.HostPool.from_config")
@patch("llm.index.index_refs")
@patch("conf.connect.bib")
@patch("llm.config.load")
def test_force_and_limit_disable_skips_and_completion(
mock_load, mock_bib, mock_index, mock_pool, mock_iter, mock_complete, _engine
):
mock_load.return_value = MagicMock()
store = MagicMock()
store.sealed_dockets.return_value = {"CMS-2019-0111": "s1"}
mock_bib.return_value = store
mock_iter.return_value = iter([])
mock_index.return_value = _STATS
result = runner.invoke(app, ["index", "--force", "--limit", "5"])
assert result.exit_code == 0, result.output
assert mock_iter.call_args.kwargs["skip_dockets"] == set()
assert mock_index.call_args.kwargs["mark_complete"] is False