session-memory Phase 2: evidence-bar + bloat guard (T04)

gating.py: two-tier evidence bar (OQ5) — promote floor (frequency/sessions/
cost_impact) plus a stricter distribution-eligibility floor that sets a
promoted pattern to approved+distribution_ready vs provisional. Wired into
review() so thin approvals land provisional. bloat_warnings flags duplicate
and near-duplicate (same signal-type+locus) candidates (OQ6). [curate]/
[curate.gate] knobs in config.toml. 6 new tests; suite 64/64 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 00:28:34 +02:00
parent e51fd8154d
commit ab22d22bfb
5 changed files with 232 additions and 7 deletions

View File

@@ -22,6 +22,7 @@ from datetime import datetime, timezone
from typing import Callable, Optional
from .catalog import Catalog
from .gating import GateConfig, evaluate
from .schema import Provenance, Resolution, Scope, SolutionPattern
APPROVE = "approve"
@@ -46,8 +47,13 @@ def evidence_fingerprint(candidate: dict) -> str:
return hashlib.sha1(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
def candidate_to_pattern(candidate: dict) -> SolutionPattern:
"""Build a (provisional) Solution Pattern from a detect candidate."""
def candidate_to_pattern(candidate: dict, *, status: str = "provisional",
distribution_ready: bool = False) -> SolutionPattern:
"""Build a Solution Pattern from a detect candidate.
``status``/``distribution_ready`` come from the evidence gate (T04); they
default to a provisional, non-distribution-ready pattern when ungated.
"""
src = candidate["key"]
flavors = list(candidate.get("flavors", []))
hints = {f: {"target": _DEFAULT_TARGET.get(f, ""), "note": "TODO: refine rendering"}
@@ -62,7 +68,8 @@ def candidate_to_pattern(candidate: dict) -> SolutionPattern:
scope=Scope(flavors=flavors, repos=list(candidate.get("repos", []))),
provenance=Provenance(source_key=src, evidence=dict(candidate), promoted_at=_now()),
rendering_hints=hints,
status="provisional",
status=status,
distribution_ready=distribution_ready,
)
@@ -112,8 +119,14 @@ class ReviewResult:
def review(candidates: list[dict], decide: Decider, catalog: Catalog,
log: ReviewLog) -> ReviewResult:
"""Run each candidate through ``decide``; promote approvals into ``catalog``."""
log: ReviewLog, gate: Optional[GateConfig] = None) -> ReviewResult:
"""Run each candidate through ``decide``; promote approvals into ``catalog``.
When a ``gate`` (T04 evidence bar) is supplied, the promoted pattern's
``status``/``distribution_ready`` are set from the gate evaluation, so an
approved-but-thin candidate lands as ``provisional`` rather than
distribution-ready.
"""
result = ReviewResult()
for cand in candidates:
key = cand["key"]
@@ -125,7 +138,11 @@ def review(candidates: list[dict], decide: Decider, catalog: Catalog,
result.deferred.append(key)
continue # not a final decision — leave for a later pass
if action == APPROVE:
cat_action = catalog.upsert(candidate_to_pattern(cand))
g = evaluate(cand, gate) if gate is not None else None
pattern = (candidate_to_pattern(cand, status=g.status,
distribution_ready=g.distribution_ready)
if g is not None else candidate_to_pattern(cand))
cat_action = catalog.upsert(pattern)
result.approved.append((key, cat_action))
elif action == REJECT:
result.rejected.append(key)