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:
@@ -754,7 +754,7 @@ async function confirmDelete() {
|
||||
</div>
|
||||
<div v-if="coverage.counts" class="coverage-gaps">
|
||||
<span
|
||||
v-for="k in ['canonical', 'instance', 'variant', 'exempt']"
|
||||
v-for="k in ['canonical', 'instance', 'variant', 'exempt', 'scoped']"
|
||||
:key="k"
|
||||
>
|
||||
<span v-if="coverage.counts[k]" class="coverage-gap-chip">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "scribe",
|
||||
"description": "Scribe system-of-record for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your always-on rules + active-project context, process-skills (writing-plans, systematic-debugging, verification, brainstorming, reusing-code), and your saved Scribe Processes auto-surfaced as skills (/scribe:sync). Replaces superpowers + file-memory with one app-backed plugin.",
|
||||
"version": "0.1.36",
|
||||
"version": "0.1.37",
|
||||
"author": { "name": "Bryan Van Deusen" },
|
||||
"mcpServers": {
|
||||
"scribe": {
|
||||
|
||||
@@ -17,6 +17,11 @@ row carries a status:
|
||||
— the why IS the record.
|
||||
- `exempt` — judged genuinely one-off. **Reason required.** A recorded
|
||||
judgment, not silence — it stops the next pass re-litigating it.
|
||||
- `scoped` — one-off **by construction**, stamped by the coverage sync
|
||||
(a Vue component's scoped `<style>` rules and its `<script setup>`
|
||||
functions — unreachable from any other file). Accounted for without a
|
||||
judgment; still proposed against, grouped and flagged; any judgment you
|
||||
make overrides it. Not the todo.
|
||||
- `unclassified` — nobody has judged it yet. **This is the todo list.**
|
||||
|
||||
## The loop
|
||||
|
||||
@@ -72,7 +72,12 @@ async def list_shapes(
|
||||
(fed by the coverage refresh). Filters compose:
|
||||
|
||||
Args:
|
||||
status: canonical | instance | variant | exempt | unclassified.
|
||||
status: canonical | instance | variant | exempt | scoped | unclassified.
|
||||
`scoped` (#2869) is the sync's mechanical stamp on one-offs by
|
||||
construction (a Vue component's scoped <style> rules and its
|
||||
<script setup> functions): accounted for, not judged, still
|
||||
proposed against / grouped / flagged, and overridable by any
|
||||
classify_shapes judgment. The human todo is `unclassified`.
|
||||
path: exact file, or a directory — matches everything beneath it
|
||||
(the coverage line's "largest" dirs go straight in here).
|
||||
snippet_id: rows classified against this snippet — a consumer map.
|
||||
@@ -162,7 +167,8 @@ async def classify_shapes_by_rule(
|
||||
snippet_id: Required for instance/variant — the canon judged against.
|
||||
reason: Required for variant/exempt — the why, recorded on every row.
|
||||
via: "agent" (default) | "audit" | "import".
|
||||
include_judged: By default only `unclassified` rows are touched — a
|
||||
include_judged: By default only unjudged rows are touched —
|
||||
`unclassified` and the sync's mechanical `scoped` stamp — a
|
||||
sweep never silently overwrites a judgment. True re-judges every
|
||||
matching live row (use to re-confirm after a recheck, or to
|
||||
revise a family you judged earlier).
|
||||
|
||||
@@ -16,8 +16,14 @@ from scribe.models import Base
|
||||
from scribe.models.base import TimestampMixin, iso
|
||||
|
||||
# The classification vocabulary (note 2786). `unclassified` is the default and
|
||||
# THE todo state; every other status is a judgment, stamped with who made it.
|
||||
SHAPE_STATUSES = ("canonical", "instance", "variant", "exempt", "unclassified")
|
||||
# THE todo state; every other status is a judgment, stamped with who made it —
|
||||
# except `scoped` (#2869): the coverage sync's mechanical stamp on shapes that
|
||||
# are one-offs BY CONSTRUCTION (a Vue component's scoped <style> rules and its
|
||||
# <script setup> functions — unreachable from any other file). Scoped rows are
|
||||
# accounted for without a human judging them, so `exempt` keeps meaning "a
|
||||
# person looked"; the proposer, derive grouping and divergence still see them,
|
||||
# and any judgment (instance/variant/exempt) overrides the stamp.
|
||||
SHAPE_STATUSES = ("canonical", "instance", "variant", "exempt", "scoped", "unclassified")
|
||||
SHAPE_CLASSIFIERS = ("agent", "audit", "hook", "mechanical", "import")
|
||||
|
||||
# How the mechanical proposer (#2792) arrived at a proposal, strongest first.
|
||||
|
||||
@@ -107,6 +107,7 @@ class Definition(NamedTuple):
|
||||
signature: str
|
||||
body_sha: str
|
||||
body: str
|
||||
line: int = -1 # 0-based line the definition starts on (#2869)
|
||||
|
||||
|
||||
def _definition_on(raw: str) -> tuple[str, str] | None:
|
||||
@@ -186,11 +187,47 @@ def extract_definitions(text: str) -> list[Definition]:
|
||||
block = lines[i:end]
|
||||
out.append(Definition(
|
||||
kind, name, lines[i].strip()[:_SIGNATURE_CAP], _block_sha(block),
|
||||
"\n".join(block),
|
||||
"\n".join(block), i,
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
# --- by-construction scope (#2869) -------------------------------------------
|
||||
#
|
||||
# A Vue single-file component's `<style scoped>` rules and its `<script setup>`
|
||||
# functions cannot be reached from any other file: they are one-offs by
|
||||
# construction, not by judgment. The sync stamps them `scoped` (mechanical) so
|
||||
# the human todo holds only shapes a person should look at, while the bodies
|
||||
# stay in play for the proposer, derive grouping and divergence — the five
|
||||
# auth views' identical rules were found exactly there. Unscoped `<style>` in
|
||||
# a .vue and every non-.vue file stay ordinary.
|
||||
_STYLE_OPEN_RE = re.compile(r"^\s*<style\b[^>]*\bscoped\b", re.IGNORECASE)
|
||||
_STYLE_CLOSE_RE = re.compile(r"^\s*</style\s*>", re.IGNORECASE)
|
||||
|
||||
|
||||
def scoped_definitions(path: str, text: str, defs: list[Definition]) -> set[tuple[str, str]]:
|
||||
"""The (kind, name) pairs among ``defs`` that are one-offs by
|
||||
construction in this file: every sym in a .vue, and every css rule
|
||||
that starts inside a `<style scoped>` block. Empty for other files."""
|
||||
if not (path or "").lower().endswith(".vue"):
|
||||
return set()
|
||||
ranges: list[tuple[int, int]] = []
|
||||
open_at: int | None = None
|
||||
for i, ln in enumerate(text.splitlines()):
|
||||
if open_at is None and _STYLE_OPEN_RE.match(ln):
|
||||
open_at = i
|
||||
elif open_at is not None and _STYLE_CLOSE_RE.match(ln):
|
||||
ranges.append((open_at, i))
|
||||
open_at = None
|
||||
out: set[tuple[str, str]] = set()
|
||||
for d in defs:
|
||||
if d.kind == "sym":
|
||||
out.add((d.kind, d.name))
|
||||
elif any(a <= d.line <= b for a, b in ranges):
|
||||
out.add((d.kind, d.name))
|
||||
return out
|
||||
|
||||
|
||||
def extract_shapes(text: str) -> list[tuple[str, str]]:
|
||||
"""Every (kind, name) this text DEFINES — kind is "css" or "sym".
|
||||
|
||||
@@ -220,6 +257,7 @@ class ArchiveShape(NamedTuple):
|
||||
signature: str
|
||||
body_sha: str
|
||||
body: str
|
||||
scoped: bool = False # one-off by construction (#2869)
|
||||
|
||||
|
||||
def shapes_from_archive(blob: bytes) -> list[tuple[str, str, str]]:
|
||||
@@ -250,9 +288,14 @@ def definitions_from_archive(blob: bytes) -> list[ArchiveShape]:
|
||||
text = handle.read().decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
defs = extract_definitions(text)
|
||||
scoped = scoped_definitions(path, text, defs)
|
||||
shapes.extend(
|
||||
ArchiveShape(path, d.kind, d.name, d.signature, d.body_sha, d.body)
|
||||
for d in extract_definitions(text)
|
||||
ArchiveShape(
|
||||
path, d.kind, d.name, d.signature, d.body_sha, d.body,
|
||||
(d.kind, d.name) in scoped,
|
||||
)
|
||||
for d in defs
|
||||
)
|
||||
return shapes
|
||||
|
||||
@@ -414,7 +457,7 @@ async def compute_coverage(
|
||||
# repo that was unreachable today still has live rows, and they count.
|
||||
rows = await shape_ledger.live_rows(project_id)
|
||||
counts = {"canonical": 0, "instance": 0, "variant": 0, "exempt": 0,
|
||||
"unclassified": 0}
|
||||
"scoped": 0, "unclassified": 0}
|
||||
for row in rows:
|
||||
counts[row.status] = counts.get(row.status, 0) + 1
|
||||
by_repo: dict[str, dict[str, int]] = {}
|
||||
@@ -571,7 +614,7 @@ def coverage_line(coverage: dict) -> str:
|
||||
counts = coverage.get("counts") or {}
|
||||
breakdown = " · ".join(
|
||||
f"{counts[k]} {k}"
|
||||
for k in ("canonical", "instance", "variant", "exempt")
|
||||
for k in ("canonical", "instance", "variant", "exempt", "scoped")
|
||||
if counts.get(k)
|
||||
)
|
||||
line = (
|
||||
|
||||
@@ -65,6 +65,17 @@ def location_covers(loc_path: str, loc_symbol: str, path: str, name: str) -> boo
|
||||
return _path_touches(loc_path, path)
|
||||
|
||||
|
||||
# The rows the machine may still speak about: nobody's judgment stands on
|
||||
# them. `scoped` (#2869) is the sync's own by-construction stamp — the
|
||||
# proposer, derive grouping, divergence, hook evidence and sweeps treat it
|
||||
# like the todo; only the human todo (`unclassified`) excludes it.
|
||||
_MECHANICAL_TODO = ("unclassified", "scoped")
|
||||
_SCOPED_REASON = (
|
||||
"by construction: a Vue component's scoped <style> rule / <script setup> "
|
||||
"function — unreachable from any other file (stamped by the coverage sync)"
|
||||
)
|
||||
|
||||
|
||||
async def sync_repo_shapes(
|
||||
project_id: int,
|
||||
repo_key: str,
|
||||
@@ -76,7 +87,9 @@ async def sync_repo_shapes(
|
||||
|
||||
``shapes`` are (path, kind, name) triples, or the richer ArchiveShape
|
||||
records (#2792) whose 4th/5th fields — signature, body_sha — refresh the
|
||||
row's content fingerprint. ``seen_marker`` is the commit the archive was
|
||||
row's content fingerprint, and whose 7th (#2869) says the shape is a
|
||||
one-off by construction: such rows are stamped `scoped` (mechanical)
|
||||
while unjudged, and un-stamped if a later tree makes them reachable. ``seen_marker`` is the commit the archive was
|
||||
read at when the forge can say, else the ref name — provenance sugar;
|
||||
the row timestamps carry the when.
|
||||
"""
|
||||
@@ -96,20 +109,32 @@ async def sync_repo_shapes(
|
||||
path, kind, name = shape[0], shape[1], shape[2]
|
||||
signature = shape[3] if len(shape) > 3 else ""
|
||||
body_sha = shape[4] if len(shape) > 4 else ""
|
||||
scoped = bool(shape[6]) if len(shape) > 6 else False
|
||||
key = (path, name, kind)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
row = by_key.get(key)
|
||||
if row is None:
|
||||
session.add(CodeShape(
|
||||
row = CodeShape(
|
||||
project_id=project_id, repo_key=repo_key,
|
||||
path=path, symbol=name, kind=kind,
|
||||
first_seen_commit=seen_marker, last_seen_commit=seen_marker,
|
||||
signature=signature, body_sha=body_sha,
|
||||
))
|
||||
)
|
||||
if scoped:
|
||||
await _judge(session, row, status="scoped", snippet_id=None,
|
||||
by="mechanical", reason=_SCOPED_REASON, at=now)
|
||||
else:
|
||||
session.add(row)
|
||||
continue
|
||||
row.last_seen_commit = seen_marker
|
||||
if scoped and row.status == "unclassified":
|
||||
await _judge(session, row, status="scoped", snippet_id=None,
|
||||
by="mechanical", reason=_SCOPED_REASON, at=now)
|
||||
elif not scoped and row.status == "scoped":
|
||||
await _judge(session, row, status="unclassified", snippet_id=None,
|
||||
by=None, reason=None, at=now)
|
||||
if signature:
|
||||
row.signature = signature
|
||||
if body_sha and body_sha != row.body_sha:
|
||||
@@ -214,7 +239,7 @@ async def mark_canonicals(
|
||||
),
|
||||
None,
|
||||
)
|
||||
if covering is not None and row.status == "unclassified":
|
||||
if covering is not None and row.status in _MECHANICAL_TODO:
|
||||
await _judge(session, row, status="canonical", snippet_id=covering,
|
||||
by="mechanical", reason=None, at=now)
|
||||
elif (
|
||||
@@ -402,8 +427,9 @@ async def classify_shapes_where(
|
||||
) -> dict:
|
||||
"""The sweep form of classify_shapes (#2868): one judgment applied to
|
||||
every live row under ``path`` whose symbol matches ``pattern`` (and
|
||||
``kind``). By default only `unclassified` rows are touched — a sweep
|
||||
must never silently overwrite a judgment; ``include_judged`` opts in.
|
||||
``kind``). By default only unjudged rows are touched — `unclassified`
|
||||
and the sync's mechanical `scoped` stamp — a sweep must never silently
|
||||
overwrite a judgment; ``include_judged`` opts in.
|
||||
Same gates as the row form (status vocabulary, snippet target, reason
|
||||
for variant/exempt, write access); one transaction, so it applies whole
|
||||
or not at all. Returns the count and a sample of what it judged."""
|
||||
@@ -431,7 +457,7 @@ async def classify_shapes_where(
|
||||
async with async_session() as session:
|
||||
conds = [CodeShape.project_id == project_id, CodeShape.vanished_at.is_(None)]
|
||||
if not include_judged:
|
||||
conds.append(CodeShape.status == "unclassified")
|
||||
conds.append(CodeShape.status.in_(_MECHANICAL_TODO))
|
||||
rows = (await session.execute(select(CodeShape).where(*conds))).scalars().all()
|
||||
for row in rows:
|
||||
if not rule_matches(row, path=path, pattern=pattern, kind=kind):
|
||||
@@ -736,7 +762,7 @@ async def stamp_write_path_instances(
|
||||
)
|
||||
session.add(row)
|
||||
by_key[(name, kind)] = row
|
||||
elif not (row.status == "unclassified" or row.classified_by == "hook"):
|
||||
elif not (row.status in _MECHANICAL_TODO or row.classified_by == "hook"):
|
||||
continue # a judgment — or the canon itself — stands
|
||||
await _judge(session, row, status="instance", snippet_id=sid, by="hook",
|
||||
reason=why, at=now)
|
||||
@@ -1066,7 +1092,7 @@ async def propose_for_repo(
|
||||
select(CodeShape).where(
|
||||
CodeShape.project_id == project_id,
|
||||
CodeShape.repo_key == repo_key,
|
||||
CodeShape.status == "unclassified",
|
||||
CodeShape.status.in_(_MECHANICAL_TODO),
|
||||
CodeShape.vanished_at.is_(None),
|
||||
)
|
||||
)
|
||||
@@ -1155,7 +1181,7 @@ async def apply_derive_groups(project_id: int) -> int:
|
||||
await session.execute(
|
||||
select(CodeShape).where(
|
||||
CodeShape.project_id == project_id,
|
||||
CodeShape.status == "unclassified",
|
||||
CodeShape.status.in_(_MECHANICAL_TODO),
|
||||
CodeShape.vanished_at.is_(None),
|
||||
CodeShape.proposed_snippet_id.is_(None),
|
||||
)
|
||||
@@ -1239,7 +1265,7 @@ async def confirm_proposals(
|
||||
|
||||
conds = [
|
||||
CodeShape.project_id == project_id,
|
||||
CodeShape.status == "unclassified",
|
||||
CodeShape.status.in_(_MECHANICAL_TODO),
|
||||
CodeShape.vanished_at.is_(None),
|
||||
CodeShape.proposed_snippet_id.isnot(None),
|
||||
]
|
||||
@@ -1396,7 +1422,7 @@ async def flag_divergence(project_id: int, *, since: datetime | None) -> int:
|
||||
for siblings in by_dir.values():
|
||||
dom = dominant_canon(siblings)
|
||||
for r in siblings:
|
||||
if r.status != "unclassified":
|
||||
if r.status not in _MECHANICAL_TODO:
|
||||
continue
|
||||
if r.diverges_from is not None:
|
||||
flagged += 1
|
||||
@@ -1413,7 +1439,7 @@ async def flag_divergence(project_id: int, *, since: datetime | None) -> int:
|
||||
|
||||
def divergence_summary(rows: Iterable[CodeShape], *, top: int = 10) -> dict:
|
||||
"""Readout view: flagged shapes (newest first) and the recheck count."""
|
||||
flagged = [r for r in rows if r.diverges_from is not None and r.status == "unclassified"]
|
||||
flagged = [r for r in rows if r.diverges_from is not None and r.status in _MECHANICAL_TODO]
|
||||
flagged.sort(key=lambda r: (r.created_at or datetime.min.replace(tzinfo=timezone.utc)), reverse=True)
|
||||
recheck = sum(1 for r in rows if r.recheck_at is not None and r.vanished_at is None)
|
||||
return {
|
||||
|
||||
@@ -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