feat(okf): adjudication_for names the unknown state and a tool carries it

Co-Authored-By: Claude <claude-opus-5>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-02 20:36:54 +02:00
commit ccd65d3b01
3 changed files with 191 additions and 1 deletions

View file

@ -455,6 +455,60 @@ def trust_tier(entries: tuple[dict[str, str], ...] | None) -> TrustTier:
return "machine-confirmed"
class AdjudicationValueError(ValueError):
"""A concept declares an ``adjudication`` value outside the closed vocabulary.
A ``ValueError`` subclass (the ``IngestStampError`` precedent) so a malformed knowledge base
reaches the CLI's refusal tuple and hosting's 400 arm rather than the crash channel."""
#: Whether a concept has been ADJUDICATED, per the cross-repo contract. The value set on the wire
#: is CLOSED (``proposed`` | ``adjudicated``); the third token is this consumer's, and it is what
#: absence means. See ``docs/okf-konsum-kontrakter.md`` § 2, which is the source for this rule.
AdjudicationState = Literal["proposed", "adjudicated", "unknown"]
#: The closed on-the-wire vocabulary. ``unknown`` is deliberately NOT in it: it is what this reader
#: concludes from absence, never something a document may declare.
_ADJUDICATION_WIRE_VALUES = ("proposed", "adjudicated")
def adjudication_for(path: str | Path) -> AdjudicationState:
"""Read a concept's adjudication state, with absence as a FIRST-CLASS state.
A concept that does not carry the key is ``unknown`` *we did not learn whether this was
adjudicated*, which is what an older bundle looks like. **Collapsing that into ``absent``**
(*it was not adjudicated*) **is the same defect as collapsing ``unreadable`` into ``absent``**
one layer up, and the same defect removed from ``RunResult.verdict``.
A value outside the two named ones is **refused by name**, never mapped to ``unknown``:
validation, never repair. An EMPTY value is refused for the same reason present-but-empty is
a different fact from absent, and folding it in would recreate the collapse this function
exists to prevent.
**Read through ``parse_frontmatter``, NOT ``evidence_for`` and that is a MEASUREMENT, not a
preference.** The plan specified ``evidence_for(path, key="adjudication")``; the contract as
delivered makes ``adjudication`` a plain SCALAR, and ``evidence_for`` routes its value through
``decode_flow_value``. Measured 2026-09-02: BOTH valid values come back as an identical
``state='unreadable', reason='unsupported-flow'`` record, so that route cannot tell ``proposed``
from ``adjudicated`` at all. ``parse_frontmatter`` is the repo's scalar reader over the SAME
``_split_frontmatter`` scan, so this is a second READER of one parse never a second parser,
which is the rule that route was reaching for.
Gated by ``tests/test_falsification_verdict_loadbearing.py``."""
raw = parse_frontmatter(path).get("adjudication")
if raw is None:
return "unknown"
value = unquote_scalar(raw)
if value not in _ADJUDICATION_WIRE_VALUES:
raise AdjudicationValueError(
f"{value!r} is not an `adjudication` value — the vocabulary is closed to "
f"{' | '.join(_ADJUDICATION_WIRE_VALUES)}, and mapping an unrecognised one to "
"`unknown` would invent the very state that token exists to keep honest"
)
# ``value`` is one of the wire literals, which the Literal type also admits.
return value # type: ignore[return-value]
#: What a document says about ONE provenance key. Three states, and the third is the whole point:
#: collapsing ``unreadable`` into ``absent`` turns a verdict on missing evidence into evidence of
#: absence.

View file

@ -0,0 +1,57 @@
"""Library-level MAF tool carriers over the framework-neutral ``okf`` readers.
One named carrier per reader whose answer a hypothesiser needs to be able to ASK for. The gate on
this module is the carrier, not the vocabulary it carries: an accessor that returns three tokens
which nothing propagates is a vocabulary, not a seam.
**Import direction is one-way and load-bearing:** this module imports ``okf``, never the reverse.
``okf`` is MAF-free by invariant (``test_okf_is_maf_free``, D7 vendor-neutrality) and this module
is not, so a back-edge would drag ``agent_framework`` into the portable context layer.
**Not the MCP seam.** ``mcp_tools`` is the opt-in EXTERNAL seam with its own allowlist and
announcement rules; a local library primitive placed there would sit behind an egress contract it
has no business in. These carriers make no network call and announce nothing.
**Deliberately NOT wired into ``run_project``.** Wiring a carrier into the run surface would move
the byte-pinned demo transcript, which nothing asks for, and would make the state reachable only by
reading a run's output rather than by calling a function — the ``evidence_for`` rule, unchanged.
"""
from __future__ import annotations
from typing import Any
from agent_framework import FunctionTool, tool
from portfolio_optimiser import okf
from portfolio_optimiser.retrieval import safe_resolve
def adjudication_payload(bundle_dir: str, concept: str) -> dict[str, Any]:
"""The carrier's answer: the concept asked about, AND the state read for it.
Both halves are the point. A payload naming the concept without its state would leave
``adjudication_for`` perfectly correct and the hypothesiser none the wiser, which is exactly
the silent gap this seam closes.
Path-safe via ``safe_resolve`` (fail-closed), so a concept name escaping the bundle raises
rather than reading an arbitrary file."""
resolved = safe_resolve(bundle_dir, concept)
return {"concept": concept, "adjudication": okf.adjudication_for(resolved)}
def make_adjudication_tool(bundle_dir: str) -> FunctionTool:
"""The ONE named carrier exposing a concept's adjudication state to a hypothesiser."""
@tool(
name="concept_adjudication_state",
description=(
"Report whether a concept in the knowledge base has been adjudicated. Returns "
"`proposed`, `adjudicated`, or `unknown` when the concept does not say — `unknown` "
"means the state was not learned, never that adjudication did not happen."
),
)
def concept_adjudication_state(concept: str) -> dict[str, Any]:
return adjudication_payload(bundle_dir, concept)
return concept_adjudication_state