feat(coverage): the line names standing work with 0 unclassified + derive_new, copies that joined a family since the previous refresh (#2899, milestone 299 step 1)
CI & Build / TypeScript typecheck (push) Successful in 40s
CI & Build / Python lint (push) Successful in 6s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m7s
CI & Build / Build & push image (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 40s
CI & Build / Python lint (push) Successful in 6s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m7s
CI & Build / Build & push image (push) Successful in 24s
Since the scoped bucket (#2869) the ledger reads 100% accounted while 439 derive rows stand; the standing block was gated on unclassified > 0 and so went silent. Build it whatever the todo count ("; standing: ..."), and add derive_new — derive-grouped rows first seen after the previous refresh stamp — so entering a project names the drift ("+2 new copies since last refresh: .error-msg in InceptionCard.vue") instead of waiting for an audit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -295,7 +295,9 @@ async def refresh_pattern_coverage(project_id: int) -> dict:
|
|||||||
Returns the accounting payload — total, accounted, counts by status,
|
Returns the accounting payload — total, accounted, counts by status,
|
||||||
unclassified, repos, largest_gaps, `proposed` (canon proposals awaiting
|
unclassified, repos, largest_gaps, `proposed` (canon proposals awaiting
|
||||||
confirmation), `derive_groups` (the biggest repeats-with-no-canon
|
confirmation), `derive_groups` (the biggest repeats-with-no-canon
|
||||||
families), `proposer` (what this refresh examined) — plus
|
families), `derive_new` (copies that joined a family since the previous
|
||||||
|
refresh — the drift to act on now: derive the canon, don't queue an
|
||||||
|
audit), `proposer` (what this refresh examined) — plus
|
||||||
`pattern_coverage`, the same one-line summary enter_project carries.
|
`pattern_coverage`, the same one-line summary enter_project carries.
|
||||||
"""
|
"""
|
||||||
uid = current_user_id()
|
uid = current_user_id()
|
||||||
|
|||||||
@@ -462,15 +462,20 @@ async def compute_coverage(
|
|||||||
await shape_ledger.apply_derive_groups(project_id)
|
await shape_ledger.apply_derive_groups(project_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("derive-first grouping failed", exc_info=True)
|
logger.warning("derive-first grouping failed", exc_info=True)
|
||||||
# The button-B pass (#2793): shapes new since the PREVIOUS computation,
|
# "Since the previous computation" — the cache's stamp. A first seed has
|
||||||
# where a canon dominates. The previous computation's stamp is the cache;
|
# none, so nothing is new then. Read once; two passes use it: the
|
||||||
# a first seed has none, so it flags nothing (everything is new then).
|
# button-B flag (#2793) and the derive-new drift count (#2899).
|
||||||
|
since = None
|
||||||
try:
|
try:
|
||||||
previous = await get_setting(user_id, f"{_CACHE_KEY_PREFIX}{project_id}")
|
previous = await get_setting(user_id, f"{_CACHE_KEY_PREFIX}{project_id}")
|
||||||
since = None
|
|
||||||
if previous:
|
if previous:
|
||||||
stamp = (json.loads(previous) or {}).get("computed_at")
|
stamp = (json.loads(previous) or {}).get("computed_at")
|
||||||
since = datetime.fromisoformat(stamp) if stamp else None
|
since = datetime.fromisoformat(stamp) if stamp else None
|
||||||
|
except Exception:
|
||||||
|
logger.warning("previous coverage stamp unreadable", exc_info=True)
|
||||||
|
# The button-B pass (#2793): shapes new since the PREVIOUS computation,
|
||||||
|
# where a canon dominates.
|
||||||
|
try:
|
||||||
await shape_ledger.flag_divergence(project_id, since=since)
|
await shape_ledger.flag_divergence(project_id, since=since)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("divergence pass failed", exc_info=True)
|
logger.warning("divergence pass failed", exc_info=True)
|
||||||
@@ -491,6 +496,7 @@ async def compute_coverage(
|
|||||||
unclassified = counts.pop("unclassified")
|
unclassified = counts.pop("unclassified")
|
||||||
proposals = shape_ledger.proposal_summary(rows)
|
proposals = shape_ledger.proposal_summary(rows)
|
||||||
divergence = shape_ledger.divergence_summary(rows)
|
divergence = shape_ledger.divergence_summary(rows)
|
||||||
|
derive_new = shape_ledger.derive_new_summary(rows, since=since)
|
||||||
return {
|
return {
|
||||||
"total": len(rows),
|
"total": len(rows),
|
||||||
"accounted": len(rows) - unclassified,
|
"accounted": len(rows) - unclassified,
|
||||||
@@ -501,6 +507,10 @@ async def compute_coverage(
|
|||||||
"proposed": proposals["proposed"],
|
"proposed": proposals["proposed"],
|
||||||
"derive_groups": proposals["derive_groups"],
|
"derive_groups": proposals["derive_groups"],
|
||||||
"top_canon": proposals.get("top_canon"),
|
"top_canon": proposals.get("top_canon"),
|
||||||
|
# Drift since the previous refresh (#2899): copies that joined a
|
||||||
|
# duplicate family — what the arrival line names so drift is noticed
|
||||||
|
# on entering, not found by an audit.
|
||||||
|
"derive_new": derive_new,
|
||||||
"proposer": proposer_stats,
|
"proposer": proposer_stats,
|
||||||
# The divergence readout (#2793): button B where button A is canon,
|
# The divergence readout (#2793): button B where button A is canon,
|
||||||
# and judged shapes whose bodies moved since they were judged.
|
# and judged shapes whose bodies moved since they were judged.
|
||||||
@@ -648,29 +658,46 @@ def coverage_line(coverage: dict) -> str:
|
|||||||
line += f" — {breakdown}"
|
line += f" — {breakdown}"
|
||||||
line += f" (estimate{', computed ' + day if day else ''})"
|
line += f" (estimate{', computed ' + day if day else ''})"
|
||||||
unclassified = coverage.get("unclassified", 0)
|
unclassified = coverage.get("unclassified", 0)
|
||||||
|
# The standing work, built whatever the todo count (#2899). Since the
|
||||||
|
# scoped bucket (#2869) a ledger can read 100% accounted and still carry
|
||||||
|
# derive groups, proposals and divergence; gating this block on
|
||||||
|
# `unclassified > 0` is how 439 derive rows went unmentioned.
|
||||||
|
standing = []
|
||||||
|
if coverage.get("proposed"):
|
||||||
|
standing.append(f"{coverage['proposed']} proposed")
|
||||||
|
n_groups = len(coverage.get("derive_groups") or [])
|
||||||
|
if n_groups:
|
||||||
|
standing.append(f"{n_groups} derive group{'s' if n_groups != 1 else ''}")
|
||||||
|
# Drift since the previous refresh: copies that joined a family, the
|
||||||
|
# first one named — the sentence the arrival moment exists to say.
|
||||||
|
new = coverage.get("derive_new") or {}
|
||||||
|
if new.get("count"):
|
||||||
|
n = new["count"]
|
||||||
|
first_new = (new.get("examples") or [{}])[0]
|
||||||
|
where = (
|
||||||
|
f": {first_new['label']} in {first_new['path']}"
|
||||||
|
if first_new.get("label") and first_new.get("path") else ""
|
||||||
|
)
|
||||||
|
standing.append(f"+{n} new cop{'y' if n == 1 else 'ies'} since last refresh{where}")
|
||||||
|
if coverage.get("divergent"):
|
||||||
|
standing.append(f"{coverage['divergent']} DIVERGENT")
|
||||||
|
# The next action, on the line (#2874): the canon with the biggest
|
||||||
|
# queue to confirm, and the widest body-identical copy to consolidate.
|
||||||
|
top = coverage.get("top_canon") or {}
|
||||||
|
if top.get("snippet_id"):
|
||||||
|
standing.append(f"top canon #{top['snippet_id']} ×{top.get('count', 0)}")
|
||||||
|
first = (coverage.get("derive_groups") or [{}])[0]
|
||||||
|
if first.get("label") and first.get("files"):
|
||||||
|
standing.append(f"top copy {first['label']} ×{first['files']} files")
|
||||||
if unclassified:
|
if unclassified:
|
||||||
line += f"; {unclassified} unclassified"
|
line += f"; {unclassified} unclassified"
|
||||||
standing = []
|
|
||||||
if coverage.get("proposed"):
|
|
||||||
standing.append(f"{coverage['proposed']} proposed")
|
|
||||||
n_groups = len(coverage.get("derive_groups") or [])
|
|
||||||
if n_groups:
|
|
||||||
standing.append(f"{n_groups} derive group{'s' if n_groups != 1 else ''}")
|
|
||||||
if coverage.get("divergent"):
|
|
||||||
standing.append(f"{coverage['divergent']} DIVERGENT")
|
|
||||||
# The next action, on the line (#2874): the canon with the biggest
|
|
||||||
# queue to confirm, and the widest body-identical copy to consolidate.
|
|
||||||
top = coverage.get("top_canon") or {}
|
|
||||||
if top.get("snippet_id"):
|
|
||||||
standing.append(f"top canon #{top['snippet_id']} ×{top.get('count', 0)}")
|
|
||||||
first = (coverage.get("derive_groups") or [{}])[0]
|
|
||||||
if first.get("label") and first.get("files"):
|
|
||||||
standing.append(f"top copy {first['label']} ×{first['files']} files")
|
|
||||||
if standing:
|
if standing:
|
||||||
line += f" ({', '.join(standing)})"
|
line += f" ({', '.join(standing)})"
|
||||||
gaps = [g["dir"] for g in coverage.get("largest_gaps") or []]
|
gaps = [g["dir"] for g in coverage.get("largest_gaps") or []]
|
||||||
if gaps:
|
if gaps:
|
||||||
line += ", largest: " + ", ".join(gaps)
|
line += ", largest: " + ", ".join(gaps)
|
||||||
|
elif standing:
|
||||||
|
line += f"; standing: {', '.join(standing)}"
|
||||||
if coverage.get("recheck"):
|
if coverage.get("recheck"):
|
||||||
line += f"; {coverage['recheck']} judged shape{'s' if coverage['recheck'] != 1 else ''} changed since judged — recheck"
|
line += f"; {coverage['recheck']} judged shape{'s' if coverage['recheck'] != 1 else ''} changed since judged — recheck"
|
||||||
return line
|
return line
|
||||||
|
|||||||
@@ -1404,6 +1404,34 @@ def proposal_summary(rows: Iterable[CodeShape], *, top: int = 8) -> dict:
|
|||||||
return {"proposed": proposed, "derive_groups": ranked[:top], "top_canon": top_canon}
|
return {"proposed": proposed, "derive_groups": ranked[:top], "top_canon": top_canon}
|
||||||
|
|
||||||
|
|
||||||
|
def derive_new_summary(
|
||||||
|
rows: Iterable[CodeShape], *, since: datetime | None, top: int = 3
|
||||||
|
) -> dict:
|
||||||
|
"""The arrival-moment drift signal (#2899): derive-grouped rows FIRST
|
||||||
|
SEEN after ``since`` — the previous refresh's stamp, the same one
|
||||||
|
flag_divergence uses. "Since the last refresh, N more copies joined a
|
||||||
|
duplicate family" is the sentence that makes the derive queue a thing
|
||||||
|
you notice on entering, not a thing an audit finds. ``since`` None (a
|
||||||
|
first seed) means nothing is new. Judged rows never count."""
|
||||||
|
if since is None:
|
||||||
|
return {"count": 0, "examples": []}
|
||||||
|
fresh = [
|
||||||
|
r for r in rows
|
||||||
|
if r.proposal_basis == "derive" and r.proposal_group
|
||||||
|
and r.status in _MECHANICAL_TODO and r.vanished_at is None
|
||||||
|
and r.created_at is not None and r.created_at > since
|
||||||
|
]
|
||||||
|
fresh.sort(key=lambda r: r.created_at, reverse=True)
|
||||||
|
return {
|
||||||
|
"count": len(fresh),
|
||||||
|
"examples": [
|
||||||
|
{"label": ("." if r.kind == "css" else "") + r.symbol,
|
||||||
|
"path": r.path, "group": r.proposal_group}
|
||||||
|
for r in fresh[:top]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def confirm_proposals(
|
async def confirm_proposals(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
project_id: int,
|
project_id: int,
|
||||||
|
|||||||
@@ -597,6 +597,39 @@ async def test_derive_groups_land_on_rows_and_in_the_summary(seeded):
|
|||||||
assert {r.symbol for r in rows} == {"slug"} # 2 files < the name floor
|
assert {r.symbol for r in rows} == {"slug"} # 2 files < the name floor
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_derive_new_names_the_copy_that_joined_a_family_since_the_stamp(seeded):
|
||||||
|
"""#2899: the first sync seeds one `slug`; a later sync adds an identical
|
||||||
|
copy. Against the stamp between them, derive_new counts ONLY the
|
||||||
|
newcomer — the drift since the last refresh, not the whole family."""
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from scribe.services.shape_ledger import (
|
||||||
|
apply_derive_groups, derive_new_summary, live_rows,
|
||||||
|
)
|
||||||
|
|
||||||
|
owner, pid = seeded["owner"], seeded["pid"]
|
||||||
|
first = _defs(
|
||||||
|
("a/one.py", "sym", "slug", "def slug(t):", "def slug(t):\n return t.lower()"),
|
||||||
|
)
|
||||||
|
await sync_repo_shapes(pid, REPO, first, seen_marker="m1")
|
||||||
|
stamp = datetime.now(timezone.utc)
|
||||||
|
second = _defs(
|
||||||
|
("a/one.py", "sym", "slug", "def slug(t):", "def slug(t):\n return t.lower()"),
|
||||||
|
("a/two.py", "sym", "slug", "def slug(t):", "def slug(t):\n return t.lower()"),
|
||||||
|
)
|
||||||
|
await sync_repo_shapes(pid, REPO, second, seen_marker="m2")
|
||||||
|
assert await apply_derive_groups(pid) == 2
|
||||||
|
|
||||||
|
rows = await live_rows(pid)
|
||||||
|
out = derive_new_summary(rows, since=stamp)
|
||||||
|
assert out["count"] == 1
|
||||||
|
assert out["examples"][0]["path"] == "a/two.py"
|
||||||
|
assert out["examples"][0]["label"] == "slug"
|
||||||
|
assert out["examples"][0]["group"].startswith("dup:")
|
||||||
|
assert derive_new_summary(rows, since=None)["count"] == 0
|
||||||
|
|
||||||
|
|
||||||
# --- #2793: the divergence readout against real rows -------------------------
|
# --- #2793: the divergence readout against real rows -------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -276,6 +276,8 @@ async def test_coverage_measures_the_tree_exactly_and_caches(seeded):
|
|||||||
assert coverage["largest_gaps"] == [
|
assert coverage["largest_gaps"] == [
|
||||||
{"dir": "src", "unclassified": 2, "total": 3}
|
{"dir": "src", "unclassified": 2, "total": 3}
|
||||||
]
|
]
|
||||||
|
# #2899: a first computation has no previous stamp — nothing is "new".
|
||||||
|
assert coverage["derive_new"] == {"count": 0, "examples": []}
|
||||||
|
|
||||||
# The walk fed the LEDGER (#2788): every extracted shape has a row, the
|
# The walk fed the LEDGER (#2788): every extracted shape has a row, the
|
||||||
# snippet reference locations are mechanically stamped canonical WITH
|
# snippet reference locations are mechanically stamped canonical WITH
|
||||||
@@ -512,6 +514,41 @@ def test_coverage_line_names_the_proposers_standing():
|
|||||||
assert "top canon #2844 ×78" in line and "top copy closed-msg (identical body) ×3 files" in line
|
assert "top canon #2844 ×78" in line and "top copy closed-msg (identical body) ×3 files" in line
|
||||||
|
|
||||||
|
|
||||||
|
def test_coverage_line_shows_standing_work_even_with_nothing_unclassified():
|
||||||
|
"""#2899: since the scoped bucket a ledger can be fully accounted and
|
||||||
|
still carry derive groups / proposals / divergence — the line names
|
||||||
|
them as `standing:` instead of hiding them behind the todo count, and
|
||||||
|
names the drift since the previous refresh first-copy-first."""
|
||||||
|
from scribe.services.coverage import coverage_line
|
||||||
|
|
||||||
|
base = {
|
||||||
|
"total": 4693, "accounted": 4693, "unclassified": 0,
|
||||||
|
"counts": {"canonical": 37, "instance": 977, "variant": 73, "exempt": 1797, "scoped": 1809},
|
||||||
|
"computed_at": "2026-08-22T00:00:00+00:00", "largest_gaps": [],
|
||||||
|
}
|
||||||
|
quiet = coverage_line(base)
|
||||||
|
assert "unclassified" not in quiet and "standing" not in quiet
|
||||||
|
line = coverage_line({
|
||||||
|
**base,
|
||||||
|
"derive_groups": [{"group": "dup:abc", "label": "log-empty (identical body)", "files": 4}],
|
||||||
|
"derive_new": {"count": 2, "examples": [
|
||||||
|
{"label": ".error-msg", "path": "frontend/src/components/InceptionCard.vue", "group": "dup:9f0"},
|
||||||
|
{"label": ".error-msg", "path": "frontend/src/components/Other.vue", "group": "dup:9f0"},
|
||||||
|
]},
|
||||||
|
"divergent": 1,
|
||||||
|
})
|
||||||
|
assert "; standing: 1 derive group, +2 new copies since last refresh: .error-msg in " \
|
||||||
|
"frontend/src/components/InceptionCard.vue, 1 DIVERGENT, top copy log-empty (identical body) ×4 files" in line
|
||||||
|
assert "unclassified" not in line
|
||||||
|
# One copy reads singular; with a todo the block keeps its old place.
|
||||||
|
one = coverage_line({**base, "derive_new": {"count": 1, "examples": []}})
|
||||||
|
assert one.endswith("; standing: +1 new copy since last refresh")
|
||||||
|
todo = coverage_line({**base, "unclassified": 3, "accounted": 4690, "proposed": 2,
|
||||||
|
"derive_new": {"count": 1, "examples": [{"label": "x", "path": "a.py"}]},
|
||||||
|
"largest_gaps": [{"dir": "src", "unclassified": 3, "total": 9}]})
|
||||||
|
assert "; 3 unclassified (2 proposed, +1 new copy since last refresh: x in a.py), largest: src" in todo
|
||||||
|
|
||||||
|
|
||||||
def test_coverage_line_names_divergence_and_recheck():
|
def test_coverage_line_names_divergence_and_recheck():
|
||||||
from scribe.services.coverage import coverage_line
|
from scribe.services.coverage import coverage_line
|
||||||
|
|
||||||
|
|||||||
@@ -317,6 +317,38 @@ def test_derive_groups_copy_before_name_with_floors():
|
|||||||
assert ("i.py", "sym", "one") not in g
|
assert ("i.py", "sym", "one") not in g
|
||||||
|
|
||||||
|
|
||||||
|
def test_derive_new_summary_counts_copies_first_seen_since_the_previous_refresh():
|
||||||
|
"""#2899: the arrival-moment drift signal — derive-grouped rows created
|
||||||
|
after the previous refresh's stamp, newest first, judged rows and a
|
||||||
|
first seed (since=None) never count."""
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
from scribe.models.code_shape import CodeShape
|
||||||
|
from scribe.services.shape_ledger import derive_new_summary
|
||||||
|
|
||||||
|
t0 = datetime(2026, 8, 22, 12, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
def row(path, symbol, at, kind="css", status="scoped", group="dup:abc", basis="derive"):
|
||||||
|
r = CodeShape(project_id=2, repo_key="r", path=path, symbol=symbol, kind=kind,
|
||||||
|
status=status, proposal_basis=basis, proposal_group=group)
|
||||||
|
r.created_at = at
|
||||||
|
return r
|
||||||
|
rows = [
|
||||||
|
row("v/Old.vue", "error-msg", t0 - timedelta(days=3)), # before the stamp
|
||||||
|
row("v/InceptionCard.vue", "error-msg", t0 + timedelta(hours=1)), # new copy
|
||||||
|
row("v/Other.vue", "error-msg", t0 + timedelta(hours=2)), # newer copy
|
||||||
|
row("v/J.vue", "error-msg", t0 + timedelta(hours=3), status="exempt"), # judged: never
|
||||||
|
row("s/a.py", "load", t0 + timedelta(hours=1), kind="sym", group=None, basis=None), # no family
|
||||||
|
]
|
||||||
|
out = derive_new_summary(rows, since=t0)
|
||||||
|
assert out["count"] == 2
|
||||||
|
assert [e["path"] for e in out["examples"]] == ["v/Other.vue", "v/InceptionCard.vue"]
|
||||||
|
assert out["examples"][0] == {"label": ".error-msg", "path": "v/Other.vue", "group": "dup:abc"}
|
||||||
|
assert derive_new_summary(rows, since=None) == {"count": 0, "examples": []}
|
||||||
|
assert derive_new_summary(rows, since=t0, top=1)["examples"] == [
|
||||||
|
{"label": ".error-msg", "path": "v/Other.vue", "group": "dup:abc"}]
|
||||||
|
|
||||||
|
|
||||||
def test_proposal_summary_ranks_body_identical_groups_first_and_sees_scoped_rows():
|
def test_proposal_summary_ranks_body_identical_groups_first_and_sees_scoped_rows():
|
||||||
"""#2872: dup groups (the real copies) outrank name groups (usually
|
"""#2872: dup groups (the real copies) outrank name groups (usually
|
||||||
convention), wider spread first; #2869: scoped rows are in the readout."""
|
convention), wider spread first; #2869: scoped rows are in the readout."""
|
||||||
|
|||||||
Reference in New Issue
Block a user