feat(4636): rule pull-through per surface, and per-rule usage with confirmed lessons on list_rules
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m3s
CI & Build / Python tests (push) Successful in 1m56s
CI & Build / Build & push image (push) Successful in 22s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m3s
CI & Build / Python tests (push) Successful in 1m56s
CI & Build / Build & push image (push) Successful in 22s
retrieval_telemetry gains rule_usage.by_source, the twin of usage.by_source: distinct rules each surface showed, how many an agent opened, and a ratio that is null on a bulk delivery. list_rules(with_usage=True, days=N) adds each rule's usage, windowed, and how many lessons are confirmed instances of it, so linked and unlinked rules can be compared through MCP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -573,3 +573,36 @@ def test_rule_outcome_is_registered_and_is_not_read_only():
|
||||
assert "rule_outcome" in mcp.names
|
||||
assert "rule_outcome" in _WRITE_TOOLS
|
||||
assert "rule_outcome" not in _READ_ONLY_TOOLS
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_rules_carries_usage_only_when_asked():
|
||||
"""A listing is for applying rules, so it stays lean; `with_usage` adds
|
||||
each rule's telemetry and confirmed-lesson count for judging them, with
|
||||
`days` passed down as a window."""
|
||||
from grimoire.mcp.tools import rulebooks as tools
|
||||
from grimoire.services.rule_usage import empty_rule_usage
|
||||
|
||||
rows = [fake_rule(id=1, title="dev is home"), fake_rule(id=2, title="ask first")]
|
||||
seen = {**empty_rule_usage(), "surfaced_count": 4, "pull_count": 1}
|
||||
usage = AsyncMock(return_value={1: seen})
|
||||
lessons = AsyncMock(return_value={1: 3})
|
||||
with patch.object(tools.rulebooks_svc, "list_rules", AsyncMock(return_value=rows)), \
|
||||
patch.object(tools.rulebooks_svc, "list_rule_moments", AsyncMock(return_value={})), \
|
||||
patch.object(tools, "usage_for_rules", usage), \
|
||||
patch.object(tools, "confirmed_lesson_counts", lessons):
|
||||
lean = await tools.list_rules()
|
||||
assert all("usage" not in r and "confirmed_lessons" not in r for r in lean["rules"])
|
||||
usage.assert_not_awaited()
|
||||
|
||||
out = await tools.list_rules(with_usage=True, days=14)
|
||||
|
||||
by_id = {r["id"]: r for r in out["rules"]}
|
||||
assert by_id[1]["usage"]["surfaced_count"] == 4
|
||||
assert by_id[1]["confirmed_lessons"] == 3
|
||||
# A rule missing from either read is zero-filled, never absent.
|
||||
assert by_id[2]["usage"] == empty_rule_usage()
|
||||
assert by_id[2]["confirmed_lessons"] == 0
|
||||
assert usage.await_args.args[0] == [1, 2]
|
||||
assert usage.await_args.kwargs["since"] is not None
|
||||
lessons.assert_awaited_once_with(7, [1, 2])
|
||||
|
||||
Reference in New Issue
Block a user