feat(cli): stack llm index lists only unsealed/incomplete dockets, lazy Zotero snapshot (refs #615)
This commit is contained in:
@@ -7,24 +7,26 @@ app = typer.Typer(no_args_is_help=True)
|
||||
_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 (
|
||||
ZoteroPdfIndex,
|
||||
iter_comment_docs,
|
||||
iter_corpus_docs,
|
||||
iter_rule_docs,
|
||||
iter_comment_refs,
|
||||
iter_corpus_refs,
|
||||
iter_rule_refs,
|
||||
)
|
||||
|
||||
if collection == "comments":
|
||||
return iter_comment_docs(store, docket=docket)
|
||||
return iter_comment_refs(store, docket=docket, skip_dockets=skip_dockets)
|
||||
if collection == "rules":
|
||||
return iter_rule_docs(store, keys=keys)
|
||||
return iter_rule_refs(store, keys=keys)
|
||||
from conf import ROOT, path
|
||||
|
||||
zotero = ZoteroPdfIndex.snapshot(
|
||||
zotero = ZoteroPdfIndex.lazy(
|
||||
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()
|
||||
@@ -47,7 +49,7 @@ def index(
|
||||
|
||||
from conf.connect import bib
|
||||
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
|
||||
|
||||
targets = _COLLECTIONS if collection == "all" else (collection,)
|
||||
@@ -55,20 +57,35 @@ def index(
|
||||
raise typer.BadParameter("collection must be comments, rules, corpus or all")
|
||||
cfg = llm_config.load()
|
||||
store = bib()
|
||||
sealed_all = {} if force else store.sealed_dockets()
|
||||
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:
|
||||
docs = itertools.islice(docs, limit)
|
||||
stats = index_docs(
|
||||
docs,
|
||||
refs = itertools.islice(refs, limit)
|
||||
stats = index_refs(
|
||||
refs,
|
||||
collection=target,
|
||||
cfg=cfg,
|
||||
pool=HostPool.from_config(cfg),
|
||||
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(
|
||||
f"{target}: indexed={stats['indexed']} skipped={stats['skipped']} "
|
||||
f"chunks={stats['chunks']}"
|
||||
f"{target}: indexed={stats['indexed']} skipped={stats['skipped']} chunks={stats['chunks']} "
|
||||
f"fp_skipped={stats['fingerprint_skipped']} hash_skipped={stats['hash_skipped']} "
|
||||
f"docket_complete={stats['docket_complete']}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,34 +10,51 @@ from cli.llm import app
|
||||
|
||||
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:
|
||||
@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.index.index_docs")
|
||||
@patch("llm.index.index_refs")
|
||||
@patch("conf.connect.bib")
|
||||
@patch("llm.config.load")
|
||||
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()
|
||||
mock_load.return_value = cfg
|
||||
store = MagicMock()
|
||||
store.sealed_dockets.return_value = {}
|
||||
mock_bib.return_value = store
|
||||
docs = iter(["doc1", "doc2"])
|
||||
mock_iter.return_value = docs
|
||||
pool = MagicMock()
|
||||
mock_from_config.return_value = pool
|
||||
mock_index_docs.return_value = _STATS
|
||||
mock_index_refs.return_value = _STATS
|
||||
|
||||
result = runner.invoke(app, ["index"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_iter.assert_called_once_with(store, docket="")
|
||||
mock_index_docs.assert_called_once()
|
||||
kwargs = mock_index_docs.call_args.kwargs
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_iter.assert_called_once_with(store, docket="", skip_dockets=set())
|
||||
mock_index_refs.assert_called_once()
|
||||
kwargs = mock_index_refs.call_args.kwargs
|
||||
assert kwargs["collection"] == "comments"
|
||||
assert kwargs["cfg"] is cfg
|
||||
assert kwargs["pool"] is pool
|
||||
@@ -46,71 +63,82 @@ class TestIndexComments:
|
||||
|
||||
|
||||
class TestIndexCorpus:
|
||||
@patch("llm.source.ZoteroPdfIndex.snapshot")
|
||||
@patch("llm.source.iter_corpus_docs")
|
||||
@patch("llm.index.docket_complete", return_value={})
|
||||
@patch("llm.index._engine")
|
||||
@patch("llm.source.ZoteroPdfIndex.lazy")
|
||||
@patch("llm.source.iter_corpus_refs")
|
||||
@patch("llm.pool.HostPool.from_config")
|
||||
@patch("llm.index.index_docs")
|
||||
@patch("llm.index.index_refs")
|
||||
@patch("conf.connect.bib")
|
||||
@patch("llm.config.load")
|
||||
def test_corpus_collection(
|
||||
self,
|
||||
mock_load,
|
||||
mock_bib,
|
||||
mock_index_docs,
|
||||
mock_index_refs,
|
||||
mock_from_config,
|
||||
mock_iter,
|
||||
mock_snapshot,
|
||||
mock_lazy,
|
||||
_engine,
|
||||
_complete,
|
||||
):
|
||||
cfg = MagicMock()
|
||||
mock_load.return_value = cfg
|
||||
store = MagicMock()
|
||||
store.sealed_dockets.return_value = {}
|
||||
mock_bib.return_value = store
|
||||
mock_iter.return_value = iter(["doc1"])
|
||||
mock_from_config.return_value = MagicMock()
|
||||
mock_index_docs.return_value = _STATS
|
||||
mock_index_refs.return_value = _STATS
|
||||
zot = MagicMock()
|
||||
mock_snapshot.return_value = zot
|
||||
mock_lazy.return_value = zot
|
||||
|
||||
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)
|
||||
kwargs = mock_index_docs.call_args.kwargs
|
||||
kwargs = mock_index_refs.call_args.kwargs
|
||||
assert kwargs["collection"] == "corpus"
|
||||
assert "corpus: indexed=3 skipped=1 chunks=7" in result.output
|
||||
|
||||
|
||||
class TestIndexAll:
|
||||
@patch("llm.source.ZoteroPdfIndex.snapshot")
|
||||
@patch("llm.source.iter_corpus_docs")
|
||||
@patch("llm.source.iter_rule_docs")
|
||||
@patch("llm.source.iter_comment_docs")
|
||||
@patch("llm.index.docket_complete", return_value={})
|
||||
@patch("llm.index._engine")
|
||||
@patch("llm.source.ZoteroPdfIndex.lazy")
|
||||
@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.index.index_docs")
|
||||
@patch("llm.index.index_refs")
|
||||
@patch("conf.connect.bib")
|
||||
@patch("llm.config.load")
|
||||
def test_all_runs_comments_rules_corpus_in_order(
|
||||
self,
|
||||
mock_load,
|
||||
mock_bib,
|
||||
mock_index_docs,
|
||||
mock_index_refs,
|
||||
mock_from_config,
|
||||
mock_comments,
|
||||
mock_rules,
|
||||
mock_corpus,
|
||||
mock_snapshot,
|
||||
mock_lazy,
|
||||
_engine,
|
||||
_complete,
|
||||
):
|
||||
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_index_docs.return_value = _STATS
|
||||
mock_index_refs.return_value = _STATS
|
||||
for m in (mock_comments, mock_rules, mock_corpus):
|
||||
m.return_value = iter(["d"])
|
||||
|
||||
result = runner.invoke(app, ["index", "--collection", "all"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert [c.kwargs["collection"] for c in mock_index_docs.call_args_list] == [
|
||||
assert result.exit_code == 0, result.output
|
||||
assert [c.kwargs["collection"] for c in mock_index_refs.call_args_list] == [
|
||||
"comments",
|
||||
"rules",
|
||||
"corpus",
|
||||
@@ -119,30 +147,40 @@ class TestIndexAll:
|
||||
|
||||
|
||||
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.index.index_docs")
|
||||
@patch("llm.index.index_refs")
|
||||
@patch("conf.connect.bib")
|
||||
@patch("llm.config.load")
|
||||
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()
|
||||
mock_load.return_value = cfg
|
||||
store = MagicMock()
|
||||
store.sealed_dockets.return_value = {}
|
||||
mock_bib.return_value = store
|
||||
mock_iter.return_value = iter(["doc1"])
|
||||
mock_from_config.return_value = MagicMock()
|
||||
mock_index_docs.return_value = _STATS
|
||||
mock_index_refs.return_value = _STATS
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["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"))
|
||||
kwargs = mock_index_docs.call_args.kwargs
|
||||
kwargs = mock_index_refs.call_args.kwargs
|
||||
assert kwargs["collection"] == "rules"
|
||||
assert "indexed=3 skipped=1 chunks=7" in result.output
|
||||
|
||||
@@ -161,25 +199,37 @@ class TestIndexBadCollection:
|
||||
|
||||
|
||||
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.index.index_docs")
|
||||
@patch("llm.index.index_refs")
|
||||
@patch("conf.connect.bib")
|
||||
@patch("llm.config.load")
|
||||
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_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_from_config.return_value = MagicMock()
|
||||
mock_index_docs.return_value = _STATS
|
||||
mock_index_refs.return_value = _STATS
|
||||
|
||||
result = runner.invoke(app, ["index", "--limit", "2"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
docs_arg = mock_index_docs.call_args.args[0]
|
||||
assert list(docs_arg) == ["doc0", "doc1"]
|
||||
assert result.exit_code == 0, result.output
|
||||
refs_arg = mock_index_refs.call_args.args[0]
|
||||
assert list(refs_arg) == ["doc0", "doc1"]
|
||||
assert mock_index_refs.call_args.kwargs["mark_complete"] is False
|
||||
|
||||
|
||||
class TestServe:
|
||||
|
||||
78
tests/cli/test_llm_index_sealed.py
Normal file
78
tests/cli/test_llm_index_sealed.py
Normal 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
|
||||
Reference in New Issue
Block a user