feat(fase2a): herd load_verdicts_from_dir — vokabular-SKIP + caps (S2.5)
This commit is contained in:
parent
a706184bdd
commit
0a227a103b
2 changed files with 126 additions and 7 deletions
|
|
@ -20,6 +20,7 @@ from __future__ import annotations
|
|||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
|
@ -29,6 +30,8 @@ from agent_framework import ContextProvider, SessionContext
|
|||
|
||||
from portfolio_optimiser import okf
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
# Weights: the affected cost-code overlap dominates, then measure type, then magnitude.
|
||||
_W_CODES, _W_MEASURE, _W_MAGNITUDE = 0.60, 0.25, 0.15
|
||||
_MAGNITUDE_BUCKETS = [(0.0, 1e5), (1e5, 5e5), (5e5, 1e6), (1e6, float("inf"))]
|
||||
|
|
@ -112,6 +115,10 @@ def capture_verdict(features: ProposalFeatures, decision: str, rationale: str) -
|
|||
# band, and a separate later run pick them up (målbilde §3 long loop).
|
||||
|
||||
_REQUIRED_VERDICT_KEYS = {"id", "decision", "rationale", "proposal_features"}
|
||||
# The binary run-path decision vocabulary (Verdict.decision domain + FeedbackContract). An inbox file
|
||||
# with any other decision is SKIPPED by load_verdicts_from_dir — ``approved_with_adjustment`` is
|
||||
# deliberately EXCLUDED here (it lives only in bundle-seed frontmatter + the promotion gate).
|
||||
_INBOX_DECISION_VOCABULARY = frozenset({"approved", "rejected"})
|
||||
|
||||
|
||||
def verdict_to_dict(verdict: Verdict) -> dict[str, Any]:
|
||||
|
|
@ -171,24 +178,62 @@ def write_verdict(directory: str, verdict: Verdict) -> Path:
|
|||
return target
|
||||
|
||||
|
||||
def load_verdicts_from_dir(directory: str) -> list[Verdict]:
|
||||
def load_verdicts_from_dir(
|
||||
directory: str,
|
||||
*,
|
||||
max_rationale_len: int | None = None,
|
||||
max_files: int | None = None,
|
||||
) -> list[Verdict]:
|
||||
"""Load every well-formed verdict JSON file from an async inbox folder, TOLERANTLY (OKF §4
|
||||
spirit): a missing folder yields ``[]``; files that are not ``.json``, fail to parse, or lack a
|
||||
required key are SKIPPED, never raised. The folder is written out of band by an external party,
|
||||
so half-written or foreign files (e.g. a bundle's ``golden.json``) are realistic — this is the
|
||||
raw layer, not required input (contrast ``okf.load_ir_projection``'s fail-fast). Order is
|
||||
deterministic (sorted by filename)."""
|
||||
spirit): a missing folder yields ``[]``; files that are not ``.json``, fail to parse, lack a
|
||||
required key, carry a decision outside the binary vocabulary ``{approved, rejected}``, or exceed
|
||||
``max_rationale_len`` are SKIPPED, never raised. The folder is written out of band by an external
|
||||
party, so half-written or foreign files (e.g. a bundle's ``golden.json``) are realistic — this is
|
||||
the raw layer, not required input (contrast ``okf.load_ir_projection``'s fail-fast). Order is
|
||||
deterministic (sorted by filename).
|
||||
|
||||
Herding (S2.5), two distinct axes by design:
|
||||
- ``max_rationale_len`` (per-file, TOLERANT): an over-long rationale is skipped + logged, never
|
||||
raised — matching the per-file skip contract the Step-7 loop relies on.
|
||||
- ``max_files`` (aggregate, FAIL-FAST): more files than the cap RAISES ``ValueError`` before any
|
||||
parsing — an oversized inbox is an aggregate integrity signal, not a per-file anomaly.
|
||||
Both default to ``None`` (no cap), so existing callers are byte-for-byte unaffected."""
|
||||
path = Path(directory)
|
||||
if not path.is_dir():
|
||||
return []
|
||||
files = sorted(path.glob("*.json"))
|
||||
if max_files is not None and len(files) > max_files:
|
||||
raise ValueError(
|
||||
f"verdict inbox {directory!r} has {len(files)} files, over the max_files cap "
|
||||
f"({max_files}) — an oversized inbox is a fail-fast aggregate guard (contrast the "
|
||||
"per-file tolerant skips)"
|
||||
)
|
||||
verdicts: list[Verdict] = []
|
||||
for file in sorted(path.glob("*.json")):
|
||||
for file in files:
|
||||
try:
|
||||
data = json.loads(file.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
continue
|
||||
if not isinstance(data, dict) or not _REQUIRED_VERDICT_KEYS <= data.keys():
|
||||
continue
|
||||
if data.get("decision") not in _INBOX_DECISION_VOCABULARY:
|
||||
_log.debug(
|
||||
"skipping inbox verdict %s: decision %r outside vocabulary",
|
||||
file.name,
|
||||
data.get("decision"),
|
||||
)
|
||||
continue
|
||||
if (
|
||||
max_rationale_len is not None
|
||||
and len(str(data.get("rationale", ""))) > max_rationale_len
|
||||
):
|
||||
_log.warning(
|
||||
"skipping inbox verdict %s: rationale length %d exceeds max_rationale_len %d",
|
||||
file.name,
|
||||
len(str(data.get("rationale", ""))),
|
||||
max_rationale_len,
|
||||
)
|
||||
continue
|
||||
try:
|
||||
verdicts.append(verdict_from_dict(data))
|
||||
except (KeyError, TypeError):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue