diff --git a/src/scribe/services/plugin_context.py b/src/scribe/services/plugin_context.py index 150cbb5..f9d048f 100644 --- a/src/scribe/services/plugin_context.py +++ b/src/scribe/services/plugin_context.py @@ -2028,7 +2028,7 @@ async def build_write_path_hint( if stamp_shapes and project_id: try: divergence = await shape_ledger_svc.write_time_divergence( - project_id, path, stamp_shapes, stamped + project_id, path, stamp_shapes, stamped, code or "", ) except Exception: logger.warning("write-time divergence check failed", exc_info=True) diff --git a/src/scribe/services/shape_ledger.py b/src/scribe/services/shape_ledger.py index 02c9d33..ef7ed67 100644 --- a/src/scribe/services/shape_ledger.py +++ b/src/scribe/services/shape_ledger.py @@ -868,6 +868,170 @@ async def snippet_consumers(user_id: int, note_id: int) -> dict: # precision comes from the in-play test above, not from this window. PULL_WINDOW = timedelta(hours=6) +# ── WHAT KIND OF THING a shape is, past `css | sym` (#4204) ────────────── +# +# THE PROBLEM THIS EXISTS FOR. `kind` has exactly two values, so a frozen +# dataclass, a module constant, a sync predicate, a class and an async service +# function are all siblings of one another. Nothing downstream could possibly +# discriminate on a distinction the column does not carry — which is how a +# directory base rate ended up standing alone as the entire divergence +# argument. It was not chosen over a better signal; it was the only signal +# there was. Measured consequence: writing a registry module of pure helpers +# produced five prompts to build them from the `async_session` service canon, +# and on another project one snippet had been recorded as the canon for a +# `class`, a plain `def`, an `async def` and a dozen test functions at once. +# +# DERIVED FROM THE SIGNATURE ON READ, not stored. `kind` is part of the row +# identity — ("project_id", "repo_key", "path", "symbol", "kind") — so +# widening that column rewrites every row's key and needs a migration plus a +# re-extract. Reading the form off the signature costs nothing, needs no +# migration, and is reversible. Widening `kind` remains the principled fix and +# this does not foreclose it: every caller below asks `shape_form`, so the day +# the column carries the answer, this function returns it instead. +# +# DELIBERATELY COARSE, and it must stay that way. It answers "could these +# plausibly be the same kind of thing", never "what language construct is +# this". A finer taxonomy would need per-language parsing and would start +# disagreeing with itself across the four languages this ledger already holds +# (Python, Go, TypeScript, Vue SFC) — and a classifier that is wrong in a new +# way is worse than the coarse one it replaced. +FORM_UNKNOWN = "" + +# How much a canon's own instances must agree before it may assert a form. +# A canon whose rows disagree about what they are cannot tell anyone else what +# to be — and that disagreement is the SYMPTOM of the bad stamping this same +# change fixes, so reading it as "no opinion" makes the two halves cooperate: +# a canon poisoned by loose stamping falls silent instead of flagging. +_FORM_SHARE = 0.6 + +# Leading words that say nothing about the form of the thing being declared. +_FORM_NOISE = ("export ", "default ", "public ", "private ", "static ", "final ") + +# A declared TYPE, across the languages in play. Coarse on purpose: a Go +# struct, a TS interface and a Python class are one bucket because the +# question is only ever "is the other thing also a type". +_FORM_TYPE_WORDS = ("class ", "interface ", "type ", "struct ", "enum ") +_FORM_FN_WORDS = ("def ", "function ", "func ", "fn ", "sub ") + +# THE DECLARING KEYWORD IS OPTIONAL, because Python has none. A module +# constant is written `MAX_BUDGET = 10` — no `const`, no `let` — and a +# pattern that required one classified every Python constant as unreadable, +# which then read downstream as "do not assert" and quietly excluded a whole +# form from both checks. Caught by the payload test, not by review. +_FORM_BINDING = re.compile(r"^(?:(?:const|let|var)\s+)?[\w$]+\s*(?::[^=]+)?=") +_FORM_ARROW = re.compile( + r"^(?:(?:const|let|var)\s+)?[\w$]+\s*(?::[^=]+)?=\s*(async\s*)?\(" +) + + +def shape_form(signature: str, kind: str = "sym") -> str: + """The structural form of a shape: css / type / async-fn / fn / binding. + + Returns FORM_UNKNOWN when the signature does not say, and every caller + treats that as "do not assert", never as "no match". An unreadable + signature must make this quieter, not more confident. + """ + if kind == "css": + return "css" + sig = (signature or "").strip() + if not sig: + return FORM_UNKNOWN + changed = True + while changed: + changed = False + for lead in _FORM_NOISE: + if sig.startswith(lead): + sig, changed = sig[len(lead):].lstrip(), True + if sig.startswith(_FORM_TYPE_WORDS): + return "type" + if sig.startswith("async "): + return "async-fn" + if sig.startswith(_FORM_FN_WORDS): + return "fn" + m = _FORM_ARROW.match(sig) + if m: + return "async-fn" if m.group(1) else "fn" + if _FORM_BINDING.match(sig): + return "binding" + return FORM_UNKNOWN + + +def forms_agree(a: str, b: str) -> bool: + """Do two forms match well enough to assert a relationship? + + BOTH must be known. "Unknown equals unknown" would make two shapes nobody + can read into a confident pair, which is the failure this whole change is + about — a guess dressed as a finding. + """ + return bool(a) and bool(b) and a == b + + +def forms_conflict(a: str, b: str) -> bool: + """Do two KNOWN forms rule each other out? + + The weaker sibling of `forms_agree`, and the pair exists because the two + kinds of evidence this ledger acts on deserve different burdens. + + An explicit by-name reference — the payload literally names the canon's + symbol — is strong, so it needs only the absence of a contradiction: stamp + unless the forms are both readable and different. A payload-level + RESEMBLANCE SCORE is weak, computed against the whole file, so one score + speaks for every symbol in it; that needs positive agreement before + asserting anything, which is `forms_agree`. + + Demanding agreement everywhere was the first version of this and it was + wrong: it silenced the by-name path whenever a shape's definition was not + in the payload — an Edit rather than a Write — turning strong evidence + into no evidence for a reason that has nothing to do with the code. + """ + return bool(a) and bool(b) and a != b + + +def canon_form(rows: Iterable, snippet_id: int) -> str: + """The form a canon's own judged rows agree on, or FORM_UNKNOWN.""" + forms: dict[str, int] = {} + for r in rows: + if r.snippet_id != snippet_id or r.status not in ("canonical", "instance"): + continue + f = shape_form(getattr(r, "signature", "") or "", r.kind) + if f: + forms[f] = forms.get(f, 0) + 1 + if not forms: + return FORM_UNKNOWN + top, n = max(forms.items(), key=lambda kv: kv[1]) + return top if n / sum(forms.values()) >= _FORM_SHARE else FORM_UNKNOWN + + +def signature_in(code: str, symbol: str, kind: str) -> str: + """The line in ``code`` that DEFINES ``symbol``, or "" if none does. + + The write-time checks are asked about a shape that may have no ledger row + yet — it is being written right now — so the payload is the only place its + signature exists. Finding nothing returns "", which reads downstream as + FORM_UNKNOWN and therefore as silence. + """ + if not code or not symbol: + return "" + name = re.escape(symbol.lstrip(".")) + if kind == "css": + pat = re.compile(r"^\s*\." + name + r"\b") + else: + pat = re.compile( + r"^\s*(?:(?:export|default|public|private|static|final)\s+)*" + r"(?:" + r"(?:(?:async\s+)?(?:def|function|func|fn|sub)|class|interface|type" + r"|struct|enum|const|let|var)\s+" + name + r"\b" + # A bare binding — `MAX = 10`, `Handler = ...` — which is how + # Python (and plain JS assignment) declares one. + r"|" + name + r"\s*(?::[^=\n]+)?=(?!=)" + r")" + ) + for line in code.splitlines(): + if pat.match(line): + return line.strip() + return "" + + def snippet_kind(symbol: str, language: str) -> str: """The ledger kind a snippet's reference belongs to — "css" when its symbol is a class selector (or it is a stylesheet with no symbol), @@ -920,6 +1084,19 @@ async def recent_pulls(user_id: int, *, window: timedelta = PULL_WINDOW) -> dict return {} +def _stamp_allowed(rank: int, mine: str, canon: str) -> bool: + """May this evidence assert that a shape of form ``mine`` IS ``canon``? + + Named rather than inlined because the asymmetry is the decision, not an + implementation detail: rank 2 (the payload names the canon's symbol) has + only to avoid contradicting, rank 1 (a whole-file similarity score) has to + positively agree. See `forms_conflict` for why both exist. + """ + if rank >= 2: + return not forms_conflict(mine, canon) + return forms_agree(mine, canon) + + async def stamp_write_path_instances( user_id: int, project_id: int, @@ -961,7 +1138,15 @@ async def stamp_write_path_instances( return [] # Which pulled canons are in play for this payload, by kind, ranked. - in_play: dict[str, list[tuple[int, datetime, int, str]]] = {} + # + # THE FORM OF THE CANON TRAVELS WITH IT (#4204), because what gets written + # here is an ASSERTION — "this shape IS that canon" — and it is permanent + # until something re-judges it. The evidence below is payload-level: a + # resemblance score is computed against the whole file, so without a + # per-shape test every symbol in that file inherits one verdict. Measured + # on another project: a single write stamped `class SessionAbsent`, + # `async def attach` and `_run_control_client` as instances of one snippet. + in_play: dict[str, list[tuple[int, datetime, int, str, str]]] = {} for sid, pulled_at in pulled.items(): note = await snippets_svc.get_snippet(user_id, sid) if note is None: @@ -971,11 +1156,19 @@ async def stamp_write_path_instances( kind = snippet_kind(symbol, fields.get("language") or "") if references_symbol(code, symbol, kind): rank, why = 2, f"hook: pulled #{sid}; payload references `{_norm_symbol(symbol)}`" - elif sid in resembles: + elif resembles.get(sid, 0.0) >= _RESEMBLE_MIN: rank, why = 1, f"hook: pulled #{sid}; payload resembles it ({resembles[sid]:.2f})" else: + # A SCORE BELOW THE FLOOR IS NOT WEAK EVIDENCE, IT IS NONE. This + # branch used to be `elif sid in resembles`, which took any score + # at all — 0.69 counted exactly as much as 0.95, and the number was + # printed into the reason line while never being tested against + # anything. A row written on that basis is indistinguishable + # afterwards from one written on real evidence. continue - in_play.setdefault(kind, []).append((rank, pulled_at, sid, why)) + in_play.setdefault(kind, []).append( + (rank, pulled_at, sid, why, shape_form(fields.get("signature") or "", kind)) + ) if not in_play: return [] for bucket in in_play.values(): @@ -998,8 +1191,31 @@ async def stamp_write_path_instances( bucket = in_play.get(kind) if not bucket: continue - _rank, _at, sid, why = bucket[0] row = by_key.get((name, kind)) + # PER SHAPE, NOT PER FILE (#4204). The best candidate whose FORM + # matches this shape's — not simply the best candidate. A class and + # a function in one file can no longer be handed the same canon + # because the file as a whole resembled it. + # + # The signature comes from the payload first: a shape being written + # right now may have no ledger row yet, and then the code is the + # only place it exists. No readable signature on either side means + # no stamp — `forms_agree` requires both to be known, so an + # unreadable shape falls silent instead of matching everything. + mine = shape_form( + signature_in(code, name, kind) or getattr(row, "signature", "") or "", kind + ) + # THE BURDEN SCALES WITH THE EVIDENCE. Rank 2 is an explicit + # by-name reference to the canon in this very payload; it stands + # unless the forms actively contradict. Rank 1 is a similarity + # score over the whole file — one number that would otherwise + # speak for every symbol in it — so it must positively agree. + cand = next( + (t for t in bucket if _stamp_allowed(t[0], mine, t[4])), None + ) + if cand is None: + continue + _rank, _at, sid, why, _cform = cand if row is None: if not repo_key: continue @@ -1671,6 +1887,24 @@ async def confirm_proposals( # A canon dominates a directory+kind when at least this many siblings are # judged (canonical/instance) and this share of them answer to one snippet. +# How much a payload must resemble a canon before the hook may assert, with +# nobody watching, that a shape IS an instance of it. +# +# WHY A FLOOR AT ALL. There was none: any score the semantic arm produced +# counted, so 0.69 asserted as confidently as 0.95, and the score went into +# the reason line without ever being compared to anything. Those rows are +# permanent — they feed `dominant_canon`, which then tells the next writer in +# that directory what to build from — so a thin stamp does not stay thin, it +# compounds. +# +# 0.80 rather than the retrieval floors near 0.70, deliberately. Those bars +# decide whether to SHOW someone a record, where being wrong costs a glance. +# This one decides whether to RECORD a claim about the codebase unattended, +# where being wrong costs a wrong instruction to everyone who writes in that +# directory afterwards. An unattended write should need more evidence than a +# suggestion, not the same. +_RESEMBLE_MIN = 0.80 + _DENSITY_MIN_JUDGED = 3 _DENSITY_SHARE = 0.6 @@ -1713,11 +1947,20 @@ async def canon_density(project_id: int, path: str, kind: str) -> tuple[int, int ) ).scalars().all() siblings = [r for r in rows if _dir_of(r.path) == directory] - return dominant_canon(siblings) + dom = dominant_canon(siblings) + if dom is None: + return None + # The canon's form travels with the count, read from its OWN judged rows. + # `dominant_canon` keeps its three-value shape: it answers "what dominates + # here", which is still a true and separately useful question, and its + # test pins that arithmetic. What changed is that nobody acts on the count + # alone any more. + return dom[0], dom[1], dom[2], canon_form(siblings, dom[0]) async def write_time_divergence( project_id: int, path: str, shapes: list[tuple[str, str]], stamped: list[dict], + code: str = "", ) -> list[dict]: """The in-band check for the shapes the hook named at ``path``: for each kind whose directory has a dominant canon, the named shapes that are @@ -1745,7 +1988,7 @@ async def write_time_divergence( dom = density.get(kind) if not dom: continue - sid, n, judged = dom + sid, n, judged, cform = dom if just_stamped.get((name, kind)) == sid: continue row = by_key.get((name, kind)) @@ -1753,6 +1996,19 @@ async def write_time_divergence( row.status != "unclassified" or row.proposed_snippet_id == sid ): continue + # DOES THIS SHAPE EVEN RESEMBLE THE CANON? (#4204) Without this the + # line was a pure base rate: "most things here are X, so be X", with + # the candidate never examined. It told a frozen dataclass and three + # pure predicates to build from the `async_session` service canon. + # + # Signature from the payload first — a shape being written now may + # have no row yet, and `forms_agree` needs both sides known, so an + # unreadable one produces silence rather than a guess. + mine = shape_form( + signature_in(code, name, kind) or getattr(row, "signature", "") or "", kind + ) + if not forms_agree(mine, cform): + continue out.append({"symbol": name, "kind": kind, "canon_snippet_id": sid, "instances": n, "judged": judged}) return out @@ -1856,6 +2112,9 @@ async def flag_divergence(project_id: int, *, since: datetime | None) -> int: flagged = 0 for siblings in by_dir.values(): dom = dominant_canon(siblings) + # Computed once per directory rather than per row: the canon's + # form is a property of the canon, not of who is being judged. + cform = canon_form(siblings, dom[0]) if dom else FORM_UNKNOWN for r in siblings: if r.status not in _MECHANICAL_TODO: continue @@ -1866,6 +2125,12 @@ async def flag_divergence(project_id: int, *, since: datetime | None) -> int: continue if r.proposed_snippet_id == dom[0]: continue # the proposer already says "instance of the canon" + # The same structural test the write-time check applies + # (#4204). The sweep and the hook must agree about what counts + # as divergence, or an audit contradicts the line the writer + # was shown at the keyboard. + if not forms_agree(shape_form(r.signature or "", r.kind), cform): + continue r.diverges_from = dom[0] flagged += 1 await session.commit() diff --git a/tests/test_shape_form_gate.py b/tests/test_shape_form_gate.py new file mode 100644 index 0000000..40931d5 --- /dev/null +++ b/tests/test_shape_form_gate.py @@ -0,0 +1,265 @@ +"""A canon may only be urged on a shape that could plausibly BE it (#4204). + +WHAT WAS WRONG. `dominant_canon` is a base rate: it answers "what is most +common in this directory" and never "is this that". With `kind` carrying only +`css | sym`, a frozen dataclass, a module constant, a sync predicate, a class +and an async service function were all siblings, so the prior was the entire +argument — it told a registry module of pure helpers to build from the +`async_session` service canon. Upstream, the auto-stamp had no resemblance +floor at all (`elif sid in resembles`), so 0.69 asserted as confidently as +0.95, and one payload-level score was applied to every symbol in the file. + +The two halves compound: loose stamping manufactures the density that the +divergence check then reads as authority. Both are fixed by the same +primitive, and it is tested here on REAL signatures taken from the ledger — +Python, Go, TypeScript and Vue — rather than invented ones, because a +classifier that only works on the examples its author imagined is the failure +this is meant to end. + +The unknown case is tested hardest. `forms_agree` requires BOTH sides to be +known, so an unreadable signature makes the checks quieter rather than more +confident. A guard that treats "I cannot tell" as "match" is the shape of the +bug, not the fix. +""" +from __future__ import annotations + +import pytest + +from scribe.services.shape_ledger import ( + FORM_UNKNOWN, canon_form, forms_agree, shape_form, signature_in, +) + + +class _Row: + """A ledger row, as `canon_form` reads one.""" + + def __init__(self, signature, snippet_id=7, status="instance", kind="sym"): + self.signature, self.snippet_id = signature, snippet_id + self.status, self.kind = status, kind + + +# ── shape_form, on signatures actually in the ledger ────────────────────── + +@pytest.mark.parametrize("signature,want", [ + # Python + ("class SessionAbsent(RuntimeError):", "type"), + ("def build_channel() -> str:", "fn"), + ("async def attach(self) -> None:", "async-fn"), + ("async def _make_room(self, user_id: int, credential_id: int, opening) -> bool:", + "async-fn"), + ("def test_version_regex_rejects_bad_formats():", "fn"), + # Go + ("func NewService(pool *pgxpool.Pool, cfg *lidarrconfig.Service) *Service {", "fn"), + ("type Store struct {", "type"), + # TypeScript / Vue SFC + ("export function useBuild() {", "fn"), + ("function offsetWords(minutes: number): string {", "fn"), + ("export default class Thing {", "type"), + ("interface Lesson {", "type"), + ("const handler = async (req) => {", "async-fn"), + ("const offset = (n) => n + 1", "fn"), + ("const MAX_BUDGET = 10", "binding"), + # Unreadable + ("", FORM_UNKNOWN), + (" ", FORM_UNKNOWN), + ("# just a comment", FORM_UNKNOWN), + ("}", FORM_UNKNOWN), +]) +def test_shape_form_reads_real_signatures(signature, want) -> None: + assert shape_form(signature, "sym") == want + + +def test_css_is_its_own_form_regardless_of_signature() -> None: + assert shape_form(".pin {", "css") == "css" + assert shape_form("", "css") == "css" + + +def test_the_five_shapes_that_started_this_are_not_service_functions() -> None: + """The concrete case. #2860 is an `async def` service unit; none of these + is one, and all five were told to build from it.""" + canon = shape_form("async def create_note(user_id: int, ...):", "sym") + assert canon == "async-fn" + for sig in ( + "class Point:", + "def _p(source, kind, what, **kw) -> tuple[str, Point]:", + "def get_point(source: str) -> Point | None:", + "def is_registered(source: str) -> bool:", + "def sources_expected_to_emit() -> list[str]:", + ): + assert not forms_agree(shape_form(sig, "sym"), canon), sig + + +# ── forms_agree: unknown never matches ──────────────────────────────────── + +def test_two_known_equal_forms_agree() -> None: + assert forms_agree("fn", "fn") + + +def test_different_forms_do_not() -> None: + assert not forms_agree("fn", "async-fn") + assert not forms_agree("type", "fn") + + +@pytest.mark.parametrize("a,b", [ + (FORM_UNKNOWN, "fn"), ("fn", FORM_UNKNOWN), (FORM_UNKNOWN, FORM_UNKNOWN), +]) +def test_unknown_never_agrees_with_anything(a, b) -> None: + """Including with itself — "I cannot tell" twice is not a match.""" + assert not forms_agree(a, b) + + +# ── canon_form: a canon whose own rows disagree has no opinion ──────────── + +def test_a_canon_whose_instances_agree_asserts_that_form() -> None: + rows = [_Row("async def a():"), _Row("async def b():"), _Row("async def c():")] + assert canon_form(rows, 7) == "async-fn" + + +def test_a_canon_whose_instances_disagree_asserts_nothing() -> None: + """Portal's #3283 exactly: one snippet recorded as canon for a class, a + plain def, an async def and a dozen tests. That spread is the SYMPTOM of + loose stamping, so reading it as "no opinion" makes the two fixes + cooperate — a poisoned canon falls silent instead of flagging others.""" + rows = [_Row("class A:"), _Row("def b():"), _Row("async def c():")] + assert canon_form(rows, 7) == FORM_UNKNOWN + + +def test_a_clear_majority_still_asserts() -> None: + rows = [_Row("def a():"), _Row("def b():"), _Row("def c():"), _Row("class D:")] + assert canon_form(rows, 7) == "fn" + + +def test_rows_of_other_canons_and_unjudged_rows_do_not_vote() -> None: + rows = [ + _Row("def a():"), _Row("def b():"), _Row("def c():"), + _Row("class X:", snippet_id=9), # another canon + _Row("class Y:", status="unclassified"), # not judged + _Row("class Z:", status="variant"), # a departure, not a vote + ] + assert canon_form(rows, 7) == "fn" + + +def test_a_canon_with_no_readable_signatures_asserts_nothing() -> None: + assert canon_form([_Row(""), _Row(""), _Row("")], 7) == FORM_UNKNOWN + + +def test_a_canon_with_no_rows_at_all_asserts_nothing() -> None: + assert canon_form([], 7) == FORM_UNKNOWN + + +# ── signature_in: the payload is where a brand-new shape lives ──────────── + +CODE = ''' +import re + +MAX = 10 + +class Point: + """A point.""" + +async def fetch(user_id: int) -> None: + ... + +def helper(x): + return x + +export function useBuild() { +''' + + +@pytest.mark.parametrize("symbol,want_form", [ + ("Point", "type"), + ("fetch", "async-fn"), + ("helper", "fn"), + ("useBuild", "fn"), + ("MAX", "binding"), +]) +def test_a_definition_in_the_payload_is_found(symbol, want_form) -> None: + assert shape_form(signature_in(CODE, symbol, "sym"), "sym") == want_form + + +def test_a_symbol_that_is_only_CALLED_is_not_a_definition() -> None: + """The conflation worth keeping out: referencing a name is evidence of + USE, never of being that shape.""" + assert signature_in("value = helper(3)\nreturn fetch(x)", "helper", "sym") == "" + + +def test_a_missing_symbol_yields_nothing_rather_than_a_guess() -> None: + assert signature_in(CODE, "nowhere", "sym") == "" + assert shape_form(signature_in(CODE, "nowhere", "sym"), "sym") == FORM_UNKNOWN + + +def test_an_empty_payload_yields_nothing() -> None: + assert signature_in("", "Point", "sym") == "" + + +def test_a_css_class_is_found_by_its_selector() -> None: + assert signature_in(".pin { color: red; }", "pin", "css").startswith(".pin") + assert signature_in(".pin { }", ".pin", "css").startswith(".pin") + + +def test_a_near_name_is_not_matched() -> None: + """Word-bounded, so `helper` never claims `helper_two`.""" + assert signature_in("def helper_two(x):\n ...", "helper", "sym") == "" + + +# ── The burden scales with the evidence ─────────────────────────────────── +# +# Two kinds of evidence reach the stamp, and treating them alike was the +# first version of this fix and was wrong in both directions. An explicit +# by-name reference — the payload literally names the canon's symbol — is +# strong; demanding positive form agreement there silenced it whenever the +# shape's definition was not in the payload (an Edit rather than a Write), +# turning strong evidence into none for a reason unrelated to the code. +# A whole-file resemblance SCORE is weak: one number speaks for every symbol +# in the file, which is precisely how a class, an async method and a private +# helper were all recorded as instances of one snippet. + +from scribe.services.shape_ledger import _stamp_allowed, forms_conflict # noqa: E402 + + +@pytest.mark.parametrize("a,b,want", [ + ("fn", "type", True), + ("async-fn", "fn", True), + ("fn", "fn", False), + (FORM_UNKNOWN, "fn", False), # cannot contradict what you cannot read + ("fn", FORM_UNKNOWN, False), + (FORM_UNKNOWN, FORM_UNKNOWN, False), +]) +def test_forms_conflict_needs_both_sides_readable(a, b, want) -> None: + assert forms_conflict(a, b) is want + + +def test_a_named_reference_stands_unless_the_forms_contradict() -> None: + assert _stamp_allowed(2, "fn", "fn") + assert _stamp_allowed(2, FORM_UNKNOWN, "fn") # unreadable shape, strong evidence + assert _stamp_allowed(2, "fn", FORM_UNKNOWN) + assert not _stamp_allowed(2, "type", "fn") # a class is not that function + + +def test_a_resemblance_score_must_positively_agree() -> None: + """The weak path, and the one that produced the mess being fixed.""" + assert _stamp_allowed(1, "fn", "fn") + assert not _stamp_allowed(1, FORM_UNKNOWN, "fn") + assert not _stamp_allowed(1, "fn", FORM_UNKNOWN) + assert not _stamp_allowed(1, "type", "fn") + + +def test_the_portal_case_no_longer_stamps_one_canon_onto_a_whole_file() -> None: + """A class, an async method and a sync helper in one file, against one + payload-level score. Previously all three were stamped; now the score + alone cannot carry any of them, and only a matching form could.""" + canon = "async-fn" + written = [("class SessionAbsent(RuntimeError):", False), + ("async def attach(self) -> None:", True), + ("def _run_control_client(self, command: str) -> None:", False)] + for signature, should_stamp in written: + assert _stamp_allowed(1, shape_form(signature, "sym"), canon) is should_stamp + + +def test_the_resemblance_floor_is_above_the_retrieval_floors() -> None: + """An unattended WRITE about the codebase should need more evidence than + a suggestion shown to a reader — the retrieval bars sit near 0.70.""" + from scribe.services.shape_ledger import _RESEMBLE_MIN + + assert _RESEMBLE_MIN >= 0.80 diff --git a/tests/test_write_path_trigger.py b/tests/test_write_path_trigger.py index e8d488a..02c2f9b 100644 --- a/tests/test_write_path_trigger.py +++ b/tests/test_write_path_trigger.py @@ -1638,8 +1638,11 @@ async def test_the_write_time_divergence_check_is_named_in_band(): 1, "frontend/src/components/Danger.vue", code=REAL_CODE, project_id=24, stamp_shapes=[("sym", "confirmDanger")], ) + # The payload travels with the call now (#4204): the check compares the + # shape being written against the canon's form, and a shape written right + # now may exist nowhere but in this code. check.assert_awaited_once_with(24, "frontend/src/components/Danger.vue", - [("sym", "confirmDanger")], []) + [("sym", "confirmDanger")], [], REAL_CODE) assert out["divergence"] == div assert "Divergence check at `frontend/src/components/Danger.vue`" in out["context"] assert "`confirmDanger` → #2761 (20 of 21 judged siblings are its instances)" in out["context"]