feat(p17b): a context set that spans TWO bases, and a judge told which one [skip-docs]
``contexts/dekke-og-kontrakt-lindaas-2027`` is the first set whose approaches route at more than one knowledge base: a1/a2 at n200-2024 (material requirements) and a3/a4 at r761-2025 (the rig, and the falsification arm). That is the whole reason it exists -- P17b measures that ONE commission can be run across several. ``bundle.txt`` grows a block per base; a set naming one base is one block, so the four pre-P17b files parse byte-identically. The reader now has ONE home (``stress.read_bundle_declarations``): it used to be a private copy in the P14 gate and a second, looser one inside ``stress.main``, and the multi-base form is exactly the change that would have let them drift. Rule U becomes the UNION of every declared base, and that is not a formality. MEASURED 15.09: ``enhetspris`` is absent from n200-2024 and carried by 70 of r761-2025's 2 756 concepts, so anchors admitted per base would have admitted a question the pass as a whole CAN ground. It was dropped from the fifth set's anchors for that reason. ``score_context_set(bundle_id=...)`` restricts the judgement to the approaches routed at THIS base. Without it, judging the n200 outbox reports the r761 approach as ``not_evaluated``/``absent`` -- a false finding, because that approach WAS evaluated, against the other base, under the other run_id. That defect is pinned by its own arm. The judge's CLI refuses to guess when a set declares several bases, with an rc-0 control on ``--bundle``. Arm (d) gained a second half: every DECLARED base must be named by some approach, because a base no approach names is never run. The P19/B2 fasit denominator moved 26 -> 32 and is asserted, not dropped: six new references, two of them bare ``prosessnr`` (12.11, 12.12), so B1's punctuation-and-digits form is now exercised by a fasit and not only by a known-positive. Suite 1774/5, golden byte-unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5e4c497a84
commit
da0ccd0489
8 changed files with 496 additions and 57 deletions
|
|
@ -183,6 +183,41 @@ class ContextSetVerdict:
|
|||
return asdict(self)
|
||||
|
||||
|
||||
def read_bundle_declarations(path: str | Path) -> tuple[dict[str, str], ...]:
|
||||
"""Parse a context set's ``bundle.txt`` into ONE declaration per knowledge base.
|
||||
|
||||
``key: value`` lines, nothing else; each ``name:`` OPENS a block and each block must close
|
||||
with its own ``bundle_id:``. A set naming ONE base is one block, so every file written before
|
||||
P17b parses byte-identically — the multi-base form (P17b DEL 2) is an extension, not a new
|
||||
format.
|
||||
|
||||
**The ONE reader.** Before P17b the rule had two private copies — one in ``stress.main``'s
|
||||
argument parsing, one in the P14 gate's own test file — and the multi-base form is exactly the
|
||||
kind of change that would have let them drift into two answers about one set (kø-(p)). Both
|
||||
now call this.
|
||||
|
||||
:raises ValueError: a malformed line, or a block that declares no ``bundle_id``.
|
||||
"""
|
||||
blocks: list[dict[str, str]] = []
|
||||
for line in Path(path).read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if ": " not in line:
|
||||
raise ValueError(f"malformed bundle.txt line in {path}: {line!r}")
|
||||
key, value = (part.strip() for part in line.split(": ", 1))
|
||||
if key == "name" or not blocks:
|
||||
blocks.append({})
|
||||
blocks[-1][key] = value
|
||||
if not blocks:
|
||||
raise ValueError(f"{path} declares no knowledge base at all")
|
||||
for block in blocks:
|
||||
for required in ("name", "bundle_id"):
|
||||
if required not in block:
|
||||
raise ValueError(f"{path} declares no {required!r}")
|
||||
return tuple(blocks)
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8")) # type: ignore[no-any-return]
|
||||
|
||||
|
|
@ -233,9 +268,20 @@ def score_context_set(
|
|||
outbox_dir: str | Path,
|
||||
run_id: str,
|
||||
bundle_dir: str | Path,
|
||||
bundle_id: str | None = None,
|
||||
) -> ContextSetVerdict:
|
||||
"""Judge ONE context set against ONE outbox. ``bundle_dir`` is the MOUNTED base itself (the
|
||||
CLI resolves it from ``--bundle-root`` plus the set's own ``bundle.txt`` name)."""
|
||||
CLI resolves it from ``--bundle-root`` plus the set's own ``bundle.txt`` name).
|
||||
|
||||
``bundle_id`` RESTRICTS the judgement to the approaches a multi-base set routed at THIS base
|
||||
(P17b DEL 2). Without it, judging a two-base set's n200 outbox would report the r761 approach
|
||||
as ``not_evaluated`` with reason ``absent`` — a false finding, because that approach WAS
|
||||
evaluated, against the other base, under the other ``run_id``. ``None`` keeps every single-base
|
||||
set judged exactly as before, which is why this is a restriction rather than a new mode: the
|
||||
order offered a ``--multibase`` summary reader, and MEASURED against the shape the artefacts
|
||||
actually take, the per-base run already has its own full artefact set and its own run_id — so
|
||||
what the judge was missing was not a new file to read but the one thing the mandate already
|
||||
knows, namely which approaches belong here."""
|
||||
context = Path(context_dir)
|
||||
outbox = Path(outbox_dir)
|
||||
base = Path(bundle_dir)
|
||||
|
|
@ -256,6 +302,19 @@ def score_context_set(
|
|||
must_cite = {row["approach_id"]: row.get("concepts", []) for row in fasit.get("must_cite", [])}
|
||||
refuse_ids = {row["approach_id"] for row in fasit.get("must_refuse", [])}
|
||||
|
||||
# P17b: the approaches THIS base was asked about. Read off the mandate, never off a second
|
||||
# per-row key in the fasit — the routing already has exactly one home (kø-(p)).
|
||||
judged_approaches = mandate.approaches
|
||||
if bundle_id is not None:
|
||||
judged_approaches = tuple(
|
||||
a for a in judged_approaches if (a.bundle_id or bundle_id) == bundle_id
|
||||
)
|
||||
if not judged_approaches:
|
||||
raise EmptyMeasurement(
|
||||
f"no approach in {context} is routed at {bundle_id!r} - a judgement over zero "
|
||||
"rows has no denominator"
|
||||
)
|
||||
|
||||
# ---- run-level trace ---------------------------------------------------------------------
|
||||
debate = outbox / f"{run_id}-debate.json"
|
||||
tool_calls: list[dict[str, Any]] = (
|
||||
|
|
@ -303,7 +362,7 @@ def score_context_set(
|
|||
validated_codes: set[str] = set()
|
||||
validated_ids: set[str] = set()
|
||||
|
||||
for approach in mandate.approaches:
|
||||
for approach in judged_approaches:
|
||||
proposal_path, outcome_path = _artefacts(outbox, run_id, approach.id)
|
||||
concepts = must_cite.get(approach.id, [])
|
||||
wanted = {c["path"] for c in concepts}
|
||||
|
|
@ -422,9 +481,14 @@ def score_context_set(
|
|||
|
||||
# ---- the falsification arm ---------------------------------------------------------------
|
||||
refusals: list[RefusalVerdict] = []
|
||||
judged_ids = {a.id for a in judged_approaches}
|
||||
for row in fasit.get("must_refuse", []):
|
||||
rid = row["approach_id"]
|
||||
commissioned = next((a for a in mandate.approaches if a.id == rid), None)
|
||||
# P17b: a falsification arm routed at ANOTHER base was neither asked nor answered here,
|
||||
# and reporting it would put a pass/fail on a run that did not happen in this outbox.
|
||||
if rid not in judged_ids:
|
||||
continue
|
||||
commissioned = next((a for a in judged_approaches if a.id == rid), None)
|
||||
refuse_codes = set(commissioned.affected_codes) if commissioned is not None else set()
|
||||
leaked = sorted(refuse_codes & validated_codes)
|
||||
if rid in validated_ids:
|
||||
|
|
@ -446,7 +510,7 @@ def score_context_set(
|
|||
return ContextSetVerdict(
|
||||
context_set=context.name,
|
||||
run_id=run_id,
|
||||
bundle_id=str(fasit.get("bundle_id", "")),
|
||||
bundle_id=bundle_id if bundle_id is not None else str(fasit.get("bundle_id", "")),
|
||||
approaches=tuple(rows),
|
||||
must_refuse=tuple(refusals),
|
||||
hallucinated_reads=tuple(hallucinated_reads),
|
||||
|
|
@ -480,18 +544,47 @@ def main(argv: list[str] | None = None) -> int:
|
|||
default=os.environ.get("PORTFOLIO_VEGNORMAL_ROOT", _DEFAULT_BUNDLE_ROOT),
|
||||
help="directory the set's bundle.txt name is mounted under",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bundle",
|
||||
default=None,
|
||||
help="which of the set's declared bases this outbox is for (name or bundle_id). Required "
|
||||
"when the set declares more than one (P17b): each approach is judged against ITS OWN "
|
||||
"base, so a judge that guessed would score one base's run against another's fasit rows",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
context = Path(args.context_dir)
|
||||
declared = dict(
|
||||
line.split(":", 1) # type: ignore[misc]
|
||||
for line in (context / "bundle.txt").read_text(encoding="utf-8").splitlines()
|
||||
if ":" in line
|
||||
)
|
||||
base = Path(args.bundle_root).expanduser() / declared["name"].strip()
|
||||
declared = read_bundle_declarations(context / "bundle.txt")
|
||||
if args.bundle is None and len(declared) > 1:
|
||||
print(
|
||||
f"stress refused: {context} declares {len(declared)} knowledge bases "
|
||||
f"({', '.join(d['name'] for d in declared)}); name the one this outbox is for with "
|
||||
"--bundle, because a judge that picked would be scoring one base's run against "
|
||||
"another base's fasit rows",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
chosen = declared[0] if args.bundle is None else None
|
||||
for block in declared:
|
||||
if args.bundle in (block["name"], block["bundle_id"]):
|
||||
chosen = block
|
||||
if chosen is None:
|
||||
print(
|
||||
f"stress refused: {args.bundle!r} is not one of the knowledge bases {context} "
|
||||
f"declares ({', '.join(d['name'] for d in declared)})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
base = Path(args.bundle_root).expanduser() / chosen["name"]
|
||||
|
||||
try:
|
||||
verdict = score_context_set(context, args.outbox_dir, args.run_id, base)
|
||||
verdict = score_context_set(
|
||||
context,
|
||||
args.outbox_dir,
|
||||
args.run_id,
|
||||
base,
|
||||
bundle_id=chosen["bundle_id"] if len(declared) > 1 else None,
|
||||
)
|
||||
except EmptyMeasurement as exc:
|
||||
print(f"stress refused: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue