feat(inbox): surface adjudication state and its dwell time

This commit is contained in:
Kjell Tore Guttormsen 2026-09-02 14:49:45 +02:00
commit a60312a5f3
9 changed files with 219 additions and 9 deletions

View file

@ -3,5 +3,5 @@ okf_version: 0.2
bundle_id: b-golden-segmented-okf-v0-2
---
- [Veiledning for eksempelbundel](veiledning.md)
- [Veiledning for eksempelbundel](veiledning.md) — adjudication: proposed
- [krav (index)](krav/index.md)

View file

@ -5,6 +5,7 @@ source_file: veiledning.md
source_sha256: 6906ec0acbcfc246e825bda9863c716eb5611b465020e8204eeb448c32343f7d
ingested_at: 2026-08-30T09:00:00Z
generated: true
adjudication: proposed
bundle_id: b-golden-segmented-okf-v0-2
parent: s0
segment_id: s1

View file

@ -1 +1 @@
- [Foerste krav](foerste-krav.md) — parent: s0?
- [Foerste krav](foerste-krav.md) — parent: s0?; adjudication: proposed

View file

@ -5,6 +5,7 @@ source_file: veiledning.md
source_sha256: 6906ec0acbcfc246e825bda9863c716eb5611b465020e8204eeb448c32343f7d
ingested_at: 2026-08-30T09:00:00Z
generated: true
adjudication: proposed
bundle_id: b-golden-segmented-okf-v0-2
parent: s0
segment_id: s2

View file

@ -1 +1 @@
- [Andre krav](andre-krav.md) — parent: s0?
- [Andre krav](andre-krav.md) — parent: s0?; adjudication: proposed

View file

@ -5,6 +5,7 @@ source_file: veiledning.md
source_sha256: 6906ec0acbcfc246e825bda9863c716eb5611b465020e8204eeb448c32343f7d
ingested_at: 2026-08-30T09:00:00Z
generated: true
adjudication: proposed
bundle_id: b-golden-segmented-okf-v0-2
segment_id: s0
source_offset: [0, 94]

View file

@ -194,6 +194,19 @@ def render_inbox_concept(
frontmatter[policy.offset_key] = _render_flow_list([str(offset) for offset in segment.span])
if segment.parent_id is not None:
frontmatter["parent"] = segment.parent_id
if policy.adjudication_key is not None:
# The per-entry verdict IS the discriminator. A plan-level
# `adjudicated: true` with no record for THIS entry leaves it
# `proposed`: B2 forbids the ratified state without the three keys
# beside it, so a flag alone cannot promote a segment.
verdict = segment.adjudication
if verdict is None:
frontmatter[policy.adjudication_key] = ADJUDICATION_PROPOSED
else:
frontmatter[policy.adjudication_key] = ADJUDICATION_ADJUDICATED
frontmatter["adjudicated_by"] = verdict.adjudicated_by
frontmatter["adjudicated_at"] = verdict.adjudicated_at
frontmatter["adjudication_dwell_s"] = str(verdict.adjudication_dwell_s)
return f"---\n{profile.frontmatter.emit(frontmatter)}\n---\n\n{_normalize_body(text)}"
@ -847,6 +860,23 @@ def process_inbox(
)
#: The adjudication states a segment concept may carry, and the whole set.
#: CLOSED on purpose (PM decision B2, `docs/plan/office-intake.md` § 5): a
#: value outside it is an error, not an extension point. The consumer's half of
#: the same contract is that ABSENCE of the key means `unknown` -- an older
#: bundle -- never a collapse to `absent`, so a producer emitting the key
#: inconsistently would make that distinction unmeasurable on their side.
ADJUDICATION_PROPOSED = "proposed"
ADJUDICATION_ADJUDICATED = "adjudicated"
ADJUDICATION_STATES = (ADJUDICATION_PROPOSED, ADJUDICATION_ADJUDICATED)
#: Written beside the state when, and only when, it is `adjudicated`. The dwell
#: time travels WITH the verdict: a ratified flag carrying no per-item time is
#: unfalsifiable, and it is the same number that makes adjudication throughput
#: measurable at all.
ADJUDICATION_COMPANION_KEYS = ("adjudicated_by", "adjudicated_at", "adjudication_dwell_s")
def _validate_facets(structure: DocumentStructure, profile: BundleProfile) -> None:
"""Refuse a document whose values cannot be rendered as index facets.
@ -859,8 +889,22 @@ def _validate_facets(structure: DocumentStructure, profile: BundleProfile) -> No
markers are this library's own tokens.
"""
assert profile.index.facets is not None
values = structure_frontmatter(structure, profile.index.facets.keys)
policy = profile.segmentation
key = None if policy is None else policy.adjudication_key
# Refused HERE rather than at reprojection, for the reason above: the only
# adjudication value that can be wrong is one the producer declared in the
# dropped document itself, and refusing it per file keeps a single bad
# document from failing the index for every other one.
if key is not None and key in values and values[key] not in ADJUDICATION_STATES:
raise MaterializationError(
f"frontmatter key {key!r} carries {values[key]!r}, which is outside the closed "
f"set {ADJUDICATION_STATES} — the adjudication state is a contract, not an "
"extension point",
code="index_facet_invalid",
)
try:
profile.index.facets.render(structure_frontmatter(structure, profile.index.facets.keys))
profile.index.facets.render(values)
except ValueError as exc:
raise MaterializationError(str(exc), code="index_facet_invalid") from exc

View file

@ -1233,6 +1233,12 @@ SEGMENTED_V1 = BundleProfile(
)
# Bound so the sixth profile's facet extension is typed: `IndexPolicy.facets`
# is `FacetPolicy | None`, and `SEGMENTED_V1` is known here to carry one.
_SEGMENTED_FACETS = SEGMENTED_V1.index.facets
assert _SEGMENTED_FACETS is not None
# The sixth profile. A segmented bundle could not declare which upstream spec
# it targets: `SEGMENTED_V1` names `bundle_id`, `OKF_V0_2` names `okf_version`,
# and the two never intersected. Additive, as upstream support always is here --
@ -1255,7 +1261,21 @@ SEGMENTED_OKF_V0_2 = BundleProfile(
types=OKF_V0_2.types,
frontmatter=OKF_V0_2.frontmatter,
paths=SEGMENTED_V1.paths,
index=replace(SEGMENTED_V1.index, root_frontmatter=("okf_version", "bundle_id")),
# The facet tuple is EXTENDED here rather than shared, and that is the
# discriminator doing its job: `SEGMENTED_V1.index.facets` is one object
# both profiles would otherwise point at, so appending `adjudication` to it
# would surface the state under the older profile too and move a
# byte-pinned golden. `FacetPolicy.render` refuses any key a policy does
# not name, which is why the key has to live here and cannot be added by
# the door at write time.
index=replace(
SEGMENTED_V1.index,
root_frontmatter=("okf_version", "bundle_id"),
facets=replace(
_SEGMENTED_FACETS,
keys=_SEGMENTED_FACETS.keys + ("adjudication",),
),
),
ownership=OKF_V0_2.ownership,
# Constructed rather than `replace`d off `SEGMENTED_V1.segmentation`: that
# attribute is typed `| None`, and the equality is asserted in the suite so

View file

@ -26,7 +26,12 @@ from pathlib import Path
from typing import Any
from llm_ingestion_okf.inbox import GateDecision, process_inbox
from llm_ingestion_okf.profiles import DEFAULT, SEGMENTED_V1, STRUCTURED_V1
from llm_ingestion_okf.profiles import (
DEFAULT,
SEGMENTED_OKF_V0_2,
SEGMENTED_V1,
STRUCTURED_V1,
)
from llm_ingestion_okf.extract import extract_text
from llm_ingestion_okf.segmentation import (
SegmentationPlan,
@ -63,14 +68,26 @@ def _extracted_text_sha256(source_bytes: bytes, filename: str = "n500.md") -> st
return hashlib.sha256(extract_text(filename, source_bytes).encode("utf-8")).hexdigest()
def build_plan(source_bytes: bytes, paths: tuple[str, ...] = PATHS, **overrides: Any):
def build_plan(
source_bytes: bytes,
paths: tuple[str, ...] = PATHS,
*,
entries_override: dict[str, dict[str, Any]] | None = None,
text: str | None = None,
**overrides: Any,
):
verdicts = entries_override or {}
payload: dict[str, Any] = {
"version": "1",
"source_sha256": hashlib.sha256(source_bytes).hexdigest(),
# The hash of the CANONICAL EXTRACTED text, which is what the spans
# index. Equal to the source hash on a `.md` passthrough and computed
# rather than copied, so the fixture keeps saying which one it means.
"text_sha256": _extracted_text_sha256(source_bytes),
"text_sha256": (
hashlib.sha256(text.encode("utf-8")).hexdigest()
if text is not None
else _extracted_text_sha256(source_bytes)
),
"extractor_id": "md",
"extractor_version": observed_extractor_version("md"),
"adjudicated_at": "2026-08-30T08:00:00Z",
@ -82,6 +99,7 @@ def build_plan(source_bytes: bytes, paths: tuple[str, ...] = PATHS, **overrides:
"okf_type": "requirement",
"span": [index * 10, index * 10 + 10],
"ingested_at": PLAN_AT,
**({"adjudication": verdicts[path]} if path in verdicts else {}),
}
for index, path in enumerate(paths)
],
@ -105,7 +123,13 @@ def run(
okf_type="requirement",
gate=gate,
profile=profile,
root_frontmatter_values={"bundle_id": "b-1"} if profile is SEGMENTED_V1 else None,
root_frontmatter_values=(
{"bundle_id": "b-1"}
if profile is SEGMENTED_V1
else {"bundle_id": "b-1", "okf_version": "0.2"}
if profile is SEGMENTED_OKF_V0_2
else None
),
segmentation=plan,
)
@ -270,3 +294,122 @@ def test_default_and_structured_write_one_root_index_only(tmp_path: Path) -> Non
assert [path.relative_to(bundle).as_posix() for path in bundle.rglob("index.md")] == [
"index.md"
]
# --- B2: the adjudication state, under the profile that asks for it --------
#
# The wire form is a CONTRACT with `portfolio-optimiser` (`docs/plan/
# office-intake.md` § 5), not a naming choice this repo may revise: key
# `adjudication`, CLOSED value set `proposed` | `adjudicated`, and when the
# value is `adjudicated` three further keys -- `adjudicated_by`,
# `adjudicated_at` (ISO 8601) and `adjudication_dwell_s` (an INTEGER number of
# seconds). The consumer's half is that ABSENCE means the state `unknown` (an
# older bundle), never a collapse to `absent`, so a producer emitting the key
# inconsistently would make that distinction unmeasurable on their side.
#
# The discriminator is `SegmentationPolicy.adjudication_key`, never
# `profile.segmentation is not None`: BOTH segmented profiles satisfy the
# latter, so keying on it would write the state into `SEGMENTED_V1` too and
# move a byte-pinned golden.
VERDICT = {
"adjudicated_by": "ktg",
"adjudicated_at": "2026-09-02T10:00:00Z",
"adjudication_dwell_s": 41,
}
def frontmatter_of(path: Path) -> dict[str, str]:
head = path.read_text(encoding="utf-8").split("---\n")[1]
return dict(
line.split(": ", 1) for line in head.splitlines() if ": " in line and line[:1] != " "
)
def build_v0_2(tmp: Path, *, adjudicated: tuple[str, ...] = (), bundle_name: str = "bundle"):
source = drop(tmp / "round", "n500.md")
plan = build_plan(
source.read_bytes(),
entries_override={path: dict(VERDICT) for path in adjudicated},
)
return run(tmp, plan=plan, profile=SEGMENTED_OKF_V0_2, bundle_name=bundle_name)
def test_an_unratified_segment_carries_the_proposed_state(tmp_path: Path) -> None:
build_v0_2(tmp_path)
values = frontmatter_of(tmp_path / "bundle" / "krav" / "forord.md")
assert values["adjudication"] == "proposed"
assert "adjudicated_by" not in values
assert "adjudication_dwell_s" not in values
def test_a_ratified_segment_carries_its_adjudicator_time_and_dwell(tmp_path: Path) -> None:
build_v0_2(tmp_path, adjudicated=("krav/forord.md",))
values = frontmatter_of(tmp_path / "bundle" / "krav" / "forord.md")
assert values["adjudication"] == "adjudicated"
assert values["adjudicated_by"] == "ktg"
assert values["adjudicated_at"] == "2026-09-02T10:00:00Z"
# An INTEGER number of seconds, per B2 -- not a float and not a duration
# string. A ratified flag with no per-item time is unfalsifiable, and this
# is the same field that instruments adjudication throughput.
assert values["adjudication_dwell_s"] == "41"
assert int(values["adjudication_dwell_s"]) == 41
def test_the_state_is_projected_as_an_index_facet(tmp_path: Path) -> None:
build_v0_2(tmp_path, adjudicated=("krav/forord.md",))
entries = indexes(tmp_path / "bundle", profile=SEGMENTED_OKF_V0_2)
facets = [entry.facets for listing in entries.values() for entry in listing if entry.facets]
states = {facet.get("adjudication") for facet in facets}
assert "adjudicated" in states
assert "proposed" in states
def test_the_facet_survives_a_reprojection_of_the_whole_bundle(tmp_path: Path) -> None:
"""The index is a PROJECTION recomputed from stored frontmatter each round.
A state that lived only in the index would be lost the moment the index
was rebuilt, which is every round.
"""
build_v0_2(tmp_path, adjudicated=("krav/forord.md",))
first = tree(tmp_path / "bundle")
build_v0_2(tmp_path, adjudicated=("krav/forord.md",))
assert tree(tmp_path / "bundle") == first
def test_a_state_outside_the_closed_set_is_refused(tmp_path: Path) -> None:
"""Closed means closed: a value outside it is an error, not an extension.
Reaches the door the way an outside value actually can -- declared in the
dropped document's own frontmatter, before this library writes anything.
"""
body = "---\nadjudication: nesten-ferdig\n---\n\n" + DOCUMENT
source = drop(tmp_path / "round", "n500.md", body)
# The span covers the block, which is the only way a declared value reaches
# derivation at all: a segment body is a SLICE, so a frontmatter block the
# span misses is simply not part of that concept.
plan = build_plan(
source.read_bytes(),
paths=("krav/forord.md",),
text=body,
entries=[
{
"segment_id": "s0",
"path": "krav/forord.md",
"title": "Forord",
"okf_type": "requirement",
"span": [0, len(body)],
"ingested_at": PLAN_AT,
}
],
)
result = run(tmp_path, plan=plan, profile=SEGMENTED_OKF_V0_2)
assert {entry.error.code for entry in result.failed} == {"index_facet_invalid"}
def test_segmented_v1_writes_no_adjudication_state_at_all(tmp_path: Path) -> None:
"""The discriminator, measured: the older profile's bytes must not move."""
build(tmp_path, bundle_name="bundle")
values = frontmatter_of(tmp_path / "bundle" / "krav" / "forord.md")
assert "adjudication" not in values
assert "adjudicated_by" not in values