docs(6205): coverage comments state the reason, not the instance that taught it

Issue, note and milestone citations are removed along with first-deploy
counts and the measured phantom-definition story; "button B" jargon becomes
the divergence it names. Each block keeps its present-tense reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:12:53 -04:00
co-authored by Claude Opus 5.5
parent c44372c8cd
commit 5598b307fa
+82 -84
View File
@@ -1,5 +1,5 @@
"""Pattern-library coverage — what fraction of a bound repo's shapes have a
recorded snippet (#2692, forge job 3 of decision #2686).
recorded snippet.
The all-shapes doctrine says every shape gets recorded at first build. This
module is the hoping→knowing move: it enumerates the definitions that exist in
@@ -43,9 +43,9 @@ logger = logging.getLogger(__name__)
# Cache key in the settings KV, on the project OWNER's user_id — the same
# channel the scheduler's last-run summary uses for machine-written state.
# v2 suffix with #2788: the payload shape inverted (accounted/unclassified);
# pre-ledger blobs under the old key simply stop being found, so the card
# honestly reads "not measured yet" until the first ledger-era refresh.
# The v2 suffix marks the accounted/unclassified payload shape; a blob under
# an older key is simply not found, so the card honestly reads "not measured
# yet" until the first refresh writes the current shape.
_CACHE_KEY_PREFIX = "pattern_coverage_v2_"
# Files whose content can't hold definitions — the hook's skip list, verbatim,
@@ -93,18 +93,15 @@ _ARROW_RE = re.compile(
)
# --- comment and string spans (#4222) ----------------------------------------
# --- comment and string spans -----------------------------------------------
#
# The matchers above are line-oriented and know nothing about what a line is
# INSIDE. A wrapped docstring whose line happens to begin "class AND the …"
# reads as a definition of a shape called `AND`, and that phantom reaches the
# agent mid-edit as a divergence prompt about a symbol that does not exist.
# This repo's own source minted twenty-two of them, measured by running the
# extractor over every scannable file with and without this scan. Two —
# `with` and `nobody`, out of one module docstring in
# scripts/check_dangling_styles.py — are persisted, JUDGED `code_shapes`
# rows, so the cost was never only noise in the moment. Those clear
# themselves: sync_shapes marks a row it no longer extracts as vanished.
# agent mid-edit as a divergence prompt about a symbol that does not exist —
# and, once judged, persists as a `code_shapes` row. Masking comment and
# string spans stops the phantoms; any already stored clear themselves, since
# sync_shapes marks a row it no longer extracts as vanished.
#
# The honest tool for .py would be `ast`, which cannot be fooled by prose at
# all. It is not what this uses, because this extractor is mirrored rule for
@@ -221,7 +218,7 @@ def _masked_lines(text: str, count: int) -> list[str]:
class Definition(NamedTuple):
"""One extracted definition with its content fingerprint (#2792).
"""One extracted definition with its content fingerprint.
`signature` is the definition line itself; `body_sha` hashes the block
whitespace- and comment-insensitively; `body` is the block's text, held
@@ -234,7 +231,7 @@ class Definition(NamedTuple):
signature: str
body_sha: str
body: str
line: int = -1 # 0-based line the definition starts on (#2869)
line: int = -1 # 0-based line the definition starts on
def _definition_on(raw: str) -> tuple[str, str] | None:
@@ -253,7 +250,7 @@ def _definition_on(raw: str) -> tuple[str, str] | None:
# `type` announces a definition only when something is declared after
# the name (`type Foo = …`, `type Foo struct {`); an import specifier
# (`import { type Foo, bar }`) is the same two words and defines
# nothing — it showed up as a two-file "identical body" family (#2904).
# nothing, and would read as a two-file "identical body" family.
if line.startswith("type") and not re.search(r"[={]", line[m.end():]):
return None
return ("sym", name)
@@ -325,10 +322,10 @@ def extract_definitions(text: str) -> list[Definition]:
end = j
break
block = lines[i:end]
# A CSS rule's fingerprint is its DECLARATIONS, not its selector
# (#2872): the row's identity already carries the selector. Since
# note 2917 the derive grouping no longer reads CSS bodies at all (a
# class is grouped by name only), so for CSS the fingerprint is the
# A CSS rule's fingerprint is its DECLARATIONS, not its selector:
# the row's identity already carries the selector. The derive
# grouping does not read CSS bodies at all (a class is grouped by
# name only), so for CSS the fingerprint is the
# recheck identity — "did this rule's body change since it was
# 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
@@ -336,20 +333,20 @@ def extract_definitions(text: str) -> list[Definition]:
if kind == "css":
# One-line rules (`.x { color: red; }`) carry their declarations on
# the selector line itself; a block that is only the selector plus
# trailing blanks must not hash to the empty string (which grouped
# 68 unrelated one-liners as one "copy" on first deploy, #2872).
# trailing blanks must not hash to the empty string, which would
# group every unrelated one-liner as one "copy".
first = lines[i]
brace = first.find("{")
head = [first[brace + 1:]] if brace >= 0 and first[brace + 1:].strip() else []
hashed = head + block[1:]
if not any(x.strip() for x in hashed):
hashed = block
# A SINGLE declaration is not a shape (#2903): `color: var(--fs-
# A SINGLE declaration is not a shape: `color: var(--fs-
# text-tertiary)` under .text-muted, .task-mark and .pin-badge-auto
# is three meanings sharing one line, not three copies of one
# rule. Keep the selector in the hash for one-liners; two
# declarations and up stay selector-agnostic. (Moot for grouping
# since note 2917, kept for fingerprint stability — see above.)
# declarations and up stay selector-agnostic. (Moot for grouping,
# kept for fingerprint stability — see above.)
elif _declaration_count(hashed) < 2:
hashed = block
else:
@@ -361,19 +358,20 @@ def extract_definitions(text: str) -> list[Definition]:
return out
# --- by-construction scope (#2869) -------------------------------------------
# --- by-construction scope ---------------------------------------------------
#
# 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
# stay in play for the proposer, derive grouping and divergence — identical
# rules copied across sibling components are found exactly there. Unscoped
# `<style>` in
# a .vue and every file that is neither .vue nor .svelte 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)
# Svelte scopes the other way round (#4608): every `<style>` is component-
# Svelte scopes the other way round: every `<style>` is component-
# scoped unless it opts out — `<style global>` (svelte-preprocess) for a whole
# block, `:global(...)` for one selector. Instance `<script>` code is the
# component's own; `<script context="module">` / `<script module>` is the one
@@ -443,12 +441,12 @@ def scoped_definitions(path: str, text: str, defs: list[Definition]) -> set[tupl
return out
# --- template class references: the CSS consumer map (milestone 302) ---------
# --- template class references: the CSS consumer map -------------------------
# Files whose MARKUP can consume a class. Styling consumers are templates —
# `querySelector('.x')` / classList in scripts are deliberately not read in
# v1 (note 2917: watch CSS by name, by recipe, by token and by what uses it;
# "what uses it" is the template).
# `querySelector('.x')` / classList in scripts are deliberately not read: CSS
# is watched by name, by recipe, by token and by what uses it, and "what uses
# it" is the template.
_TEMPLATE_SUFFIXES = (
".vue", ".html", ".htm", ".jsx", ".tsx", ".js", ".ts", ".svelte", ".astro",
)
@@ -473,7 +471,7 @@ _SVELTE_CLASS_RE = re.compile(r"(?<![:\w.-])class:([A-Za-z_][A-Za-z0-9_-]*)\s*="
# runtime, and React's <CSSTransition classNames="fade"> does the same. A
# reader of `class=` attributes alone therefore calls every one of those
# rules unused, which is a false positive no amount of care in the
# stylesheet can avoid (#2970). A dynamic `:name="…"` stays unknowable.
# stylesheet can avoid. A dynamic `:name="…"` stays unknowable.
_TRANSITION_NAME_RE = re.compile(
r"""<\s*[Tt]ransition(?:-[Gg]roup|Group)?\b[^>]*?(?<![:\w.-])name\s*=\s*"""
r"""(?:"([^"]*)"|'([^']*)')"""
@@ -552,8 +550,8 @@ def class_references(path: str, text: str) -> dict[str, int]:
`class:x` directive; never a CSS selector (`.x {` is a definition, read
by extract_definitions) and never a script's `querySelector('.x')`.
Two forms name classes without spelling them out, and both are read
(#2970): a transition `name=` stands for every class the framework
Two forms name classes without spelling them out, and both are read:
a transition `name=` stands for every class the framework
generates from it, and a concatenated name contributes the prefix
reference `head-*` — which resolve_consumers matches against every row
whose symbol starts with `head-`."""
@@ -597,7 +595,7 @@ def scannable(path: str) -> bool:
return not path.lower().endswith(_SKIP_SUFFIXES)
# --- the file as a unit (milestone 439) ---------------------------------------
# --- the file as a unit ------------------------------------------------------
#
# A component is the thing people reuse in a UI, and in a single-file
# component nothing inside is named after it: CatalogueBookCard.svelte defines
@@ -644,7 +642,7 @@ class ArchiveShape(NamedTuple):
signature: str
body_sha: str
body: str
scoped: bool = False # one-off by construction (#2869)
scoped: bool = False # one-off by construction
def shapes_from_archive(blob: bytes) -> list[tuple[str, str, str]]:
@@ -656,11 +654,11 @@ def shapes_from_archive(blob: bytes) -> list[tuple[str, str, str]]:
class ArchiveScan(NamedTuple):
"""One walk of a repo tarball: what each file DEFINES (the ledger rows)
and which class names each file's markup REFERENCES (the CSS consumer
map, milestone 302) — read together because the bodies are in hand once."""
map) — read together because the bodies are in hand once."""
definitions: list[ArchiveShape]
references: dict[str, dict[str, int]] # path → class token → count
# EVERY file's repo-relative path, scannable or not (milestone 463). The
# EVERY file's repo-relative path, scannable or not. The
# platform markers are files this scan otherwise skips — go.mod,
# AndroidManifest.xml, a Dockerfile — so detection reads the names here
# rather than re-walking the tarball.
@@ -722,9 +720,9 @@ def scan_archive(blob: bytes) -> ArchiveScan:
# --- matching shapes against recorded locations ------------------------------
# The covering predicate (location_covers) lives in services/shape_ledger.py
# since #2788 — the ledger's canonical marking and this module's readout are
# two consumers of ONE doctrine, and the ledger is its home.
# The covering predicate (location_covers) lives in services/shape_ledger.py:
# the ledger's canonical marking and this module's readout are two consumers
# of ONE doctrine, and the ledger is its home.
def largest_gaps(
@@ -756,7 +754,7 @@ async def _recorded_locations(
user_id: int, project_id: int
) -> list[tuple[int, str, str]]:
"""(snippet_note_id, path, symbol) for every location of every live
snippet in a project — the canonical-marking input (#2788): the id is what
snippet in a project — the canonical-marking input: the id is what
lets a ledger row point back at the snippet it references."""
from sqlalchemy import select
@@ -788,15 +786,14 @@ async def compute_coverage(
) -> dict | None:
"""Sync the shape ledger from the bound repos and read the accounting.
Since #2788 this is the ledger's sync point, not just a measurement:
every walk upserts the extracted shapes (new → unclassified, vanished →
stamped), re-stamps snippet reference locations as canonical, and then
reports the ACCOUNTING — how many shapes carry a classification at all —
rather than the old "has a snippet" fraction. Unclassified is the todo
(note 2786).
This is the ledger's sync point, not just a measurement: every walk
upserts the extracted shapes (new → unclassified, vanished → stamped),
re-stamps snippet reference locations as canonical, and then reports the
ACCOUNTING — how many shapes carry a classification at all — rather than
a "has a snippet" fraction. Unclassified is the todo.
None means "nothing to measure" — the owner's keyring serves none of the
project's bound repos (#2778). That is the ordinary state for a
project's bound repos. That is the ordinary state for a
forge-less user and every caller treats it as silence, not failure; the
ledger is untouched in that case. Forge errors (unreachable, bad token)
RAISE — the two callers are a refresh button and a background task, and
@@ -818,7 +815,7 @@ async def compute_coverage(
tree_paths: list[str] = []
recorded = await _recorded_locations(user_id, project_id)
# The proposer's canon catalog, read once per refresh and shared across
# the project's repos (#2792).
# the project's repos.
canons = None
proposer_stats = {"examined": 0, "proposed": 0, "semantic_checked": 0}
for binding in await bindings_for_project(user_id, project_id):
@@ -827,8 +824,8 @@ async def compute_coverage(
if hit is None:
continue # bound to a host no connection serves
forge, api_repo = hit
# The binding's own ref when it names one (#2873: a dev-first project
# has its ledger follow dev), else the forge's default branch.
# The binding's own ref when it names one (a dev-first project has
# its ledger follow dev), else the forge's default branch.
ref = binding.ref or await forge.default_branch(api_repo)
scan = scan_archive(await forge.archive(api_repo, ref))
definitions = scan.definitions
@@ -844,7 +841,7 @@ async def compute_coverage(
project_id, key, definitions, seen_marker=marker
)
served.append((key, ref))
# The CSS consumer map (milestone 302) rides the same archive: which
# The CSS consumer map rides the same archive: which
# files' markup names each class. Mechanical and recomputable, so it
# must not be able to fail the refresh either.
try:
@@ -868,7 +865,7 @@ async def compute_coverage(
if not served:
return None
# Platform detection (milestone 463) rides the same walk: the paths are in
# Platform detection rides the same walk: the paths are in
# hand once. It only ever ADDS membership the project has no answer for,
# and it must not be able to fail the refresh it rides on.
try:
@@ -879,7 +876,7 @@ async def compute_coverage(
logger.warning("platform detection failed for project %s", project_id, exc_info=True)
await shape_ledger.mark_canonicals(project_id, recorded)
# The adoption ledger follows the shapes (milestone 463 step 5). The
# The adoption ledger follows the shapes. The
# judge paths sync the ideas they touch; the refresh is the one place
# that sees canonical stamps land and shapes vanish, so it answers every
# canon idea reaching the project. It must not fail the refresh.
@@ -895,7 +892,7 @@ async def compute_coverage(
logger.warning("derive-first grouping failed", exc_info=True)
# "Since the previous computation" — the cache's stamp. A first seed has
# none, so nothing is new then. Read once; two passes use it: the
# button-B flag (#2793) and the derive-new drift count (#2899).
# divergence flag and the derive-new drift count.
since = None
try:
previous = await get_setting(user_id, f"{_CACHE_KEY_PREFIX}{project_id}")
@@ -904,7 +901,7 @@ async def compute_coverage(
since = datetime.fromisoformat(stamp) if stamp else None
except Exception:
logger.warning("previous coverage stamp unreadable", exc_info=True)
# The button-B pass (#2793): shapes new since the PREVIOUS computation,
# The divergence pass: shapes new since the PREVIOUS computation,
# where a canon dominates.
try:
await shape_ledger.flag_divergence(project_id, since=since)
@@ -925,7 +922,7 @@ async def compute_coverage(
agg["accounted"] += row.status != "unclassified"
unclassified = counts.pop("unclassified")
# The CSS consumer map's readout (milestone 302): which files render each
# The CSS consumer map's readout: which files render each
# css row — on the derive groups (a shared recipe vs a scoped one is a
# count), and the negative space: css rules no template names. "Unused"
# is measured only where the map has evidence of templates at all (one
@@ -944,16 +941,16 @@ async def compute_coverage(
proposals = shape_ledger.proposal_summary(rows, consumer_paths=consumer_paths)
divergence = shape_ledger.divergence_summary(rows)
derive_new = shape_ledger.derive_new_summary(rows, since=since)
# What `stamps_to_review` would list (#4608). The review tool existed and
# nobody on two projects knew to call it: 132 and 488 weak stamps sat
# electing canons for weeks. The arrival line is the one place a
# project's own session is sure to look, so the count goes there.
# What `stamps_to_review` would list. A review tool nobody knows to call
# leaves weak stamps electing canons indefinitely; the arrival line is the
# one place a project's own session is sure to look, so the count goes
# there.
try:
review = shape_ledger.stamps_to_review(rows, top=0)
except Exception:
logger.warning("stamp review read failed", exc_info=True)
review = {}
# The write-time figure (milestone 439): how often the end-of-turn
# The write-time figure: how often the end-of-turn
# question was put on this project, and how often it was walked past.
# An audit reads the ledger; this reads whether the reflex held.
try:
@@ -968,27 +965,28 @@ async def compute_coverage(
"accounted": len(rows) - unclassified,
"unclassified": unclassified,
"counts": counts,
# The proposer's standing (#2792): canon proposals awaiting a
# The proposer's standing: canon proposals awaiting a
# confirm, the largest derive-first groups, and what this refresh did.
"proposed": proposals["proposed"],
"derive_groups": proposals["derive_groups"],
"top_canon": proposals.get("top_canon"),
# Drift since the previous refresh (#2899): copies that joined a
# Drift since the previous refresh: copies that joined a
# duplicate family — what the arrival line names so drift is noticed
# on entering, not found by an audit.
"derive_new": derive_new,
# The consumer map's negative space (milestone 302): live css rules
# The consumer map's negative space: live css rules
# no template names — None when the map has no evidence of templates.
"unused_css": unused_css,
"proposer": proposer_stats,
# The divergence readout (#2793): button B where button A is canon,
# and judged shapes whose bodies moved since they were judged.
# The divergence readout: new shapes where a different canon
# dominates, and judged shapes whose bodies moved since they were
# judged.
"divergent": divergence["divergent"],
"divergence": divergence["divergence"],
"recheck": divergence["recheck"],
# Unattended stamps below today's floor, and canons whose members no
# longer agree what they are — the judgment queue `stamps_to_review`
# holds (#4608).
# holds.
"weak_stamps": review.get("weak_count", 0),
"incoherent_canons": review.get("incoherent_count", 0),
# {checked, asked, left, days} from the Stop hook's recorded outcomes.
@@ -1021,9 +1019,9 @@ async def refresh_coverage(
return coverage
# The arrival-moment self-seed (#2802). A ledger that has never been computed
# used to wait for someone to find the UI Refresh button; entering the project
# is the moment the number is wanted, so entering is the moment it seeds.
# The arrival-moment self-seed. A ledger that has never been computed should
# not wait for someone to find the UI Refresh button; entering the project is
# the moment the number is wanted, so entering is the moment it seeds.
_SEED_MAX_AGE = timedelta(hours=24)
# Projects with a refresh already running — concurrent enters must not fetch
@@ -1056,7 +1054,7 @@ async def refresh_if_stale(
return
selector = await get_forges(user_id, project_id)
if not selector.configured:
return # rule #115: forge-less stays exactly as it was
return # forge-less stays exactly as it was
_inflight_seed.add(project_id)
try:
await refresh_coverage(user_id, project_id, selector=selector)
@@ -1070,7 +1068,7 @@ async def refresh_if_stale(
async def refresh_for_caller(caller_id: int, project_id: int) -> dict:
"""The explicit agent-facing refresh (#2802) — synchronous, named errors.
"""The explicit agent-facing refresh — synchronous, named errors.
Write-gated: recomputing spends the owner's forge API budget and rewrites
ledger rows' seen-markers, so a read share doesn't grant it. Resolution
@@ -1134,10 +1132,10 @@ def coverage_line(coverage: dict) -> str:
line += f" — {breakdown}"
line += f" (estimate{', computed ' + day if day else ''})"
unclassified = coverage.get("unclassified", 0)
# The standing work, built whatever the todo count (#2899). Since the
# scoped bucket (#2869) a ledger can read 100% accounted and still carry
# derive groups, proposals and divergence; gating this block on
# `unclassified > 0` is how 439 derive rows went unmentioned.
# The standing work, built whatever the todo count. With the scoped
# bucket a ledger can read 100% accounted and still carry derive groups,
# proposals and divergence; gating this block on `unclassified > 0` would
# leave them unmentioned.
standing = []
if coverage.get("proposed"):
standing.append(f"{coverage['proposed']} proposed")
@@ -1157,7 +1155,7 @@ def coverage_line(coverage: dict) -> str:
standing.append(f"+{n} new cop{'y' if n == 1 else 'ies'} since last refresh{where}")
if coverage.get("divergent"):
standing.append(f"{coverage['divergent']} DIVERGENT")
# The next action, on the line (#2874): the canon with the biggest
# The next action, on the line: the canon with the biggest
# queue to confirm, and the widest body-identical copy to consolidate.
top = coverage.get("top_canon") or {}
if top.get("snippet_id"):
@@ -1165,7 +1163,7 @@ def coverage_line(coverage: dict) -> str:
first = (coverage.get("derive_groups") or [{}])[0]
if first.get("label") and first.get("files"):
top_copy = f"top copy {first['label']} ×{first['files']} files"
# A css family says what renders it (milestone 302): the count that
# A css family says what renders it: the count that
# tells a shared recipe from a scoped convention.
if "consumers" in first:
n_t = (first.get("consumers") or {}).get("count", 0)
@@ -1185,8 +1183,8 @@ def coverage_line(coverage: dict) -> str:
line += f"; standing: {', '.join(standing)}"
if coverage.get("recheck"):
line += f"; {coverage['recheck']} judged shape{'s' if coverage['recheck'] != 1 else ''} changed since judged — recheck"
# Rows the hook asserted on evidence the ledger no longer accepts (#4608).
# They no longer elect a canon, but they still claim to be instances
# Rows the hook asserted on evidence below the ledger's floor. They do
# not elect a canon, but they still claim to be instances
# until someone reads them — the tool to call is named, not implied.
review = []
if coverage.get("weak_stamps"):
@@ -1197,7 +1195,7 @@ def coverage_line(coverage: dict) -> str:
review.append(f"{n_i} incoherent canon{'s' if n_i != 1 else ''}")
if review:
line += f"; {' · '.join(review)} to judge — stamps_to_review"
# Whether the writers judged what they wrote (milestone 439). Silent until
# Whether the writers judged what they wrote. Silent until
# the question has been put at least once: a project nobody has written to
# through the plugin has no figure, not a perfect one.
wt = coverage.get("write_time") or {}