feat(ledger): CSS derive families are names, never bodies — name floor 2 for css, dup: grouping sym-only; derive line says "repeated name" and dismisses scoped-css; plugin 0.1.41 (note #2917)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Build & push image (push) Successful in 25s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Build & push image (push) Successful in 25s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "scribe",
|
"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.",
|
"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.40",
|
"version": "0.1.41",
|
||||||
"author": { "name": "Bryan Van Deusen" },
|
"author": { "name": "Bryan Van Deusen" },
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"scribe": {
|
"scribe": {
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ through recall/auto-inject; this skill is the active reflex around that.
|
|||||||
duplicate — reuse it and drop yours — or it isn't, and the record needs the new
|
duplicate — reuse it and drop yours — or it isn't, and the record needs the new
|
||||||
location adding. Both are cheaper now than after the duplicate settles in.
|
location adding. Both are cheaper now than after the duplicate settles in.
|
||||||
- **A `Shape ledger at …` line is the ledger speaking, not the record.** It
|
- **A `Shape ledger at …` line is the ledger speaking, not the record.** It
|
||||||
names a duplicate family ("identical body in N other files, no canon") or a
|
names a duplicate family ("identical body in N other files, no canon"), a
|
||||||
canon elsewhere for a name you just wrote — for edits made through Bash
|
repeated name ("defined in N other files") or a canon elsewhere for a name
|
||||||
|
you just wrote — for edits made through Bash
|
||||||
(sed, heredocs, scripts) as much as through Write/Edit. Derive the family or
|
(sed, heredocs, scripts) as much as through Write/Edit. Derive the family or
|
||||||
reuse the canon *now*; a family that is convention rather than copies is
|
reuse the canon *now*; a family that is convention rather than copies is
|
||||||
dismissed with `classify_shapes(..., status="exempt",
|
dismissed with `classify_shapes(..., status="exempt",
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ the last sweep left it. Three surfaces say so without anyone running an audit
|
|||||||
- **At the write** — the prior-art hint (the Write/Edit hook, and since
|
- **At the write** — the prior-art hint (the Write/Edit hook, and since
|
||||||
0.1.39 the after-write hook on Bash, so sed/heredoc/script edits count too)
|
0.1.39 the after-write hook on Bash, so sed/heredoc/script edits count too)
|
||||||
carries a `Shape ledger at <path>` line when a name just written is a known
|
carries a `Shape ledger at <path>` line when a name just written is a known
|
||||||
**duplicate family** ("identical body in N other files, no canon") or a
|
**duplicate family** ("identical body in N other files, no canon"), a
|
||||||
|
**repeated name** ("defined in N other files, no canon") or a
|
||||||
**canon elsewhere** ("snippet #N at <path> — reuse, don't redefine"). Act
|
**canon elsewhere** ("snippet #N at <path> — reuse, don't redefine"). Act
|
||||||
on it *then*: pull the canon and build from it, or derive the family now —
|
on it *then*: pull the canon and build from it, or derive the family now —
|
||||||
`create_snippet` the dominant form, repoint the copies, `classify_shapes`
|
`create_snippet` the dominant form, repoint the copies, `classify_shapes`
|
||||||
@@ -109,6 +110,14 @@ the last sweep left it. Three surfaces say so without anyone running an audit
|
|||||||
reason_code="convention-plumbing", reason=…)` (or `classify_shapes_by_rule`
|
reason_code="convention-plumbing", reason=…)` (or `classify_shapes_by_rule`
|
||||||
for a whole family) removes it from the queue. Dismissal is a judgment and
|
for a whole family) removes it from the queue. Dismissal is a judgment and
|
||||||
it is recorded; silence is not.
|
it is recorded; silence is not.
|
||||||
|
- **CSS is watched by name, never by body** (note 2917). Classes serving
|
||||||
|
different purposes share declarations because the style system makes them
|
||||||
|
alike — `.text-muted` and `.pin-badge-auto` carrying the same `color:` are
|
||||||
|
two meanings, not two copies — so a CSS family is the *same class defined
|
||||||
|
in ≥2 files* (a recipe living in several places), and identical bodies
|
||||||
|
under different names are never a family. Derive a CSS family by moving
|
||||||
|
the recipe to the shared sheet and recording it; a class name reused for
|
||||||
|
genuinely different things is dismissed with `reason_code="scoped-css"`.
|
||||||
|
|
||||||
After the one-time pay-down the derive queue reads empty; anything in it
|
After the one-time pay-down the derive queue reads empty; anything in it
|
||||||
afterwards is drift of the moment, and the hint already said so at the write.
|
afterwards is drift of the moment, and the hint already said so at the write.
|
||||||
|
|||||||
@@ -198,11 +198,13 @@ def extract_definitions(text: str) -> list[Definition]:
|
|||||||
break
|
break
|
||||||
block = lines[i:end]
|
block = lines[i:end]
|
||||||
# A CSS rule's fingerprint is its DECLARATIONS, not its selector
|
# A CSS rule's fingerprint is its DECLARATIONS, not its selector
|
||||||
# (#2872): the row's identity already carries the selector, and the
|
# (#2872): the row's identity already carries the selector. Since
|
||||||
# question the fingerprint answers for derive grouping is "is this the
|
# note 2917 the derive grouping no longer reads CSS bodies at all (a
|
||||||
# same rule under another name?" — .closed-msg / .error-block /
|
# class is grouped by name only), so for CSS the fingerprint is the
|
||||||
# .success-msg with identical bodies are one dup group, not three
|
# recheck identity — "did this rule's body change since it was
|
||||||
# lonely rows. Sym blocks keep their signature line in the hash.
|
# judged?" — and nothing more. The shape of the hash is kept as-is on
|
||||||
|
# purpose: changing it would flip every judged CSS row to recheck on
|
||||||
|
# the next sync. Sym blocks keep their signature line in the hash.
|
||||||
if kind == "css":
|
if kind == "css":
|
||||||
# One-line rules (`.x { color: red; }`) carry their declarations on
|
# One-line rules (`.x { color: red; }`) carry their declarations on
|
||||||
# the selector line itself; a block that is only the selector plus
|
# the selector line itself; a block that is only the selector plus
|
||||||
@@ -217,10 +219,9 @@ def extract_definitions(text: str) -> list[Definition]:
|
|||||||
# A SINGLE declaration is not a shape (#2903): `color: var(--fs-
|
# A SINGLE declaration is not a shape (#2903): `color: var(--fs-
|
||||||
# text-tertiary)` under .text-muted, .task-mark and .pin-badge-auto
|
# text-tertiary)` under .text-muted, .task-mark and .pin-badge-auto
|
||||||
# is three meanings sharing one line, not three copies of one
|
# is three meanings sharing one line, not three copies of one
|
||||||
# rule — the first pay-down found 5-file "families" of exactly
|
# rule. Keep the selector in the hash for one-liners; two
|
||||||
# this and nobody would consolidate them. Keep the selector in the
|
# declarations and up stay selector-agnostic. (Moot for grouping
|
||||||
# hash for one-liners, so they group only with same-name copies;
|
# since note 2917, kept for fingerprint stability — see above.)
|
||||||
# two declarations and up stay selector-agnostic.
|
|
||||||
elif _declaration_count(hashed) < 2:
|
elif _declaration_count(hashed) < 2:
|
||||||
hashed = block
|
hashed = block
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1061,17 +1061,27 @@ def _derive_line(path: str, derive: list[dict]) -> str:
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
f = d["family"]
|
f = d["family"]
|
||||||
how = "identical body" if f.get("identical") else "same name defined"
|
|
||||||
files = ", ".join(f"`{x}`" for x in f.get("files") or [])
|
files = ", ".join(f"`{x}`" for x in f.get("files") or [])
|
||||||
more = f.get("file_count", 0) - len(f.get("files") or [])
|
more = f.get("file_count", 0) - len(f.get("files") or [])
|
||||||
if more > 0:
|
if more > 0:
|
||||||
files += f" +{more} more"
|
files += f" +{more} more"
|
||||||
|
n = f.get("file_count", 0)
|
||||||
|
if f.get("identical"):
|
||||||
|
what = f"is a duplicate family with no canon — identical body in {n} other file(s)"
|
||||||
|
else:
|
||||||
|
# A name family: the same definition name living in several
|
||||||
|
# files. CSS is only ever grouped this way (note 2917) — a class
|
||||||
|
# is a recipe, and the recipe is what gets derived or dismissed.
|
||||||
|
what = f"is a repeated name with no canon — defined in {n} other file(s)"
|
||||||
|
# The dismissal reason the family most likely earns: a class name
|
||||||
|
# reused for different purposes is scoped styling; a code name reused
|
||||||
|
# across modules is convention plumbing.
|
||||||
|
dismiss = "scoped-css" if d.get("kind") == "css" else "convention-plumbing"
|
||||||
parts.append(
|
parts.append(
|
||||||
f"`{f['label']}` is a duplicate family with no canon — {how} in "
|
f"`{f['label']}` {what}: {files}; derive it now: "
|
||||||
f"{f.get('file_count', 0)} other file(s): {files}; derive it now: "
|
|
||||||
"record the canon (create_snippet) and make the copies instances "
|
"record the canon (create_snippet) and make the copies instances "
|
||||||
"(classify_shapes) — or, if these are convention not copies, "
|
"(classify_shapes) — or, if these are convention not copies, "
|
||||||
"`classify_shapes(..., status=\"exempt\", reason_code=\"convention-plumbing\")` "
|
f"`classify_shapes(..., status=\"exempt\", reason_code=\"{dismiss}\")` "
|
||||||
"dismisses the family — rather than adding another copy"
|
"dismisses the family — rather than adding another copy"
|
||||||
)
|
)
|
||||||
return f"> Shape ledger at `{path}`: " + "; ".join(parts) + "."
|
return f"> Shape ledger at `{path}`: " + "; ".join(parts) + "."
|
||||||
|
|||||||
@@ -923,6 +923,13 @@ async def stamp_write_path_instances(
|
|||||||
# recur by convention, not by duplication).
|
# recur by convention, not by duplication).
|
||||||
_DERIVE_MIN_DUP = 2
|
_DERIVE_MIN_DUP = 2
|
||||||
_DERIVE_MIN_NAME = 3
|
_DERIVE_MIN_NAME = 3
|
||||||
|
# CSS is never grouped by body (note 2917): classes for different purposes
|
||||||
|
# share declarations because the style system makes them alike — `.text-muted`
|
||||||
|
# and `.pin-badge-auto` carrying the same `color: var(--fs-text-tertiary)` are
|
||||||
|
# two meanings, not two copies. A CSS family is a NAME defined in more than
|
||||||
|
# one file: that is a recipe living in several places, and two is already
|
||||||
|
# the signal (a class name is deliberate in a way `setup`/`load` are not).
|
||||||
|
_DERIVE_MIN_NAME_CSS = 2
|
||||||
# Semantic checks per repo per refresh — an embedding each (local fastembed),
|
# Semantic checks per repo per refresh — an embedding each (local fastembed),
|
||||||
# bounded so a 4,000-row ledger is worked through over refreshes, not in one.
|
# bounded so a 4,000-row ledger is worked through over refreshes, not in one.
|
||||||
_SEMANTIC_CAP = 150
|
_SEMANTIC_CAP = 150
|
||||||
@@ -1298,15 +1305,16 @@ def derive_groups(
|
|||||||
rows: Iterable[tuple[str, str, str, str]]
|
rows: Iterable[tuple[str, str, str, str]]
|
||||||
) -> dict[tuple[str, str, str], str]:
|
) -> dict[tuple[str, str, str], str]:
|
||||||
"""The derive-first grouping over (path, kind, symbol, body_sha) rows
|
"""The derive-first grouping over (path, kind, symbol, body_sha) rows
|
||||||
that matched no canon: {(path, kind, symbol): group_key}. Identical
|
that matched no canon: {(path, kind, symbol): group_key}. For code
|
||||||
bodies in ≥2 places group as `dup:<sha>`; the same name defined in ≥3
|
(kind `sym`) identical bodies in ≥2 places group as `dup:<sha>` and the
|
||||||
files groups as `name:<kind>:<symbol>`; a row joins at most one group,
|
same name defined in ≥3 files groups as `name:sym:<symbol>`, the copy
|
||||||
the copy before the name."""
|
before the name. CSS groups by name only — the same class defined in
|
||||||
|
≥2 files is `name:css:<symbol>`; its body never groups it (note 2917)."""
|
||||||
by_sha: dict[str, list[tuple[str, str, str]]] = {}
|
by_sha: dict[str, list[tuple[str, str, str]]] = {}
|
||||||
by_name: dict[tuple[str, str], list[tuple[str, str, str]]] = {}
|
by_name: dict[tuple[str, str], list[tuple[str, str, str]]] = {}
|
||||||
for path, kind, symbol, sha in rows:
|
for path, kind, symbol, sha in rows:
|
||||||
key = (path, kind, symbol)
|
key = (path, kind, symbol)
|
||||||
if sha:
|
if sha and kind != "css":
|
||||||
by_sha.setdefault(sha, []).append(key)
|
by_sha.setdefault(sha, []).append(key)
|
||||||
by_name.setdefault((kind, _norm_symbol(symbol)), []).append(key)
|
by_name.setdefault((kind, _norm_symbol(symbol)), []).append(key)
|
||||||
out: dict[tuple[str, str, str], str] = {}
|
out: dict[tuple[str, str, str], str] = {}
|
||||||
@@ -1315,7 +1323,8 @@ def derive_groups(
|
|||||||
for key in keys:
|
for key in keys:
|
||||||
out.setdefault(key, f"dup:{sha}")
|
out.setdefault(key, f"dup:{sha}")
|
||||||
for (kind, symbol), keys in by_name.items():
|
for (kind, symbol), keys in by_name.items():
|
||||||
if len({k[0] for k in keys}) >= _DERIVE_MIN_NAME:
|
floor = _DERIVE_MIN_NAME_CSS if kind == "css" else _DERIVE_MIN_NAME
|
||||||
|
if len({k[0] for k in keys}) >= floor:
|
||||||
for key in keys:
|
for key in keys:
|
||||||
out.setdefault(key, f"name:{kind}:{symbol}")
|
out.setdefault(key, f"name:{kind}:{symbol}")
|
||||||
return out
|
return out
|
||||||
@@ -1606,9 +1615,10 @@ async def write_time_derive(
|
|||||||
named at ``path``, what the ledger already knows about that name
|
named at ``path``, what the ledger already knows about that name
|
||||||
elsewhere in the project —
|
elsewhere in the project —
|
||||||
|
|
||||||
family the name sits in a derive-first group (identical body in N
|
family the name sits in a derive-first group (code: identical body
|
||||||
files, or the same name in ≥3): "this is a known duplicate
|
in N files or the same name in ≥3; CSS: the same class in
|
||||||
family with no canon — derive it now, don't add a copy";
|
≥2 files, note 2917): "this is a known family with no canon
|
||||||
|
— derive it now, don't add a copy";
|
||||||
canon a `canonical` row of that name at another path: "this is
|
canon a `canonical` row of that name at another path: "this is
|
||||||
canon #N at <path> — reuse, don't redefine".
|
canon #N at <path> — reuse, don't redefine".
|
||||||
|
|
||||||
|
|||||||
@@ -588,21 +588,30 @@ async def test_derive_groups_land_on_rows_and_in_the_summary(seeded):
|
|||||||
assert summary["derive_groups"][1]["label"] == ".card"
|
assert summary["derive_groups"][1]["label"] == ".card"
|
||||||
assert summary["derive_groups"][0]["size"] == 2 and summary["derive_groups"][1]["size"] == 3
|
assert summary["derive_groups"][0]["size"] == 2 and summary["derive_groups"][1]["size"] == 3
|
||||||
|
|
||||||
# One of the css copies gets judged → the group shrinks on the next pass.
|
# One of the css copies gets judged → the group shrinks on the next pass
|
||||||
|
# but stays a family: a class in two files is already a recipe living in
|
||||||
|
# two places (css name floor 2, note 2917). Judge the second and it's gone.
|
||||||
await classify_shapes(owner, pid, [
|
await classify_shapes(owner, pid, [
|
||||||
{"path": "b/z.css", "symbol": "card", "status": "exempt", "reason": "print sheet"},
|
{"path": "b/z.css", "symbol": "card", "status": "exempt", "reason": "print sheet"},
|
||||||
])
|
])
|
||||||
await apply_derive_groups(pid)
|
await apply_derive_groups(pid)
|
||||||
rows, _ = await list_project_shapes(owner, pid, proposal="derive")
|
rows, _ = await list_project_shapes(owner, pid, proposal="derive")
|
||||||
assert {r.symbol for r in rows} == {"slug"} # 2 files < the name floor
|
assert {r.symbol for r in rows} == {"slug", "card"}
|
||||||
|
assert {r.path for r in rows if r.symbol == "card"} == {"b/x.css", "b/y.css"}
|
||||||
|
await classify_shapes(owner, pid, [
|
||||||
|
{"path": "b/y.css", "symbol": "card", "status": "exempt", "reason": "print sheet"},
|
||||||
|
])
|
||||||
|
await apply_derive_groups(pid)
|
||||||
|
rows, _ = await list_project_shapes(owner, pid, proposal="derive")
|
||||||
|
assert {r.symbol for r in rows} == {"slug"} # 1 file < the css name floor
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_write_time_derive_names_the_family_or_the_canon_for_a_name(seeded):
|
async def test_write_time_derive_names_the_family_or_the_canon_for_a_name(seeded):
|
||||||
"""#2900: against real rows — a name in a dup family → the family (other
|
"""#2900: against real rows — a name in a family → the family (other
|
||||||
files, count); a name whose canonical row lives elsewhere → that canon;
|
files, count; for CSS a NAME family, never a body one — note 2917); a
|
||||||
a judged row at the path, the canon's own file, or an unknown name →
|
name whose canonical row lives elsewhere → that canon; a judged row at
|
||||||
silence."""
|
the path, the canon's own file, or an unknown name → silence."""
|
||||||
from scribe.services.shape_ledger import apply_derive_groups, write_time_derive
|
from scribe.services.shape_ledger import apply_derive_groups, write_time_derive
|
||||||
|
|
||||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||||
@@ -622,9 +631,10 @@ async def test_write_time_derive_names_the_family_or_the_canon_for_a_name(seeded
|
|||||||
out = await write_time_derive(pid, "v/D.vue", [("css", "log-empty"), ("css", "unknown")])
|
out = await write_time_derive(pid, "v/D.vue", [("css", "log-empty"), ("css", "unknown")])
|
||||||
assert len(out) == 1 and out[0]["symbol"] == "log-empty" and out[0]["kind"] == "css"
|
assert len(out) == 1 and out[0]["symbol"] == "log-empty" and out[0]["kind"] == "css"
|
||||||
fam = out[0]["family"]
|
fam = out[0]["family"]
|
||||||
assert fam["identical"] is True and fam["label"] == ".log-empty"
|
# Three identical bodies, and still a NAME family: CSS never groups by body.
|
||||||
|
assert fam["identical"] is False and fam["label"] == ".log-empty"
|
||||||
assert fam["files"] == ["v/A.vue", "v/B.vue", "v/C.vue"] and fam["file_count"] == 3
|
assert fam["files"] == ["v/A.vue", "v/B.vue", "v/C.vue"] and fam["file_count"] == 3
|
||||||
assert out[0]["key"] == fam["group"] and fam["group"].startswith("dup:")
|
assert out[0]["key"] == fam["group"] == "name:css:log-empty"
|
||||||
# Editing one existing member still names the OTHER members.
|
# Editing one existing member still names the OTHER members.
|
||||||
out = await write_time_derive(pid, "v/A.vue", [("css", "log-empty")])
|
out = await write_time_derive(pid, "v/A.vue", [("css", "log-empty")])
|
||||||
assert out[0]["family"]["files"] == ["v/B.vue", "v/C.vue"] and out[0]["family"]["size"] == 3
|
assert out[0]["family"]["files"] == ["v/B.vue", "v/C.vue"] and out[0]["family"]["size"] == 3
|
||||||
|
|||||||
@@ -308,6 +308,12 @@ def test_derive_groups_copy_before_name_with_floors():
|
|||||||
("d.css", "css", "btn", "s1"), ("e.css", "css", "btn", "s2"), ("f.css", "css", "btn", "s3"),
|
("d.css", "css", "btn", "s1"), ("e.css", "css", "btn", "s2"), ("f.css", "css", "btn", "s3"),
|
||||||
("g.py", "sym", "main", "s4"), ("h.py", "sym", "main", "s5"), # only 2 files → no name group
|
("g.py", "sym", "main", "s4"), ("h.py", "sym", "main", "s5"), # only 2 files → no name group
|
||||||
("i.py", "sym", "one", "s6"),
|
("i.py", "sym", "one", "s6"),
|
||||||
|
# CSS (note 2917): identical bodies under different names are NOT a
|
||||||
|
# copy — two meanings sharing the style system's look; the same
|
||||||
|
# class in two files IS a family (the name floor is 2 for css).
|
||||||
|
("j.css", "css", "muted", "same"), ("k.css", "css", "pin-auto", "same"),
|
||||||
|
("l.css", "css", "card", "c1"), ("m.css", "css", "card", "c2"),
|
||||||
|
("n.css", "css", "alone", "c3"),
|
||||||
]
|
]
|
||||||
g = derive_groups(rows)
|
g = derive_groups(rows)
|
||||||
assert g[("a.py", "sym", "helper")] == "dup:sha1" == g[("b.py", "sym", "helper")]
|
assert g[("a.py", "sym", "helper")] == "dup:sha1" == g[("b.py", "sym", "helper")]
|
||||||
@@ -315,6 +321,10 @@ def test_derive_groups_copy_before_name_with_floors():
|
|||||||
assert g[("d.css", "css", "btn")] == "name:css:btn"
|
assert g[("d.css", "css", "btn")] == "name:css:btn"
|
||||||
assert ("g.py", "sym", "main") not in g
|
assert ("g.py", "sym", "main") not in g
|
||||||
assert ("i.py", "sym", "one") not in g
|
assert ("i.py", "sym", "one") not in g
|
||||||
|
assert ("j.css", "css", "muted") not in g and ("k.css", "css", "pin-auto") not in g
|
||||||
|
assert g[("l.css", "css", "card")] == "name:css:card" == g[("m.css", "css", "card")]
|
||||||
|
assert ("n.css", "css", "alone") not in g
|
||||||
|
assert not any(v.startswith("dup:") for k, v in g.items() if k[1] == "css")
|
||||||
|
|
||||||
|
|
||||||
def test_derive_new_summary_counts_copies_first_seen_since_the_previous_refresh():
|
def test_derive_new_summary_counts_copies_first_seen_since_the_previous_refresh():
|
||||||
|
|||||||
@@ -1250,13 +1250,16 @@ async def test_the_write_time_derive_check_names_a_family_or_a_canon_in_band():
|
|||||||
channel silences a family already named."""
|
channel silences a family already named."""
|
||||||
from scribe.services import plugin_context as pc
|
from scribe.services import plugin_context as pc
|
||||||
found = [
|
found = [
|
||||||
{"symbol": "log-empty", "kind": "css", "key": "dup:483a",
|
{"symbol": "log-empty", "kind": "css", "key": "name:css:log-empty",
|
||||||
"family": {"group": "dup:483a", "label": ".log-empty", "identical": True,
|
"family": {"group": "name:css:log-empty", "label": ".log-empty", "identical": False,
|
||||||
"files": ["a/TaskLogSection.vue", "a/WorkspaceTaskPanel.vue"],
|
"files": ["a/TaskLogSection.vue", "a/WorkspaceTaskPanel.vue"],
|
||||||
"file_count": 5, "size": 6}},
|
"file_count": 5, "size": 6}},
|
||||||
{"symbol": "btn-primary", "kind": "css", "key": "canon:2855",
|
{"symbol": "btn-primary", "kind": "css", "key": "canon:2855",
|
||||||
"canon": {"snippet_id": 2855, "path": "frontend/src/assets/components.css",
|
"canon": {"snippet_id": 2855, "path": "frontend/src/assets/components.css",
|
||||||
"label": ".btn-primary"}},
|
"label": ".btn-primary"}},
|
||||||
|
{"symbol": "slugify", "kind": "sym", "key": "dup:483a",
|
||||||
|
"family": {"group": "dup:483a", "label": "slugify", "identical": True,
|
||||||
|
"files": ["a/x.py", "a/y.py"], "file_count": 2, "size": 3}},
|
||||||
{"symbol": "load", "kind": "sym", "key": "name:sym:load",
|
{"symbol": "load", "kind": "sym", "key": "name:sym:load",
|
||||||
"family": {"group": "name:sym:load", "label": "load", "identical": False,
|
"family": {"group": "name:sym:load", "label": "load", "identical": False,
|
||||||
"files": ["a/X.vue", "a/Y.vue", "a/Z.vue"], "file_count": 3, "size": 4}},
|
"files": ["a/X.vue", "a/Y.vue", "a/Z.vue"], "file_count": 3, "size": 4}},
|
||||||
@@ -1274,20 +1277,27 @@ async def test_the_write_time_derive_check_names_a_family_or_a_canon_in_band():
|
|||||||
patch.object(pc.shape_ledger_svc, "write_time_derive", check):
|
patch.object(pc.shape_ledger_svc, "write_time_derive", check):
|
||||||
out = await pc.build_write_path_hint(
|
out = await pc.build_write_path_hint(
|
||||||
1, "frontend/src/components/New.vue", code=REAL_CODE, project_id=24,
|
1, "frontend/src/components/New.vue", code=REAL_CODE, project_id=24,
|
||||||
stamp_shapes=[("css", "log-empty"), ("css", "btn-primary"), ("sym", "load")],
|
stamp_shapes=[("css", "log-empty"), ("css", "btn-primary"), ("sym", "slugify"),
|
||||||
|
("sym", "load")],
|
||||||
exclude_derive=["name:sym:load"],
|
exclude_derive=["name:sym:load"],
|
||||||
)
|
)
|
||||||
check.assert_awaited_once_with(24, "frontend/src/components/New.vue",
|
check.assert_awaited_once_with(24, "frontend/src/components/New.vue",
|
||||||
[("css", "log-empty"), ("css", "btn-primary"), ("sym", "load")])
|
[("css", "log-empty"), ("css", "btn-primary"),
|
||||||
# The excluded family is gone; the other two render and are keyed.
|
("sym", "slugify"), ("sym", "load")])
|
||||||
assert [d["key"] for d in out["derive"]] == ["dup:483a", "canon:2855"]
|
# The excluded family is gone; the other three render and are keyed.
|
||||||
assert out["derive_keys"] == ["dup:483a", "canon:2855"]
|
assert [d["key"] for d in out["derive"]] == ["name:css:log-empty", "canon:2855", "dup:483a"]
|
||||||
|
assert out["derive_keys"] == ["name:css:log-empty", "canon:2855", "dup:483a"]
|
||||||
ctx = out["context"]
|
ctx = out["context"]
|
||||||
assert "Shape ledger at `frontend/src/components/New.vue`" in ctx
|
assert "Shape ledger at `frontend/src/components/New.vue`" in ctx
|
||||||
assert "`.log-empty` is a duplicate family with no canon — identical body in 5 other file(s): " \
|
# A CSS family is a repeated NAME (note 2917) and its dismissal is scoped-css;
|
||||||
|
# a code dup family is an identical body and dismisses as convention-plumbing.
|
||||||
|
assert "`.log-empty` is a repeated name with no canon — defined in 5 other file(s): " \
|
||||||
"`a/TaskLogSection.vue`, `a/WorkspaceTaskPanel.vue` +3 more; derive it now" in ctx
|
"`a/TaskLogSection.vue`, `a/WorkspaceTaskPanel.vue` +3 more; derive it now" in ctx
|
||||||
|
assert "`slugify` is a duplicate family with no canon — identical body in 2 other file(s): " \
|
||||||
|
"`a/x.py`, `a/y.py`; derive it now" in ctx
|
||||||
assert "`.btn-primary` is canon — snippet #2855 at `frontend/src/assets/components.css`" in ctx
|
assert "`.btn-primary` is canon — snippet #2855 at `frontend/src/assets/components.css`" in ctx
|
||||||
assert "convention-plumbing" in ctx
|
assert 'reason_code=\"scoped-css\")` dismisses' in ctx
|
||||||
|
assert 'reason_code=\"convention-plumbing\")` dismisses' in ctx
|
||||||
assert "`load`" not in ctx
|
assert "`load`" not in ctx
|
||||||
# No project → no check; a failing check never sinks the hint.
|
# No project → no check; a failing check never sinks the hint.
|
||||||
check.reset_mock()
|
check.reset_mock()
|
||||||
|
|||||||
Reference in New Issue
Block a user