feat(ledger): coverage refresh feeds the shape ledger; the readout inverts to accounting (#2788, milestone 294 step 2)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Failing after 27s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 44s
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Failing after 27s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 44s
compute_coverage is now the ledger's sync point: every walk upserts the extracted shapes (new → unclassified, the todo state; surviving → last-seen bump; vanished → stamped, kept as history), re-files judgments whose snippet target went away, and mechanically stamps snippet reference locations as canonical — the one always-safe rule, self-healing only for its own stamps (an agent's judgment is never unwound by machinery). The covering predicate moves to shape_ledger.location_covers as the single home (match_shapes retired with its consumer); coverage's payload and line invert from 'N/M shapes recorded' to shape ACCOUNTING per note 2786: accounted/total with a canonical·instance·variant·exempt breakdown, and unclassified — THE todo — with its largest directories. Cache key bumps to v2 so pre-ledger blobs honestly read 'not measured yet' instead of rendering in a shape no longer spoken. Readout is deliberately project-wide (all repos' live rows), while the walk serves whichever repos the owner's keyring reaches this refresh. Integration tests pin the new contract: rows for every extracted shape, mechanical canonical stamps carrying snippet ids, idempotent recompute, agent judgments surviving recompute AND vanish/return, vanished rows leaving the readout but keeping their history. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+116
-28
@@ -19,10 +19,10 @@ from scribe.services.coverage import (
|
||||
coverage_line,
|
||||
extract_shapes,
|
||||
largest_gaps,
|
||||
match_shapes,
|
||||
scannable,
|
||||
shapes_from_archive,
|
||||
)
|
||||
from scribe.services.shape_ledger import location_covers
|
||||
|
||||
# --- unit: the definition extractor (shared vectors with the hook) -----------
|
||||
|
||||
@@ -117,51 +117,53 @@ def test_shapes_from_archive_strips_the_wrapper_and_gates_files():
|
||||
assert shapes_from_archive(_tarball(TREE)) == TREE_SHAPES
|
||||
|
||||
|
||||
# --- unit: matching shapes against recorded locations ------------------------
|
||||
# --- unit: the covering predicate (lives with the ledger since #2788) --------
|
||||
|
||||
|
||||
def test_match_covers_by_exact_path_dir_prefix_and_css_dot():
|
||||
recorded = [
|
||||
("src/app.py", "make_app"), # exact file
|
||||
("web", ".btn"), # dir prefix + css dot normalization
|
||||
]
|
||||
matched = match_shapes(TREE_SHAPES, recorded)
|
||||
covered = {name for _p, _k, name, ok in matched if ok}
|
||||
assert covered == {"make_app", "btn"}
|
||||
def test_location_covers_by_exact_path_dir_prefix_and_css_dot():
|
||||
assert location_covers("src/app.py", "make_app", "src/app.py", "make_app")
|
||||
# dir prefix + css dot normalization
|
||||
assert location_covers("web", ".btn", "web/button.css", "btn")
|
||||
assert not location_covers("src/app.py", "make_app", "src/util.py", "make_app")
|
||||
|
||||
|
||||
def test_a_symbol_less_record_covers_nothing():
|
||||
"""A whole-file snippet makes no claim about any particular definition
|
||||
inside it — crediting all of them would inflate the number for free."""
|
||||
matched = match_shapes(TREE_SHAPES, [("src/app.py", "")])
|
||||
assert not any(ok for *_x, ok in matched)
|
||||
assert not location_covers("src/app.py", "", "src/app.py", "make_app")
|
||||
|
||||
|
||||
def test_no_prefix_bleed_between_sibling_directories():
|
||||
matched = match_shapes(
|
||||
[("src/library/x.py", "sym", "helper")], [("src/lib", "helper")]
|
||||
)
|
||||
assert not matched[0][3]
|
||||
assert not location_covers("src/lib", "helper", "src/library/x.py", "helper")
|
||||
|
||||
|
||||
def test_largest_gaps_ranks_by_uncovered_and_drops_clean_dirs():
|
||||
matched = match_shapes(TREE_SHAPES, [("src/app.py", "make_app"), ("web", ".btn")])
|
||||
gaps = largest_gaps(matched)
|
||||
assert gaps == [{"dir": "src", "uncovered": 2, "total": 3}]
|
||||
def test_largest_gaps_ranks_by_unclassified_and_drops_clean_dirs():
|
||||
accounted = [
|
||||
("src/app.py", "sym", "make_app", True),
|
||||
("src/app.py", "sym", "Config", False),
|
||||
("src/util.py", "sym", "helper", False),
|
||||
("web/button.css", "css", "btn", True),
|
||||
]
|
||||
gaps = largest_gaps(accounted)
|
||||
assert gaps == [{"dir": "src", "unclassified": 2, "total": 3}]
|
||||
|
||||
|
||||
def test_coverage_line_is_evidence_carrying_and_labeled_estimate():
|
||||
line = coverage_line({
|
||||
"total": 210, "recorded": 34, "estimate": True,
|
||||
"total": 4573, "accounted": 3100, "unclassified": 1473,
|
||||
"counts": {"canonical": 12, "instance": 2900, "variant": 0, "exempt": 188},
|
||||
"estimate": True,
|
||||
"computed_at": "2026-08-16T12:00:00+00:00",
|
||||
"largest_gaps": [
|
||||
{"dir": "internal/api", "uncovered": 40, "total": 60},
|
||||
{"dir": "web/src/components", "uncovered": 25, "total": 30},
|
||||
{"dir": "internal/api", "unclassified": 40, "total": 60},
|
||||
{"dir": "web/src/components", "unclassified": 25, "total": 30},
|
||||
],
|
||||
})
|
||||
assert "34/210 shapes recorded" in line
|
||||
assert "3100/4573 shapes accounted for" in line
|
||||
assert "12 canonical · 2900 instance · 188 exempt" in line # zero variant elided
|
||||
assert "estimate" in line
|
||||
assert "2026-08-16" in line
|
||||
assert "1473 unclassified" in line
|
||||
assert "internal/api, web/src/components" in line
|
||||
|
||||
|
||||
@@ -257,19 +259,52 @@ async def test_coverage_measures_the_tree_exactly_and_caches(seeded):
|
||||
refresh_coverage,
|
||||
)
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.code_shape import CodeShape
|
||||
|
||||
uid, pid = seeded["uid"], seeded["pid"]
|
||||
selector = _selector(_tarball(TREE))
|
||||
|
||||
coverage = await compute_coverage(uid, pid, selector=selector)
|
||||
assert coverage is not None
|
||||
assert coverage["total"] == 4
|
||||
assert coverage["recorded"] == 2
|
||||
assert coverage["accounted"] == 2
|
||||
assert coverage["unclassified"] == 2
|
||||
assert coverage["counts"] == {
|
||||
"canonical": 2, "instance": 0, "variant": 0, "exempt": 0,
|
||||
}
|
||||
assert coverage["estimate"] is True
|
||||
assert coverage["repos"] == [{
|
||||
"repo": "git.example.com/alice/widget", "ref": "main",
|
||||
"total": 4, "recorded": 2,
|
||||
"total": 4, "accounted": 2,
|
||||
}]
|
||||
assert coverage["largest_gaps"] == [{"dir": "src", "uncovered": 2, "total": 3}]
|
||||
assert coverage["largest_gaps"] == [
|
||||
{"dir": "src", "unclassified": 2, "total": 3}
|
||||
]
|
||||
|
||||
# The walk fed the LEDGER (#2788): every extracted shape has a row, the
|
||||
# snippet reference locations are mechanically stamped canonical WITH
|
||||
# their snippet id, and the rest sit in the todo state.
|
||||
async with async_session() as s:
|
||||
rows = (await s.execute(
|
||||
select(CodeShape).where(CodeShape.project_id == pid)
|
||||
)).scalars().all()
|
||||
by_symbol = {r.symbol: r for r in rows}
|
||||
assert set(by_symbol) == {"make_app", "Config", "helper", "btn"}
|
||||
assert by_symbol["make_app"].status == "canonical"
|
||||
assert by_symbol["make_app"].snippet_id is not None
|
||||
assert by_symbol["make_app"].classified_by == "mechanical"
|
||||
assert by_symbol["btn"].status == "canonical"
|
||||
assert by_symbol["Config"].status == "unclassified"
|
||||
assert by_symbol["helper"].status == "unclassified"
|
||||
assert all(r.vanished_at is None for r in rows)
|
||||
assert all(r.first_seen_commit for r in rows) # ref at minimum
|
||||
|
||||
# Idempotence: a second walk changes nothing about the readout.
|
||||
again = await compute_coverage(uid, pid, selector=_selector(_tarball(TREE)))
|
||||
assert (again["total"], again["accounted"]) == (4, 2)
|
||||
|
||||
# Nothing computed → nothing cached; refresh writes; the cache reads back
|
||||
# byte-equal, because enter_project will serve exactly this.
|
||||
@@ -278,6 +313,57 @@ async def test_coverage_measures_the_tree_exactly_and_caches(seeded):
|
||||
assert (await cached_coverage(uid, pid)) == json.loads(json.dumps(stored))
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_ledger_keeps_judgments_and_stamps_vanished_shapes(seeded):
|
||||
"""The two survival rules (#2788): an agent's classification outlives
|
||||
recompute, and a shape that leaves the tree is stamped vanished — kept
|
||||
for history, dropped from the readout."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.code_shape import CodeShape
|
||||
from scribe.services.coverage import compute_coverage
|
||||
|
||||
uid, pid = seeded["uid"], seeded["pid"]
|
||||
await compute_coverage(uid, pid, selector=_selector(_tarball(TREE)))
|
||||
|
||||
# An agent judges `helper` a deliberate one-off.
|
||||
async with async_session() as s:
|
||||
helper = (await s.execute(select(CodeShape).where(
|
||||
CodeShape.project_id == pid, CodeShape.symbol == "helper",
|
||||
))).scalar_one()
|
||||
helper.status = "exempt"
|
||||
helper.reason = "test scaffolding, deliberately local"
|
||||
helper.classified_by = "agent"
|
||||
helper.classified_at = datetime.now(timezone.utc)
|
||||
await s.commit()
|
||||
|
||||
# The tree moves on: util.py (helper) is gone entirely, app.py loses
|
||||
# nothing. The judgment on `helper` must survive AS HISTORY (vanished,
|
||||
# still exempt), never be reset by the sync.
|
||||
smaller = {k: v for k, v in TREE.items() if k != "src/util.py"}
|
||||
coverage = await compute_coverage(uid, pid, selector=_selector(_tarball(smaller)))
|
||||
assert coverage["total"] == 3 # helper's row left the readout
|
||||
assert coverage["accounted"] == 2
|
||||
assert coverage["counts"]["exempt"] == 0 # vanished rows don't count
|
||||
|
||||
async with async_session() as s:
|
||||
helper = (await s.execute(select(CodeShape).where(
|
||||
CodeShape.project_id == pid, CodeShape.symbol == "helper",
|
||||
))).scalar_one()
|
||||
assert helper.vanished_at is not None
|
||||
assert helper.status == "exempt" # the judgment is history, kept
|
||||
assert helper.reason == "test scaffolding, deliberately local"
|
||||
|
||||
# And it returns: the shape reappearing clears the stamp, judgment intact.
|
||||
coverage = await compute_coverage(uid, pid, selector=_selector(_tarball(TREE)))
|
||||
assert coverage["total"] == 4
|
||||
assert coverage["accounted"] == 3 # exempt counts as accounted again
|
||||
assert coverage["counts"]["exempt"] == 1
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_enter_project_surfaces_the_line_only_once_computed(seeded):
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
@@ -296,8 +382,10 @@ async def test_enter_project_surfaces_the_line_only_once_computed(seeded):
|
||||
after = await enter_project(project_id=pid)
|
||||
line = after["pattern_coverage"]
|
||||
assert line.startswith(
|
||||
"pattern-library coverage: 2/4 shapes recorded (estimate, computed "
|
||||
"shape accounting: 2/4 shapes accounted for — 2 canonical "
|
||||
"(estimate, computed "
|
||||
)
|
||||
assert "2 unclassified, largest: src" in line
|
||||
assert line.endswith("; largest gaps: src")
|
||||
finally:
|
||||
_user_id_ctx.reset(token)
|
||||
|
||||
Reference in New Issue
Block a user