From e237dcc62298bb5a304ed6b4bf0f15c556aaab50 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 7 Jun 2026 21:09:44 +0200 Subject: [PATCH] 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 --- ...p-problem-file_not_read-edit.history.jsonl | 1 + .../sp-problem-file_not_read-edit.json | 9 ++- session_memory/curate/catalog.py | 18 +++++- session_memory/curate/schema.py | 5 ++ session_memory/retro/build.py | 7 +-- session_memory/retro/last_retro.json | 30 ++++----- session_memory/retro/last_retro.md | 26 ++++---- tests/test_catalog_covers.py | 62 +++++++++++++++++++ tests/test_retro_build.py | 20 ++++++ 9 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 session_memory/catalog/sp-problem-file_not_read-edit.history.jsonl create mode 100644 tests/test_catalog_covers.py diff --git a/session_memory/catalog/sp-problem-file_not_read-edit.history.jsonl b/session_memory/catalog/sp-problem-file_not_read-edit.history.jsonl new file mode 100644 index 0000000..dcd9bfc --- /dev/null +++ b/session_memory/catalog/sp-problem-file_not_read-edit.history.jsonl @@ -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"} diff --git a/session_memory/catalog/sp-problem-file_not_read-edit.json b/session_memory/catalog/sp-problem-file_not_read-edit.json index de05108..d5cf66c 100644 --- a/session_memory/catalog/sp-problem-file_not_read-edit.json +++ b/session_memory/catalog/sp-problem-file_not_read-edit.json @@ -1,4 +1,9 @@ { + "covers": [ + "file has not been read", + "modified since read", + "file_not_read" + ], "created_at": "2026-06-07T13:26:25Z", "distribution_ready": true, "id": "sp-problem-file_not_read-edit", @@ -53,6 +58,6 @@ "repos": [] }, "status": "approved", - "updated_at": "2026-06-07T13:26:25Z", - "version": "1.0.0" + "updated_at": "2026-06-07T19:06:45Z", + "version": "1.0.1" } diff --git a/session_memory/curate/catalog.py b/session_memory/curate/catalog.py index 6d3a913..7467448 100644 --- a/session_memory/curate/catalog.py +++ b/session_memory/curate/catalog.py @@ -30,7 +30,7 @@ from .schema import SolutionPattern # Content fields that define a pattern's substance. Version, timestamps, status, # and distribution_ready are metadata — changes to them never bump the version. _CONTENT_KEYS = ("name", "polarity", "problem", "resolutions", "scope", - "provenance", "rendering_hints") + "provenance", "rendering_hints", "covers") ADDED = "added" UNCHANGED = "unchanged" @@ -86,6 +86,22 @@ class Catalog: with open(path, encoding="utf-8") as fh: 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 --------------------------------------------- def upsert(self, pattern: SolutionPattern) -> str: diff --git a/session_memory/curate/schema.py b/session_memory/curate/schema.py index de43179..ee5e732 100644 --- a/session_memory/curate/schema.py +++ b/session_memory/curate/schema.py @@ -81,6 +81,11 @@ class SolutionPattern: # per-flavor rendering hints, kept OUT of the agnostic core (OQ4): # {"claude": {...}, "codex": {...}, "grok": {...}} 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" distribution_ready: bool = False created_at: Optional[str] = None diff --git a/session_memory/retro/build.py b/session_memory/retro/build.py index 5cce9a0..dca5ade 100644 --- a/session_memory/retro/build.py +++ b/session_memory/retro/build.py @@ -14,7 +14,6 @@ from dataclasses import asdict, dataclass from datetime import datetime, timedelta, timezone from typing import Optional -from ..curate.schema import SolutionPattern from ..detect.cluster import cluster from ..detect.quality import QualityConfig, filter_real from ..detect.signals import extract_signals @@ -48,10 +47,10 @@ class Suggestion: 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: return None - sp = catalog.load(SolutionPattern.make_id(pattern_key)) + sp = catalog.find_for(pattern_key, locus) if sp and sp.resolutions: return sp.resolutions[0].summary return None @@ -75,7 +74,7 @@ def weekly_retro(digests: list[dict], catalog=None, *, since: Optional[str] = No for p in patterns: if p.polarity != "problem": 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}") priority = "high" if (p.cross_flavor or p.score >= _HIGH_SCORE) else "medium" for repo in (p.repos or ["(unknown)"]): diff --git a/session_memory/retro/last_retro.json b/session_memory/retro/last_retro.json index 317c9ca..7ee1ba2 100644 --- a/session_memory/retro/last_retro.json +++ b/session_memory/retro/last_retro.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-06-07T17:14:00Z", + "generated_at": "2026-06-07T19:07:51Z", "measure": { "error_rate": 0.957, "infra_overhead_share_median": 0.167, @@ -137,7 +137,7 @@ "cross_flavor": false, "pattern_key": "problem:recurring_error:file has not been read yet. read it first before writing to it.<>", "priority": "high", - "recommendation": "Investigate recurring error on file has not been read yet. read it first before writing to it.<>", + "recommendation": "Read the file (or the region you'll touch) before Edit/Write", "repo": "activity-core", "score": 290.0, "signal_type": "recurring_error", @@ -147,7 +147,7 @@ "cross_flavor": false, "pattern_key": "problem:recurring_error:file has not been read yet. read it first before writing to it.<>", "priority": "high", - "recommendation": "Investigate recurring error on file has not been read yet. read it first before writing to it.<>", + "recommendation": "Read the file (or the region you'll touch) before Edit/Write", "repo": "citation-evidence", "score": 290.0, "signal_type": "recurring_error", @@ -157,7 +157,7 @@ "cross_flavor": false, "pattern_key": "problem:recurring_error:file has not been read yet. read it first before writing to it.<>", "priority": "high", - "recommendation": "Investigate recurring error on file has not been read yet. read it first before writing to it.<>", + "recommendation": "Read the file (or the region you'll touch) before Edit/Write", "repo": "infospace-bench", "score": 290.0, "signal_type": "recurring_error", @@ -167,7 +167,7 @@ "cross_flavor": false, "pattern_key": "problem:recurring_error:file has not been read yet. read it first before writing to it.<>", "priority": "high", - "recommendation": "Investigate recurring error on file has not been read yet. read it first before writing to it.<>", + "recommendation": "Read the file (or the region you'll touch) before Edit/Write", "repo": "issue-facade", "score": 290.0, "signal_type": "recurring_error", @@ -177,7 +177,7 @@ "cross_flavor": false, "pattern_key": "problem:recurring_error:file has not been read yet. read it first before writing to it.<>", "priority": "high", - "recommendation": "Investigate recurring error on file has not been read yet. read it first before writing to it.<>", + "recommendation": "Read the file (or the region you'll touch) before Edit/Write", "repo": "railiance-apps", "score": 290.0, "signal_type": "recurring_error", @@ -187,7 +187,7 @@ "cross_flavor": false, "pattern_key": "problem:recurring_error:file has not been read yet. read it first before writing to it.<>", "priority": "high", - "recommendation": "Investigate recurring error on file has not been read yet. read it first before writing to it.<>", + "recommendation": "Read the file (or the region you'll touch) before Edit/Write", "repo": "state-hub", "score": 290.0, "signal_type": "recurring_error", @@ -197,7 +197,7 @@ "cross_flavor": false, "pattern_key": "problem:recurring_error:file has not been read yet. read it first before writing to it.<>", "priority": "high", - "recommendation": "Investigate recurring error on file has not been read yet. read it first before writing to it.<>", + "recommendation": "Read the file (or the region you'll touch) before Edit/Write", "repo": "the-custodian", "score": 290.0, "signal_type": "recurring_error", @@ -207,7 +207,7 @@ "cross_flavor": false, "pattern_key": "problem:recurring_error:file has not been read yet. read it first before writing to it.<>", "priority": "high", - "recommendation": "Investigate recurring error on file has not been read yet. read it first before writing to it.<>", + "recommendation": "Read the file (or the region you'll touch) before Edit/Write", "repo": "vergabe-teilnahme", "score": 290.0, "signal_type": "recurring_error", @@ -217,7 +217,7 @@ "cross_flavor": false, "pattern_key": "problem:recurring_error:file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<>", "priority": "medium", - "recommendation": "Investigate recurring error on file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<>", + "recommendation": "Read the file (or the region you'll touch) before Edit/Write", "repo": "artifact-store", "score": 78.0, "signal_type": "recurring_error", @@ -227,7 +227,7 @@ "cross_flavor": false, "pattern_key": "problem:recurring_error:file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<>", "priority": "medium", - "recommendation": "Investigate recurring error on file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<>", + "recommendation": "Read the file (or the region you'll touch) before Edit/Write", "repo": "issue-facade", "score": 78.0, "signal_type": "recurring_error", @@ -237,7 +237,7 @@ "cross_flavor": false, "pattern_key": "problem:recurring_error:file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<>", "priority": "medium", - "recommendation": "Investigate recurring error on file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<>", + "recommendation": "Read the file (or the region you'll touch) before Edit/Write", "repo": "railiance-apps", "score": 78.0, "signal_type": "recurring_error", @@ -247,7 +247,7 @@ "cross_flavor": false, "pattern_key": "problem:recurring_error:file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<>", "priority": "medium", - "recommendation": "Investigate recurring error on file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<>", + "recommendation": "Read the file (or the region you'll touch) before Edit/Write", "repo": "state-hub", "score": 78.0, "signal_type": "recurring_error", @@ -316,7 +316,7 @@ ], "window": { "days": 30, - "since": "2026-05-08T17:14:00Z", - "until": "2026-06-07T17:14:00Z" + "since": "2026-05-08T19:07:51Z", + "until": "2026-06-07T19:07:51Z" } } diff --git a/session_memory/retro/last_retro.md b/session_memory/retro/last_retro.md index 539955b..4e6388e 100644 --- a/session_memory/retro/last_retro.md +++ b/session_memory/retro/last_retro.md @@ -1,5 +1,5 @@ # 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) - **net-kingdom** (high, score=54.0) [CROSS-FLAVOR]: cross-flavor problem: recurring error — Investigate recurring error on make: *** [makefile:: fix-consistency] error @@ -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 - **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 -- **activity-core** (high, score=290.0): problem: recurring error — Investigate recurring error on file has not been read yet. read it first before writing to it.<> -- **citation-evidence** (high, score=290.0): problem: recurring error — Investigate recurring error on file has not been read yet. read it first before writing to it.<> -- **infospace-bench** (high, score=290.0): problem: recurring error — Investigate recurring error on file has not been read yet. read it first before writing to it.<> -- **issue-facade** (high, score=290.0): problem: recurring error — Investigate recurring error on file has not been read yet. read it first before writing to it.<> -- **railiance-apps** (high, score=290.0): problem: recurring error — Investigate recurring error on file has not been read yet. read it first before writing to it.<> -- **state-hub** (high, score=290.0): problem: recurring error — Investigate recurring error on file has not been read yet. read it first before writing to it.<> -- **the-custodian** (high, score=290.0): problem: recurring error — Investigate recurring error on file has not been read yet. read it first before writing to it.<> -- **vergabe-teilnahme** (high, score=290.0): problem: recurring error — Investigate recurring error on file has not been read yet. read it first before writing to it.<> -- **artifact-store** (medium, score=78.0): problem: recurring error — Investigate recurring error on file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<> -- **issue-facade** (medium, score=78.0): problem: recurring error — Investigate recurring error on file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<> -- **railiance-apps** (medium, score=78.0): problem: recurring error — Investigate recurring error on file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<> -- **state-hub** (medium, score=78.0): problem: recurring error — Investigate recurring error on file has been modified since read, either by the user or by a linter. read it again before attempting to write it.<> +- **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 — Read the file (or the region you'll touch) before Edit/Write +- **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 — Read the file (or the region you'll touch) before Edit/Write +- **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 — Read the file (or the region you'll touch) before Edit/Write +- **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 — Read the file (or the region you'll touch) before Edit/Write +- **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 — Read the file (or the region you'll touch) before Edit/Write +- **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 — 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 - **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 errors ( fixed, remaining). diff --git a/tests/test_catalog_covers.py b/tests/test_catalog_covers.py new file mode 100644 index 0000000..ca5a851 --- /dev/null +++ b/tests/test_catalog_covers.py @@ -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:file has not been read yet...", + locus="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" diff --git a/tests/test_retro_build.py b/tests/test_retro_build.py index ec36baa..c1e73f2 100644 --- a/tests/test_retro_build.py +++ b/tests/test_retro_build.py @@ -55,6 +55,26 @@ def test_recommendation_from_catalog(tmp_path): 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": "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(): # five distinct problem signals in one repo -> capped at 3 digs = []