session-memory: map signals to catalog recommendations via covers (WP-0010 follow-up)

Closes the gap where recurring_error suggestions showed generic 'Investigate'
instead of the curated recommendation. Added a covers[] field to SolutionPattern
(lowercase substrings a pattern's recommendation also applies to) + Catalog.find_for
(exact key first, then covers match against signal key+locus). Retro now resolves
recommendations through find_for. Tagged the read-before-edit pattern with
covers=['file has not been read','modified since read','file_not_read'] (v1.0.1).
Live: file-not-read suggestions across all repos now inherit 'Read the file before
Edit/Write'. 6 new tests; suite 158/158.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 21:09:44 +02:00
parent 0d05dfcc5d
commit e237dcc622
9 changed files with 143 additions and 35 deletions

View File

@@ -0,0 +1 @@
{"covers": [], "created_at": "2026-06-07T13:26:25Z", "distribution_ready": true, "id": "sp-problem-file_not_read-edit", "name": "Read before you Edit", "polarity": "problem", "problem": "Agents call Edit/Write on a file they have not read in the current session, or after it changed under them. The edit tools reject this ('File has not been read yet' / 'File has been modified since read'), and the retry burns a turn. Top recurring error in the corpus (12/27 sessions, 8 repos).", "provenance": {"detected_at": null, "evidence": {"frequency": 32, "origin": "AGENTIC-WP-0006 error mining / ASSESSMENT-infra-friction.md", "polarity": "problem", "repos": 8, "sessions": 12}, "promoted_at": null, "source_key": "problem:file_not_read:edit"}, "rendering_hints": {"claude": {"target": "CLAUDE.md"}, "codex": {"target": "AGENTS.md"}, "grok": {"target": ".grok/instructions.md"}}, "resolutions": [{"detail": "Never blind-write a file you haven't read this session.", "steps": ["Read the target file", "Then Edit/Write"], "summary": "Read the file (or the region you'll touch) before Edit/Write"}, {"detail": "A stale read means the file changed under you; refresh, don't loop.", "steps": ["Re-Read the file", "Re-apply the Edit"], "summary": "On 'modified since read', re-Read then re-Edit"}], "schema_version": 1, "scope": {"domains": [], "flavors": [], "repos": []}, "status": "superseded", "updated_at": "2026-06-07T13:26:25Z", "version": "1.0.0"}

View File

@@ -1,4 +1,9 @@
{ {
"covers": [
"file has not been read",
"modified since read",
"file_not_read"
],
"created_at": "2026-06-07T13:26:25Z", "created_at": "2026-06-07T13:26:25Z",
"distribution_ready": true, "distribution_ready": true,
"id": "sp-problem-file_not_read-edit", "id": "sp-problem-file_not_read-edit",
@@ -53,6 +58,6 @@
"repos": [] "repos": []
}, },
"status": "approved", "status": "approved",
"updated_at": "2026-06-07T13:26:25Z", "updated_at": "2026-06-07T19:06:45Z",
"version": "1.0.0" "version": "1.0.1"
} }

View File

@@ -30,7 +30,7 @@ from .schema import SolutionPattern
# Content fields that define a pattern's substance. Version, timestamps, status, # Content fields that define a pattern's substance. Version, timestamps, status,
# and distribution_ready are metadata — changes to them never bump the version. # and distribution_ready are metadata — changes to them never bump the version.
_CONTENT_KEYS = ("name", "polarity", "problem", "resolutions", "scope", _CONTENT_KEYS = ("name", "polarity", "problem", "resolutions", "scope",
"provenance", "rendering_hints") "provenance", "rendering_hints", "covers")
ADDED = "added" ADDED = "added"
UNCHANGED = "unchanged" UNCHANGED = "unchanged"
@@ -86,6 +86,22 @@ class Catalog:
with open(path, encoding="utf-8") as fh: with open(path, encoding="utf-8") as fh:
return [json.loads(line) for line in fh if line.strip()] return [json.loads(line) for line in fh if line.strip()]
def find_for(self, signal_key: str, locus: str = "") -> Optional[SolutionPattern]:
"""Best catalog pattern for a detect signal: exact id first, then ``covers``.
Lets a signal that doesn't share a pattern's exact key (e.g. a
``recurring_error`` fingerprint) inherit the curated recommendation when a
pattern declares it covers that text.
"""
exact = self.load(SolutionPattern.make_id(signal_key))
if exact is not None:
return exact
hay = f"{signal_key} {locus}".lower()
for p in self.list(): # sorted by id -> deterministic
if any(c.lower() in hay for c in p.covers):
return p
return None
# --- the single write path --------------------------------------------- # --- the single write path ---------------------------------------------
def upsert(self, pattern: SolutionPattern) -> str: def upsert(self, pattern: SolutionPattern) -> str:

View File

@@ -81,6 +81,11 @@ class SolutionPattern:
# per-flavor rendering hints, kept OUT of the agnostic core (OQ4): # per-flavor rendering hints, kept OUT of the agnostic core (OQ4):
# {"claude": {...}, "codex": {...}, "grok": {...}} # {"claude": {...}, "codex": {...}, "grok": {...}}
rendering_hints: dict[str, dict[str, Any]] = field(default_factory=dict) rendering_hints: dict[str, dict[str, Any]] = field(default_factory=dict)
# other signal keys/loci this pattern's recommendation also applies to —
# lowercase substrings matched against a candidate signal's key+locus, so a
# detect signal that doesn't share this pattern's exact key (e.g. a
# recurring_error fingerprint) can still inherit the curated resolution.
covers: list[str] = field(default_factory=list)
status: str = "provisional" status: str = "provisional"
distribution_ready: bool = False distribution_ready: bool = False
created_at: Optional[str] = None created_at: Optional[str] = None

View File

@@ -14,7 +14,6 @@ from dataclasses import asdict, dataclass
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
from ..curate.schema import SolutionPattern
from ..detect.cluster import cluster from ..detect.cluster import cluster
from ..detect.quality import QualityConfig, filter_real from ..detect.quality import QualityConfig, filter_real
from ..detect.signals import extract_signals from ..detect.signals import extract_signals
@@ -48,10 +47,10 @@ class Suggestion:
pattern_key: str pattern_key: str
def _recommendation(pattern_key: str, catalog) -> Optional[str]: def _recommendation(pattern_key: str, locus: str, catalog) -> Optional[str]:
if catalog is None: if catalog is None:
return None return None
sp = catalog.load(SolutionPattern.make_id(pattern_key)) sp = catalog.find_for(pattern_key, locus)
if sp and sp.resolutions: if sp and sp.resolutions:
return sp.resolutions[0].summary return sp.resolutions[0].summary
return None return None
@@ -75,7 +74,7 @@ def weekly_retro(digests: list[dict], catalog=None, *, since: Optional[str] = No
for p in patterns: for p in patterns:
if p.polarity != "problem": if p.polarity != "problem":
continue # improvements come from problems continue # improvements come from problems
rec = (_recommendation(p.key, catalog) rec = (_recommendation(p.key, p.locus, catalog)
or f"Investigate {p.signal_type.replace('_', ' ')} on {p.locus}") or f"Investigate {p.signal_type.replace('_', ' ')} on {p.locus}")
priority = "high" if (p.cross_flavor or p.score >= _HIGH_SCORE) else "medium" priority = "high" if (p.cross_flavor or p.score >= _HIGH_SCORE) else "medium"
for repo in (p.repos or ["(unknown)"]): for repo in (p.repos or ["(unknown)"]):

View File

@@ -1,5 +1,5 @@
{ {
"generated_at": "2026-06-07T17:14:00Z", "generated_at": "2026-06-07T19:07:51Z",
"measure": { "measure": {
"error_rate": 0.957, "error_rate": 0.957,
"infra_overhead_share_median": 0.167, "infra_overhead_share_median": 0.167,
@@ -137,7 +137,7 @@
"cross_flavor": false, "cross_flavor": false,
"pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>",
"priority": "high", "priority": "high",
"recommendation": "Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "recommendation": "Read the file (or the region you'll touch) before Edit/Write",
"repo": "activity-core", "repo": "activity-core",
"score": 290.0, "score": 290.0,
"signal_type": "recurring_error", "signal_type": "recurring_error",
@@ -147,7 +147,7 @@
"cross_flavor": false, "cross_flavor": false,
"pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>",
"priority": "high", "priority": "high",
"recommendation": "Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "recommendation": "Read the file (or the region you'll touch) before Edit/Write",
"repo": "citation-evidence", "repo": "citation-evidence",
"score": 290.0, "score": 290.0,
"signal_type": "recurring_error", "signal_type": "recurring_error",
@@ -157,7 +157,7 @@
"cross_flavor": false, "cross_flavor": false,
"pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>",
"priority": "high", "priority": "high",
"recommendation": "Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "recommendation": "Read the file (or the region you'll touch) before Edit/Write",
"repo": "infospace-bench", "repo": "infospace-bench",
"score": 290.0, "score": 290.0,
"signal_type": "recurring_error", "signal_type": "recurring_error",
@@ -167,7 +167,7 @@
"cross_flavor": false, "cross_flavor": false,
"pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>",
"priority": "high", "priority": "high",
"recommendation": "Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "recommendation": "Read the file (or the region you'll touch) before Edit/Write",
"repo": "issue-facade", "repo": "issue-facade",
"score": 290.0, "score": 290.0,
"signal_type": "recurring_error", "signal_type": "recurring_error",
@@ -177,7 +177,7 @@
"cross_flavor": false, "cross_flavor": false,
"pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>",
"priority": "high", "priority": "high",
"recommendation": "Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "recommendation": "Read the file (or the region you'll touch) before Edit/Write",
"repo": "railiance-apps", "repo": "railiance-apps",
"score": 290.0, "score": 290.0,
"signal_type": "recurring_error", "signal_type": "recurring_error",
@@ -187,7 +187,7 @@
"cross_flavor": false, "cross_flavor": false,
"pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>",
"priority": "high", "priority": "high",
"recommendation": "Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "recommendation": "Read the file (or the region you'll touch) before Edit/Write",
"repo": "state-hub", "repo": "state-hub",
"score": 290.0, "score": 290.0,
"signal_type": "recurring_error", "signal_type": "recurring_error",
@@ -197,7 +197,7 @@
"cross_flavor": false, "cross_flavor": false,
"pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>",
"priority": "high", "priority": "high",
"recommendation": "Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "recommendation": "Read the file (or the region you'll touch) before Edit/Write",
"repo": "the-custodian", "repo": "the-custodian",
"score": 290.0, "score": 290.0,
"signal_type": "recurring_error", "signal_type": "recurring_error",
@@ -207,7 +207,7 @@
"cross_flavor": false, "cross_flavor": false,
"pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "pattern_key": "problem:recurring_error:<tool_use_error>file has not been read yet. read it first before writing to it.<<path>>",
"priority": "high", "priority": "high",
"recommendation": "Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>>", "recommendation": "Read the file (or the region you'll touch) before Edit/Write",
"repo": "vergabe-teilnahme", "repo": "vergabe-teilnahme",
"score": 290.0, "score": 290.0,
"signal_type": "recurring_error", "signal_type": "recurring_error",
@@ -217,7 +217,7 @@
"cross_flavor": false, "cross_flavor": false,
"pattern_key": "problem:recurring_error:<tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>>", "pattern_key": "problem:recurring_error:<tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>>",
"priority": "medium", "priority": "medium",
"recommendation": "Investigate recurring error on <tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>>", "recommendation": "Read the file (or the region you'll touch) before Edit/Write",
"repo": "artifact-store", "repo": "artifact-store",
"score": 78.0, "score": 78.0,
"signal_type": "recurring_error", "signal_type": "recurring_error",
@@ -227,7 +227,7 @@
"cross_flavor": false, "cross_flavor": false,
"pattern_key": "problem:recurring_error:<tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>>", "pattern_key": "problem:recurring_error:<tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>>",
"priority": "medium", "priority": "medium",
"recommendation": "Investigate recurring error on <tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>>", "recommendation": "Read the file (or the region you'll touch) before Edit/Write",
"repo": "issue-facade", "repo": "issue-facade",
"score": 78.0, "score": 78.0,
"signal_type": "recurring_error", "signal_type": "recurring_error",
@@ -237,7 +237,7 @@
"cross_flavor": false, "cross_flavor": false,
"pattern_key": "problem:recurring_error:<tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>>", "pattern_key": "problem:recurring_error:<tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>>",
"priority": "medium", "priority": "medium",
"recommendation": "Investigate recurring error on <tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>>", "recommendation": "Read the file (or the region you'll touch) before Edit/Write",
"repo": "railiance-apps", "repo": "railiance-apps",
"score": 78.0, "score": 78.0,
"signal_type": "recurring_error", "signal_type": "recurring_error",
@@ -247,7 +247,7 @@
"cross_flavor": false, "cross_flavor": false,
"pattern_key": "problem:recurring_error:<tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>>", "pattern_key": "problem:recurring_error:<tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>>",
"priority": "medium", "priority": "medium",
"recommendation": "Investigate recurring error on <tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>>", "recommendation": "Read the file (or the region you'll touch) before Edit/Write",
"repo": "state-hub", "repo": "state-hub",
"score": 78.0, "score": 78.0,
"signal_type": "recurring_error", "signal_type": "recurring_error",
@@ -316,7 +316,7 @@
], ],
"window": { "window": {
"days": 30, "days": 30,
"since": "2026-05-08T17:14:00Z", "since": "2026-05-08T19:07:51Z",
"until": "2026-06-07T17:14:00Z" "until": "2026-06-07T19:07:51Z"
} }
} }

View File

@@ -1,5 +1,5 @@
# Weekly Coding Retro (2026-05-08 → 2026-06-07) # Weekly Coding Retro (2026-05-08 → 2026-06-07)
_23 real sessions · generated 2026-06-07T17:14:00Z_ _23 real sessions · generated 2026-06-07T19:07:51Z_
## Top improvement suggestions (cross-flavor first, ≤3 per repo) ## Top improvement suggestions (cross-flavor first, ≤3 per repo)
- **net-kingdom** (high, score=54.0) [CROSS-FLAVOR]: cross-flavor problem: recurring error — Investigate recurring error on make: *** [makefile:<n>: fix-consistency] error <n> - **net-kingdom** (high, score=54.0) [CROSS-FLAVOR]: cross-flavor problem: recurring error — Investigate recurring error on make: *** [makefile:<n>: fix-consistency] error <n>
@@ -14,18 +14,18 @@ _23 real sessions · generated 2026-06-07T17:14:00Z_
- **flex-auth** (high, score=441.0): problem: schema thrash — Load the tool schemas you'll need once, up front - **flex-auth** (high, score=441.0): problem: schema thrash — Load the tool schemas you'll need once, up front
- **infospace-bench** (high, score=441.0): problem: schema thrash — Load the tool schemas you'll need once, up front - **infospace-bench** (high, score=441.0): problem: schema thrash — Load the tool schemas you'll need once, up front
- **ops-bridge** (high, score=441.0): problem: schema thrash — Load the tool schemas you'll need once, up front - **ops-bridge** (high, score=441.0): problem: schema thrash — Load the tool schemas you'll need once, up front
- **activity-core** (high, score=290.0): problem: recurring error — Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>> - **activity-core** (high, score=290.0): problem: recurring error — Read the file (or the region you'll touch) before Edit/Write
- **citation-evidence** (high, score=290.0): problem: recurring error — Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>> - **citation-evidence** (high, score=290.0): problem: recurring error — Read the file (or the region you'll touch) before Edit/Write
- **infospace-bench** (high, score=290.0): problem: recurring error — Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>> - **infospace-bench** (high, score=290.0): problem: recurring error — Read the file (or the region you'll touch) before Edit/Write
- **issue-facade** (high, score=290.0): problem: recurring error — Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>> - **issue-facade** (high, score=290.0): problem: recurring error — Read the file (or the region you'll touch) before Edit/Write
- **railiance-apps** (high, score=290.0): problem: recurring error — Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>> - **railiance-apps** (high, score=290.0): problem: recurring error — Read the file (or the region you'll touch) before Edit/Write
- **state-hub** (high, score=290.0): problem: recurring error — Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>> - **state-hub** (high, score=290.0): problem: recurring error — Read the file (or the region you'll touch) before Edit/Write
- **the-custodian** (high, score=290.0): problem: recurring error — Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>> - **the-custodian** (high, score=290.0): problem: recurring error — Read the file (or the region you'll touch) before Edit/Write
- **vergabe-teilnahme** (high, score=290.0): problem: recurring error — Investigate recurring error on <tool_use_error>file has not been read yet. read it first before writing to it.<<path>> - **vergabe-teilnahme** (high, score=290.0): problem: recurring error — Read the file (or the region you'll touch) before Edit/Write
- **artifact-store** (medium, score=78.0): problem: recurring error — Investigate recurring error on <tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>> - **artifact-store** (medium, score=78.0): problem: recurring error — Read the file (or the region you'll touch) before Edit/Write
- **issue-facade** (medium, score=78.0): problem: recurring error — Investigate recurring error on <tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>> - **issue-facade** (medium, score=78.0): problem: recurring error — Read the file (or the region you'll touch) before Edit/Write
- **railiance-apps** (medium, score=78.0): problem: recurring error — Investigate recurring error on <tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>> - **railiance-apps** (medium, score=78.0): problem: recurring error — Read the file (or the region you'll touch) before Edit/Write
- **state-hub** (medium, score=78.0): problem: recurring error — Investigate recurring error on <tool_use_error>file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<<path>> - **state-hub** (medium, score=78.0): problem: recurring error — Read the file (or the region you'll touch) before Edit/Write
- **artifact-store** (medium, score=50.55): problem: budget overrun — Read narrowly — target the region you need, not whole large files - **artifact-store** (medium, score=50.55): problem: budget overrun — Read narrowly — target the region you need, not whole large files
- **vergabe-teilnahme** (medium, score=12.0): problem: recurring error — Investigate recurring error on { - **vergabe-teilnahme** (medium, score=12.0): problem: recurring error — Investigate recurring error on {
- **ops-bridge** (medium, score=10.0): problem: recurring error — Investigate recurring error on found <n> errors (<n> fixed, <n> remaining). - **ops-bridge** (medium, score=10.0): problem: recurring error — Investigate recurring error on found <n> errors (<n> fixed, <n> remaining).

View File

@@ -0,0 +1,62 @@
"""find_for / covers tests (AGENTIC-WP-0010 follow-up)."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from session_memory.curate.catalog import Catalog # noqa: E402
from session_memory.curate.schema import ( # noqa: E402
Provenance,
Resolution,
SolutionPattern,
)
def _pattern(pid, src, covers=None, name="P"):
return SolutionPattern(
id=pid, name=name, version="1.0.0", polarity="problem", problem="p",
resolutions=[Resolution(summary="do x")],
provenance=Provenance(source_key=src), covers=covers or [])
def test_covers_round_trips(tmp_path):
cat = Catalog(str(tmp_path))
cat.upsert(_pattern("sp-a", "problem:file_not_read:edit",
covers=["file has not been read"]))
assert cat.load("sp-a").covers == ["file has not been read"]
def test_find_for_exact_key(tmp_path):
cat = Catalog(str(tmp_path))
cat.upsert(_pattern(SolutionPattern.make_id("problem:retry_storm:retries"),
"problem:retry_storm:retries"))
got = cat.find_for("problem:retry_storm:retries")
assert got is not None and got.id == "sp-problem-retry_storm-retries"
def test_find_for_covers_match(tmp_path):
cat = Catalog(str(tmp_path))
cat.upsert(_pattern("sp-rbe", "problem:file_not_read:edit",
covers=["file has not been read", "modified since read"]))
# a recurring_error signal with a different key but matching fingerprint locus
got = cat.find_for(
"problem:recurring_error:<tool_use_error>file has not been read yet...",
locus="<tool_use_error>file has not been read yet. read it first...")
assert got is not None and got.id == "sp-rbe"
def test_find_for_no_match_returns_none(tmp_path):
cat = Catalog(str(tmp_path))
cat.upsert(_pattern("sp-rbe", "problem:file_not_read:edit",
covers=["file has not been read"]))
assert cat.find_for("problem:recurring_error:some unrelated error") is None
def test_covers_change_versions(tmp_path):
cat = Catalog(str(tmp_path))
cat.upsert(_pattern("sp-a", "problem:x:y"))
p = cat.load("sp-a")
p.covers = ["new coverage"]
assert cat.upsert(p) == "versioned" # covers is substantive content
assert cat.load("sp-a").version == "1.0.1"

View File

@@ -55,6 +55,26 @@ def test_recommendation_from_catalog(tmp_path):
assert r["suggestions"][0]["recommendation"] == "Stop and diagnose before retrying" assert r["suggestions"][0]["recommendation"] == "Stop and diagnose before retrying"
def test_recurring_error_inherits_recommendation_via_covers(tmp_path):
cat = Catalog(str(tmp_path / "catalog"))
cat.upsert(SolutionPattern(
id="sp-rbe", name="Read before edit", version="1.0.0", polarity="problem",
problem="edit before read",
resolutions=[Resolution(summary="Read the file first before Edit/Write")],
covers=["file has not been read"]))
digs = []
for i in range(2):
d = _digest(f"claude:{i}", "r1", "2026-06-0{}T10:00:00Z".format(i + 1))
d["error_snippets"] = [{
"fingerprint": "<tool_use_error>file has not been read yet. read it first...",
"sample": "File has not been read yet", "count": 2, "tool": "Edit"}]
digs.append(d)
r = weekly_retro(digs, catalog=cat, since="2026-05-30T00:00:00Z", until="2026-06-08T00:00:00Z")
rec_err = [s for s in r["suggestions"] if s["signal_type"] == "recurring_error"]
assert rec_err, "expected a recurring_error suggestion"
assert rec_err[0]["recommendation"] == "Read the file first before Edit/Write"
def test_caps_three_per_repo(): def test_caps_three_per_repo():
# five distinct problem signals in one repo -> capped at 3 # five distinct problem signals in one repo -> capped at 3
digs = [] digs = []