feat(ledger): reason codes, case-insensitive repo filter, next action on the coverage line (#2874, milestone 294)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Canceled after 32s
CI & Build / Python tests (push) Canceled after 31s
CI & Build / Build & push image (push) Canceled after 0s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Canceled after 32s
CI & Build / Python tests (push) Canceled after 31s
CI & Build / Build & push image (push) Canceled after 0s
- code_shapes.reason_code (migration 0083): an optional code from a fixed catalogue (scoped-css, one-off-handler, test-helper, convention-plumbing, pure-helper, generated, script, typed-record) beside the prose reason, so the ledger can be filtered/aggregated by kind of one-off; validated in classify_shapes and classify_shapes_by_rule; on to_dict/to_compact. - Snippet location lookups match repo case-insensitively in both dialects (location_matches / location_jsonpath via like_regex flag "i") — "Scribe" vs "FabledScribe" vs "fabledscribe" recorded free-form hid half the canon from list_snippets(repo=, path=). - coverage line names the next action: "top canon #N ×k" (biggest proposal queue) and "top copy <label> ×files" (widest body-identical group). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,7 @@ from typing import Iterable, NamedTuple
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.code_shape import CodeShape, CodeShapeEvent
|
||||
from scribe.models.code_shape import REASON_CODES, CodeShape, CodeShapeEvent
|
||||
from scribe.models.base import iso
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -183,7 +183,7 @@ def _event(row: CodeShape, event: str, at: datetime, *, commit: str = "") -> Cod
|
||||
|
||||
async def _judge(
|
||||
session, row: CodeShape, *, status: str, snippet_id: int | None,
|
||||
by: str | None, reason: str | None, at: datetime,
|
||||
by: str | None, reason: str | None, at: datetime, reason_code: str | None = None,
|
||||
) -> None:
|
||||
"""Apply a judgment to a row — the ONE place a status is set — and write
|
||||
its history. Clears what a judgment settles: the standing proposal, the
|
||||
@@ -194,6 +194,7 @@ async def _judge(
|
||||
row.status = status
|
||||
row.snippet_id = snippet_id if status in _NEEDS_TARGET else None
|
||||
row.reason = (reason or "").strip() or None
|
||||
row.reason_code = (reason_code or "").strip() or None
|
||||
row.classified_by = by if status != "unclassified" else None
|
||||
row.classified_at = at if status != "unclassified" else None
|
||||
row.classified_sha = row.body_sha if status != "unclassified" else ""
|
||||
@@ -314,6 +315,12 @@ def validate_classifications(items: list[dict]) -> str | None:
|
||||
f"classifications[{i}]: status {status!r} needs a reason — "
|
||||
"the WHY is the record (note 2786)"
|
||||
)
|
||||
code = (item.get("reason_code") or "").strip()
|
||||
if code and code not in REASON_CODES:
|
||||
return (
|
||||
f"classifications[{i}]: unknown reason_code {code!r} "
|
||||
f"(one of: {', '.join(REASON_CODES)})"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -389,6 +396,7 @@ async def classify_shapes(
|
||||
session, row, status=status,
|
||||
snippet_id=int(item["snippet_id"]) if status in _NEEDS_TARGET else None,
|
||||
by=via, reason=item.get("reason"), at=now,
|
||||
reason_code=item.get("reason_code"),
|
||||
)
|
||||
classified += 1
|
||||
await session.commit()
|
||||
@@ -424,6 +432,7 @@ async def classify_shapes_where(
|
||||
reason: str | None = None,
|
||||
via: str = "agent",
|
||||
include_judged: bool = False,
|
||||
reason_code: str | None = None,
|
||||
) -> dict:
|
||||
"""The sweep form of classify_shapes (#2868): one judgment applied to
|
||||
every live row under ``path`` whose symbol matches ``pattern`` (and
|
||||
@@ -443,7 +452,8 @@ async def classify_shapes_where(
|
||||
if status == "canonical":
|
||||
raise ValueError("canonical is the sync's stamp on a snippet's own location — a sweep cannot set it")
|
||||
probe = {"path": path, "symbol": "*", "status": status,
|
||||
"snippet_id": snippet_id or 0, "reason": reason or ""}
|
||||
"snippet_id": snippet_id or 0, "reason": reason or "",
|
||||
"reason_code": reason_code or ""}
|
||||
error = validate_classifications([probe])
|
||||
if error:
|
||||
raise ValueError(error.replace("classifications[0]", "rule"))
|
||||
@@ -465,7 +475,7 @@ async def classify_shapes_where(
|
||||
await _judge(
|
||||
session, row, status=status,
|
||||
snippet_id=int(snippet_id) if status in _NEEDS_TARGET else None,
|
||||
by=via, reason=reason, at=now,
|
||||
by=via, reason=reason, at=now, reason_code=reason_code,
|
||||
)
|
||||
judged.append(f"{row.path}::{row.symbol}")
|
||||
await session.commit()
|
||||
@@ -1214,6 +1224,7 @@ def proposal_summary(rows: Iterable[CodeShape], *, top: int = 8) -> dict:
|
||||
"""The readout's view of the proposer's standing: how many canon
|
||||
proposals await confirmation, and the largest derive-first groups."""
|
||||
proposed = 0
|
||||
by_canon: dict[int, int] = {}
|
||||
groups: dict[str, dict] = {}
|
||||
files: dict[str, set[str]] = {}
|
||||
for row in rows:
|
||||
@@ -1221,6 +1232,7 @@ def proposal_summary(rows: Iterable[CodeShape], *, top: int = 8) -> dict:
|
||||
continue
|
||||
if row.proposed_snippet_id is not None:
|
||||
proposed += 1
|
||||
by_canon[row.proposed_snippet_id] = by_canon.get(row.proposed_snippet_id, 0) + 1
|
||||
elif row.proposal_group:
|
||||
dup = not row.proposal_group.startswith("name:")
|
||||
g = groups.setdefault(row.proposal_group, {
|
||||
@@ -1245,7 +1257,11 @@ def proposal_summary(rows: Iterable[CodeShape], *, top: int = 8) -> dict:
|
||||
groups.values(),
|
||||
key=lambda g: (g["group"].startswith("name:"), -g["files"], -g["size"], g["group"]),
|
||||
)
|
||||
return {"proposed": proposed, "derive_groups": ranked[:top]}
|
||||
top_canon = None
|
||||
if by_canon:
|
||||
sid, n = max(by_canon.items(), key=lambda kv: (kv[1], -kv[0]))
|
||||
top_canon = {"snippet_id": sid, "count": n}
|
||||
return {"proposed": proposed, "derive_groups": ranked[:top], "top_canon": top_canon}
|
||||
|
||||
|
||||
async def confirm_proposals(
|
||||
|
||||
Reference in New Issue
Block a user