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")
|
_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']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
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