feat(ledger): the scoped bucket — by-construction one-offs are stamped by the sync, not judged by a person (#2869, milestone 294)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 23s
CI & Build / integration (push) Failing after 25s
CI & Build / Python tests (push) Canceled after 51s
CI & Build / Build & push image (push) Canceled after 0s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 23s
CI & Build / integration (push) Failing after 25s
CI & Build / Python tests (push) Canceled after 51s
CI & Build / Build & push image (push) Canceled after 0s
The 2026-08 audit left 77% of Scribe's ledger `exempt`, most of it a Vue component's scoped <style> rules and <script setup> functions — one-offs by construction (unreachable from any other file) that add nothing when judged one by one and bury the rows a person should look at. - coverage: Definition carries its line; scoped_definitions() names, per .vue file, every sym and every css rule inside <style scoped>; ArchiveShape carries the flag. - sync: such rows are stamped status=scoped / classified_by=mechanical with the by-construction reason (history event recorded); un-stamped back to unclassified if a later tree makes them ordinary; a judgment overrides. - The machine still sees them: proposer, derive grouping, divergence, hook evidence, canonical stamping and classify_shapes_by_rule's default all treat unclassified + scoped as the unjudged set (_MECHANICAL_TODO). Only the human todo (status=unclassified) and largest_gaps exclude them. - accounting counts `scoped`; coverage line and the project card legend show it; SHAPE_STATUSES gains it (no DB CHECK on status — no migration). - shape-accounting skill documents the bucket; plugin 0.1.37. Operator decision on #2869 (2026-08-21): keep extracting everything, stamp mechanically, keep `exempt` a human judgment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -169,6 +169,38 @@ async def test_rule_form_sweeps_unclassified_rows_only_and_applies_whole(seeded)
|
||||
await classify_shapes_where(other, pid, path="src", status="exempt", reason="x")
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_sync_stamps_scoped_rows_and_unstamps_when_they_become_reachable(seeded):
|
||||
"""#2869: by-construction one-offs arrive `scoped` (mechanical), count as
|
||||
accounted, are reached by the sweep's default, and go back to the todo
|
||||
if a later tree makes them ordinary. A judgment overrides the stamp."""
|
||||
from scribe.services.coverage import ArchiveShape
|
||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||
scoped_shapes = [
|
||||
ArchiveShape("web/Card.vue", "css", "card", ".card {", "s1", ".card { x: 1 }", True),
|
||||
ArchiveShape("web/Card.vue", "sym", "load", "function load() {", "s2", "function load() {}", True),
|
||||
ArchiveShape("src/util.py", "sym", "helper", "def helper():", "s3", "def helper(): pass", False),
|
||||
]
|
||||
await sync_repo_shapes(pid, REPO, scoped_shapes, seen_marker="main")
|
||||
rows, _ = await list_project_shapes(owner, pid, path="web/Card.vue")
|
||||
assert {r.status for r in rows} == {"scoped"}
|
||||
assert all(r.classified_by == "mechanical" and "by construction" in (r.reason or "") for r in rows)
|
||||
# The human todo excludes them; the sweep's default still reaches them.
|
||||
assert (await list_project_shapes(owner, pid, status="unclassified", path="web/Card.vue"))[1] == 0
|
||||
out = await classify_shapes_where(
|
||||
owner, pid, path="web/Card.vue", status="instance", snippet_id=sid, kind="css",
|
||||
)
|
||||
assert out["classified"] == 1
|
||||
# Re-synced as ordinary: the stamped sym returns to the todo; the
|
||||
# judged css keeps its judgment.
|
||||
plain = [ArchiveShape(s.path, s.kind, s.name, s.signature, s.body_sha, s.body, False) for s in scoped_shapes]
|
||||
await sync_repo_shapes(pid, REPO, plain, seen_marker="main")
|
||||
rows, _ = await list_project_shapes(owner, pid, path="web/Card.vue")
|
||||
by_symbol = {r.symbol: r for r in rows}
|
||||
assert by_symbol["load"].status == "unclassified" and by_symbol["load"].classified_by is None
|
||||
assert by_symbol["card"].status == "instance" and by_symbol["card"].snippet_id == sid
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_list_filters_compose(seeded):
|
||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||
|
||||
@@ -258,7 +258,7 @@ async def test_coverage_measures_the_tree_exactly_and_caches(seeded):
|
||||
assert coverage["accounted"] == 2
|
||||
assert coverage["unclassified"] == 2
|
||||
assert coverage["counts"] == {
|
||||
"canonical": 2, "instance": 0, "variant": 0, "exempt": 0,
|
||||
"canonical": 2, "instance": 0, "variant": 0, "exempt": 0, "scoped": 0,
|
||||
}
|
||||
assert coverage["estimate"] is True
|
||||
assert coverage["repos"] == [{
|
||||
@@ -500,3 +500,31 @@ def test_coverage_line_names_divergence_and_recheck():
|
||||
assert line.endswith("; 1 judged shape changed since judged — recheck")
|
||||
assert "DIVERGENT" not in coverage_line(base)
|
||||
assert "recheck" not in coverage_line(base)
|
||||
|
||||
|
||||
def test_scoped_definitions_are_vue_script_setup_and_scoped_style_only():
|
||||
"""#2869: one-offs by construction — every sym in a .vue and every css
|
||||
rule inside <style scoped>; an unscoped <style> block and non-.vue files
|
||||
stay ordinary."""
|
||||
from scribe.services.coverage import extract_definitions, scoped_definitions
|
||||
vue = (
|
||||
"<script setup lang=\"ts\">\n"
|
||||
"function load() {\n return 1;\n}\n"
|
||||
"const save = async () => {\n return 2;\n};\n"
|
||||
"</script>\n\n"
|
||||
"<template><div class=\"card\"/></template>\n\n"
|
||||
"<style scoped>\n.card {\n padding: 1rem;\n}\n.title {\n margin: 0;\n}\n</style>\n"
|
||||
"<style>\n.global-toast {\n color: red;\n}\n</style>\n"
|
||||
)
|
||||
defs = extract_definitions(vue)
|
||||
names = {(d.kind, d.name) for d in defs}
|
||||
assert {("sym", "load"), ("sym", "save"), ("css", "card"), ("css", "title"), ("css", "global-toast")} <= names
|
||||
scoped = scoped_definitions("frontend/src/views/A.vue", vue, defs)
|
||||
assert scoped == {("sym", "load"), ("sym", "save"), ("css", "card"), ("css", "title")}
|
||||
# Definitions know their line, which is what the scoped-style range uses.
|
||||
assert next(d for d in defs if d.name == "card").line > next(d for d in defs if d.name == "save").line
|
||||
# Not a .vue: nothing is scoped, whatever it contains.
|
||||
assert scoped_definitions("frontend/src/assets/components.css", ".card {\n x: 1;\n}\n",
|
||||
extract_definitions(".card {\n x: 1;\n}\n")) == set()
|
||||
assert scoped_definitions("src/a.py", "def load():\n pass\n", extract_definitions("def load():\n pass\n")) == set()
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def test_the_todo_state_is_the_default():
|
||||
assert CodeShape.__table__.c.status.default.arg == "unclassified"
|
||||
assert "unclassified" in SHAPE_STATUSES
|
||||
assert set(SHAPE_STATUSES) == {
|
||||
"canonical", "instance", "variant", "exempt", "unclassified",
|
||||
"canonical", "instance", "variant", "exempt", "scoped", "unclassified",
|
||||
}
|
||||
assert set(SHAPE_CLASSIFIERS) == {
|
||||
"agent", "audit", "hook", "mechanical", "import",
|
||||
|
||||
Reference in New Issue
Block a user