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 """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 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 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 # 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. # channel the scheduler's last-run summary uses for machine-written state.
# v2 suffix with #2788: the payload shape inverted (accounted/unclassified); # The v2 suffix marks the accounted/unclassified payload shape; a blob under
# pre-ledger blobs under the old key simply stop being found, so the card # an older key is simply not found, so the card honestly reads "not measured
# honestly reads "not measured yet" until the first ledger-era refresh. # yet" until the first refresh writes the current shape.
_CACHE_KEY_PREFIX = "pattern_coverage_v2_" _CACHE_KEY_PREFIX = "pattern_coverage_v2_"
# Files whose content can't hold definitions — the hook's skip list, verbatim, # 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 # 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 …" # 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 # 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. # 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 # and, once judged, persists as a `code_shapes` row. Masking comment and
# extractor over every scannable file with and without this scan. Two — # string spans stops the phantoms; any already stored clear themselves, since
# `with` and `nobody`, out of one module docstring in # sync_shapes marks a row it no longer extracts as vanished.
# 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.
# #
# The honest tool for .py would be `ast`, which cannot be fooled by prose at # 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 # 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): 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 `signature` is the definition line itself; `body_sha` hashes the block
whitespace- and comment-insensitively; `body` is the block's text, held whitespace- and comment-insensitively; `body` is the block's text, held
@@ -234,7 +231,7 @@ class Definition(NamedTuple):
signature: str signature: str
body_sha: str body_sha: str
body: 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: 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 # `type` announces a definition only when something is declared after
# the name (`type Foo = …`, `type Foo struct {`); an import specifier # the name (`type Foo = …`, `type Foo struct {`); an import specifier
# (`import { type Foo, bar }`) is the same two words and defines # (`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():]): if line.startswith("type") and not re.search(r"[={]", line[m.end():]):
return None return None
return ("sym", name) return ("sym", name)
@@ -325,10 +322,10 @@ def extract_definitions(text: str) -> list[Definition]:
end = j end = j
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. Since # the row's identity already carries the selector. The derive
# note 2917 the derive grouping no longer reads CSS bodies at all (a # grouping does not read CSS bodies at all (a class is grouped by
# class is grouped by name only), so for CSS the fingerprint is the # name only), so for CSS the fingerprint is the
# recheck identity — "did this rule's body change since it was # 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 # 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 # 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": 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
# trailing blanks must not hash to the empty string (which grouped # trailing blanks must not hash to the empty string, which would
# 68 unrelated one-liners as one "copy" on first deploy, #2872). # group every unrelated one-liner as one "copy".
first = lines[i] first = lines[i]
brace = first.find("{") brace = first.find("{")
head = [first[brace + 1:]] if brace >= 0 and first[brace + 1:].strip() else [] head = [first[brace + 1:]] if brace >= 0 and first[brace + 1:].strip() else []
hashed = head + block[1:] hashed = head + block[1:]
if not any(x.strip() for x in hashed): if not any(x.strip() for x in hashed):
hashed = block 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 # 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. Keep the selector in the hash for one-liners; two # rule. Keep the selector in the hash for one-liners; two
# declarations and up stay selector-agnostic. (Moot for grouping # declarations and up stay selector-agnostic. (Moot for grouping,
# since note 2917, kept for fingerprint stability — see above.) # kept for fingerprint stability — see above.)
elif _declaration_count(hashed) < 2: elif _declaration_count(hashed) < 2:
hashed = block hashed = block
else: else:
@@ -361,19 +358,20 @@ def extract_definitions(text: str) -> list[Definition]:
return out return out
# --- by-construction scope (#2869) ------------------------------------------- # --- by-construction scope ---------------------------------------------------
# #
# A Vue single-file component's `<style scoped>` rules and its `<script setup>` # 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 # functions cannot be reached from any other file: they are one-offs by
# construction, not by judgment. The sync stamps them `scoped` (mechanical) so # 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 # 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 # stay in play for the proposer, derive grouping and divergence — identical
# auth views' identical rules were found exactly there. Unscoped `<style>` in # 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. # 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_OPEN_RE = re.compile(r"^\s*<style\b[^>]*\bscoped\b", re.IGNORECASE)
_STYLE_CLOSE_RE = re.compile(r"^\s*</style\s*>", 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 # scoped unless it opts out — `<style global>` (svelte-preprocess) for a whole
# block, `:global(...)` for one selector. Instance `<script>` code is the # block, `:global(...)` for one selector. Instance `<script>` code is the
# component's own; `<script context="module">` / `<script module>` is the one # 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 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 — # Files whose MARKUP can consume a class. Styling consumers are templates —
# `querySelector('.x')` / classList in scripts are deliberately not read in # `querySelector('.x')` / classList in scripts are deliberately not read: CSS
# v1 (note 2917: watch CSS by name, by recipe, by token and by what uses it; # is watched by name, by recipe, by token and by what uses it, and "what uses
# "what uses it" is the template). # it" is the template.
_TEMPLATE_SUFFIXES = ( _TEMPLATE_SUFFIXES = (
".vue", ".html", ".htm", ".jsx", ".tsx", ".js", ".ts", ".svelte", ".astro", ".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 # runtime, and React's <CSSTransition classNames="fade"> does the same. A
# reader of `class=` attributes alone therefore calls every one of those # reader of `class=` attributes alone therefore calls every one of those
# rules unused, which is a false positive no amount of care in the # 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( _TRANSITION_NAME_RE = re.compile(
r"""<\s*[Tt]ransition(?:-[Gg]roup|Group)?\b[^>]*?(?<![:\w.-])name\s*=\s*""" r"""<\s*[Tt]ransition(?:-[Gg]roup|Group)?\b[^>]*?(?<![:\w.-])name\s*=\s*"""
r"""(?:"([^"]*)"|'([^']*)')""" 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 `class:x` directive; never a CSS selector (`.x {` is a definition, read
by extract_definitions) and never a script's `querySelector('.x')`. by extract_definitions) and never a script's `querySelector('.x')`.
Two forms name classes without spelling them out, and both are read Two forms name classes without spelling them out, and both are read:
(#2970): a transition `name=` stands for every class the framework a transition `name=` stands for every class the framework
generates from it, and a concatenated name contributes the prefix generates from it, and a concatenated name contributes the prefix
reference `head-*` — which resolve_consumers matches against every row reference `head-*` — which resolve_consumers matches against every row
whose symbol starts with `head-`.""" whose symbol starts with `head-`."""
@@ -597,7 +595,7 @@ def scannable(path: str) -> bool:
return not path.lower().endswith(_SKIP_SUFFIXES) 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 # 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 # component nothing inside is named after it: CatalogueBookCard.svelte defines
@@ -644,7 +642,7 @@ class ArchiveShape(NamedTuple):
signature: str signature: str
body_sha: str body_sha: str
body: 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]]: 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): class ArchiveScan(NamedTuple):
"""One walk of a repo tarball: what each file DEFINES (the ledger rows) """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 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] definitions: list[ArchiveShape]
references: dict[str, dict[str, int]] # path → class token → count 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, # platform markers are files this scan otherwise skips — go.mod,
# AndroidManifest.xml, a Dockerfile — so detection reads the names here # AndroidManifest.xml, a Dockerfile — so detection reads the names here
# rather than re-walking the tarball. # rather than re-walking the tarball.
@@ -722,9 +720,9 @@ def scan_archive(blob: bytes) -> ArchiveScan:
# --- matching shapes against recorded locations ------------------------------ # --- matching shapes against recorded locations ------------------------------
# The covering predicate (location_covers) lives in services/shape_ledger.py # The covering predicate (location_covers) lives in services/shape_ledger.py:
# since #2788 — the ledger's canonical marking and this module's readout are # the ledger's canonical marking and this module's readout are two consumers
# two consumers of ONE doctrine, and the ledger is its home. # of ONE doctrine, and the ledger is its home.
def largest_gaps( def largest_gaps(
@@ -756,7 +754,7 @@ async def _recorded_locations(
user_id: int, project_id: int user_id: int, project_id: int
) -> list[tuple[int, str, str]]: ) -> list[tuple[int, str, str]]:
"""(snippet_note_id, path, symbol) for every location of every live """(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.""" lets a ledger row point back at the snippet it references."""
from sqlalchemy import select from sqlalchemy import select
@@ -788,15 +786,14 @@ async def compute_coverage(
) -> dict | None: ) -> dict | None:
"""Sync the shape ledger from the bound repos and read the accounting. """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: This is the ledger's sync point, not just a measurement: every walk
every walk upserts the extracted shapes (new → unclassified, vanished → upserts the extracted shapes (new → unclassified, vanished → stamped),
stamped), re-stamps snippet reference locations as canonical, and then re-stamps snippet reference locations as canonical, and then reports the
reports the ACCOUNTING — how many shapes carry a classification at all — ACCOUNTING — how many shapes carry a classification at all — rather than
rather than the old "has a snippet" fraction. Unclassified is the todo a "has a snippet" fraction. Unclassified is the todo.
(note 2786).
None means "nothing to measure" — the owner's keyring serves none of the 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 forge-less user and every caller treats it as silence, not failure; the
ledger is untouched in that case. Forge errors (unreachable, bad token) ledger is untouched in that case. Forge errors (unreachable, bad token)
RAISE — the two callers are a refresh button and a background task, and 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] = [] tree_paths: list[str] = []
recorded = await _recorded_locations(user_id, project_id) recorded = await _recorded_locations(user_id, project_id)
# The proposer's canon catalog, read once per refresh and shared across # The proposer's canon catalog, read once per refresh and shared across
# the project's repos (#2792). # the project's repos.
canons = None canons = None
proposer_stats = {"examined": 0, "proposed": 0, "semantic_checked": 0} proposer_stats = {"examined": 0, "proposed": 0, "semantic_checked": 0}
for binding in await bindings_for_project(user_id, project_id): for binding in await bindings_for_project(user_id, project_id):
@@ -827,8 +824,8 @@ async def compute_coverage(
if hit is None: if hit is None:
continue # bound to a host no connection serves continue # bound to a host no connection serves
forge, api_repo = hit forge, api_repo = hit
# The binding's own ref when it names one (#2873: a dev-first project # The binding's own ref when it names one (a dev-first project has
# has its ledger follow dev), else the forge's default branch. # its ledger follow dev), else the forge's default branch.
ref = binding.ref or await forge.default_branch(api_repo) ref = binding.ref or await forge.default_branch(api_repo)
scan = scan_archive(await forge.archive(api_repo, ref)) scan = scan_archive(await forge.archive(api_repo, ref))
definitions = scan.definitions definitions = scan.definitions
@@ -844,7 +841,7 @@ async def compute_coverage(
project_id, key, definitions, seen_marker=marker project_id, key, definitions, seen_marker=marker
) )
served.append((key, ref)) 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 # files' markup names each class. Mechanical and recomputable, so it
# must not be able to fail the refresh either. # must not be able to fail the refresh either.
try: try:
@@ -868,7 +865,7 @@ async def compute_coverage(
if not served: if not served:
return None 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, # 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. # and it must not be able to fail the refresh it rides on.
try: try:
@@ -879,7 +876,7 @@ async def compute_coverage(
logger.warning("platform detection failed for project %s", project_id, exc_info=True) logger.warning("platform detection failed for project %s", project_id, exc_info=True)
await shape_ledger.mark_canonicals(project_id, recorded) 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 # 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 # that sees canonical stamps land and shapes vanish, so it answers every
# canon idea reaching the project. It must not fail the refresh. # 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) logger.warning("derive-first grouping failed", exc_info=True)
# "Since the previous computation" — the cache's stamp. A first seed has # "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 # 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 since = None
try: try:
previous = await get_setting(user_id, f"{_CACHE_KEY_PREFIX}{project_id}") 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 since = datetime.fromisoformat(stamp) if stamp else None
except Exception: except Exception:
logger.warning("previous coverage stamp unreadable", exc_info=True) 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. # where a canon dominates.
try: try:
await shape_ledger.flag_divergence(project_id, since=since) await shape_ledger.flag_divergence(project_id, since=since)
@@ -925,7 +922,7 @@ async def compute_coverage(
agg["accounted"] += row.status != "unclassified" agg["accounted"] += row.status != "unclassified"
unclassified = counts.pop("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 # 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" # count), and the negative space: css rules no template names. "Unused"
# is measured only where the map has evidence of templates at all (one # 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) proposals = shape_ledger.proposal_summary(rows, consumer_paths=consumer_paths)
divergence = shape_ledger.divergence_summary(rows) divergence = shape_ledger.divergence_summary(rows)
derive_new = shape_ledger.derive_new_summary(rows, since=since) derive_new = shape_ledger.derive_new_summary(rows, since=since)
# What `stamps_to_review` would list (#4608). The review tool existed and # What `stamps_to_review` would list. A review tool nobody knows to call
# nobody on two projects knew to call it: 132 and 488 weak stamps sat # leaves weak stamps electing canons indefinitely; the arrival line is the
# electing canons for weeks. The arrival line is the one place a # one place a project's own session is sure to look, so the count goes
# project's own session is sure to look, so the count goes there. # there.
try: try:
review = shape_ledger.stamps_to_review(rows, top=0) review = shape_ledger.stamps_to_review(rows, top=0)
except Exception: except Exception:
logger.warning("stamp review read failed", exc_info=True) logger.warning("stamp review read failed", exc_info=True)
review = {} 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. # question was put on this project, and how often it was walked past.
# An audit reads the ledger; this reads whether the reflex held. # An audit reads the ledger; this reads whether the reflex held.
try: try:
@@ -968,27 +965,28 @@ async def compute_coverage(
"accounted": len(rows) - unclassified, "accounted": len(rows) - unclassified,
"unclassified": unclassified, "unclassified": unclassified,
"counts": counts, "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. # confirm, the largest derive-first groups, and what this refresh did.
"proposed": proposals["proposed"], "proposed": proposals["proposed"],
"derive_groups": proposals["derive_groups"], "derive_groups": proposals["derive_groups"],
"top_canon": proposals.get("top_canon"), "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 # duplicate family — what the arrival line names so drift is noticed
# on entering, not found by an audit. # on entering, not found by an audit.
"derive_new": derive_new, "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. # no template names — None when the map has no evidence of templates.
"unused_css": unused_css, "unused_css": unused_css,
"proposer": proposer_stats, "proposer": proposer_stats,
# The divergence readout (#2793): button B where button A is canon, # The divergence readout: new shapes where a different canon
# and judged shapes whose bodies moved since they were judged. # dominates, and judged shapes whose bodies moved since they were
# judged.
"divergent": divergence["divergent"], "divergent": divergence["divergent"],
"divergence": divergence["divergence"], "divergence": divergence["divergence"],
"recheck": divergence["recheck"], "recheck": divergence["recheck"],
# Unattended stamps below today's floor, and canons whose members no # Unattended stamps below today's floor, and canons whose members no
# longer agree what they are — the judgment queue `stamps_to_review` # longer agree what they are — the judgment queue `stamps_to_review`
# holds (#4608). # holds.
"weak_stamps": review.get("weak_count", 0), "weak_stamps": review.get("weak_count", 0),
"incoherent_canons": review.get("incoherent_count", 0), "incoherent_canons": review.get("incoherent_count", 0),
# {checked, asked, left, days} from the Stop hook's recorded outcomes. # {checked, asked, left, days} from the Stop hook's recorded outcomes.
@@ -1021,9 +1019,9 @@ async def refresh_coverage(
return coverage return coverage
# The arrival-moment self-seed (#2802). A ledger that has never been computed # The arrival-moment self-seed. A ledger that has never been computed should
# used to wait for someone to find the UI Refresh button; entering the project # not wait for someone to find the UI Refresh button; entering the project is
# is the moment the number is wanted, so entering is the moment it seeds. # the moment the number is wanted, so entering is the moment it seeds.
_SEED_MAX_AGE = timedelta(hours=24) _SEED_MAX_AGE = timedelta(hours=24)
# Projects with a refresh already running — concurrent enters must not fetch # Projects with a refresh already running — concurrent enters must not fetch
@@ -1056,7 +1054,7 @@ async def refresh_if_stale(
return return
selector = await get_forges(user_id, project_id) selector = await get_forges(user_id, project_id)
if not selector.configured: 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) _inflight_seed.add(project_id)
try: try:
await refresh_coverage(user_id, project_id, selector=selector) 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: 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 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 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" — {breakdown}"
line += f" (estimate{', computed ' + day if day else ''})" line += f" (estimate{', computed ' + day if day else ''})"
unclassified = coverage.get("unclassified", 0) unclassified = coverage.get("unclassified", 0)
# The standing work, built whatever the todo count (#2899). Since the # The standing work, built whatever the todo count. With the scoped
# scoped bucket (#2869) a ledger can read 100% accounted and still carry # bucket a ledger can read 100% accounted and still carry derive groups,
# derive groups, proposals and divergence; gating this block on # proposals and divergence; gating this block on `unclassified > 0` would
# `unclassified > 0` is how 439 derive rows went unmentioned. # leave them unmentioned.
standing = [] standing = []
if coverage.get("proposed"): if coverage.get("proposed"):
standing.append(f"{coverage['proposed']} 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}") standing.append(f"+{n} new cop{'y' if n == 1 else 'ies'} since last refresh{where}")
if coverage.get("divergent"): if coverage.get("divergent"):
standing.append(f"{coverage['divergent']} 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. # queue to confirm, and the widest body-identical copy to consolidate.
top = coverage.get("top_canon") or {} top = coverage.get("top_canon") or {}
if top.get("snippet_id"): if top.get("snippet_id"):
@@ -1165,7 +1163,7 @@ def coverage_line(coverage: dict) -> str:
first = (coverage.get("derive_groups") or [{}])[0] first = (coverage.get("derive_groups") or [{}])[0]
if first.get("label") and first.get("files"): if first.get("label") and first.get("files"):
top_copy = f"top copy {first['label']} ×{first['files']} 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. # tells a shared recipe from a scoped convention.
if "consumers" in first: if "consumers" in first:
n_t = (first.get("consumers") or {}).get("count", 0) n_t = (first.get("consumers") or {}).get("count", 0)
@@ -1185,8 +1183,8 @@ def coverage_line(coverage: dict) -> str:
line += f"; standing: {', '.join(standing)}" line += f"; standing: {', '.join(standing)}"
if coverage.get("recheck"): if coverage.get("recheck"):
line += f"; {coverage['recheck']} judged shape{'s' if coverage['recheck'] != 1 else ''} changed since judged — 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). # Rows the hook asserted on evidence below the ledger's floor. They do
# They no longer elect a canon, but they still claim to be instances # not elect a canon, but they still claim to be instances
# until someone reads them — the tool to call is named, not implied. # until someone reads them — the tool to call is named, not implied.
review = [] review = []
if coverage.get("weak_stamps"): 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 ''}") review.append(f"{n_i} incoherent canon{'s' if n_i != 1 else ''}")
if review: if review:
line += f"; {' · '.join(review)} to judge — stamps_to_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 # the question has been put at least once: a project nobody has written to
# through the plugin has no figure, not a perfect one. # through the plugin has no figure, not a perfect one.
wt = coverage.get("write_time") or {} wt = coverage.get("write_time") or {}