feat(consume): derive trust_tier, and withhold what cannot be tiered
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
1ec3f5289c
commit
aa33555208
13 changed files with 278 additions and 0 deletions
1
tests/fixtures/consume-bundle/dyp/index.md
vendored
Normal file
1
tests/fixtures/consume-bundle/dyp/index.md
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
- [nivaa (index)](nivaa/index.md)
|
||||||
14
tests/fixtures/consume-bundle/dyp/nivaa/alminnelig-notat.md
vendored
Normal file
14
tests/fixtures/consume-bundle/dyp/nivaa/alminnelig-notat.md
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
type: verdict
|
||||||
|
title: Alminnelig notat
|
||||||
|
source_file: Dom.md
|
||||||
|
source_sha256: 5555555555555555555555555555555555555555555555555555555555555555
|
||||||
|
ingested_at: 2026-09-01T00:00:00Z
|
||||||
|
adjudication: proposed
|
||||||
|
bundle_id: consume-fixture
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alminnelig notat
|
||||||
|
|
||||||
|
Navnet er alminnelig og stien er dyp. Typen er `verdict`, og det er typen
|
||||||
|
som utelukker den -- paa hvert nivaa, ikke bare paa rota.
|
||||||
17
tests/fixtures/consume-bundle/dyp/nivaa/blokkform-verifisert.md
vendored
Normal file
17
tests/fixtures/consume-bundle/dyp/nivaa/blokkform-verifisert.md
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
type: reference
|
||||||
|
title: Blokkform verifisert
|
||||||
|
source_file: Blokk.md
|
||||||
|
source_sha256: 6666666666666666666666666666666666666666666666666666666666666666
|
||||||
|
ingested_at: 2026-09-01T00:00:00Z
|
||||||
|
adjudication: proposed
|
||||||
|
bundle_id: consume-fixture
|
||||||
|
verified:
|
||||||
|
- by: process:okf-check
|
||||||
|
at: 2026-09-01T00:00:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blokkform
|
||||||
|
|
||||||
|
Verdien staar i blokkform. Denne bibliotekets linjeorienterte leser ser
|
||||||
|
den som tom streng, ikke som fravaer.
|
||||||
2
tests/fixtures/consume-bundle/dyp/nivaa/index.md
vendored
Normal file
2
tests/fixtures/consume-bundle/dyp/nivaa/index.md
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
- [Alminnelig notat](alminnelig-notat.md) — adjudication: proposed
|
||||||
|
- [Blokkform verifisert](blokkform-verifisert.md) — adjudication: proposed
|
||||||
8
tests/fixtures/consume-bundle/index.md
vendored
Normal file
8
tests/fixtures/consume-bundle/index.md
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
okf_version: 0.2
|
||||||
|
bundle_id: consume-fixture
|
||||||
|
---
|
||||||
|
|
||||||
|
- [Omfang og formaal](scope.md)
|
||||||
|
- [krav (index)](krav/index.md)
|
||||||
|
- [dyp (index)](dyp/index.md)
|
||||||
15
tests/fixtures/consume-bundle/krav/aarlig-kontroll.md
vendored
Normal file
15
tests/fixtures/consume-bundle/krav/aarlig-kontroll.md
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
type: reference
|
||||||
|
title: Årlig kontroll av anlegget
|
||||||
|
source_file: Kontrollplan.pdf
|
||||||
|
source_sha256: 2222222222222222222222222222222222222222222222222222222222222222
|
||||||
|
ingested_at: 2026-09-01T00:00:00Z
|
||||||
|
adjudication: adjudicated
|
||||||
|
bundle_id: consume-fixture
|
||||||
|
verified: [{ by: human:ktg, at: 2026-09-01T00:00:00Z }]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Årlig kontroll
|
||||||
|
|
||||||
|
Anlegget kontrolleres årlig. Kontrollen dokumenteres skriftlig, og
|
||||||
|
avvik lukkes før neste kontroll.
|
||||||
4
tests/fixtures/consume-bundle/krav/index.md
vendored
Normal file
4
tests/fixtures/consume-bundle/krav/index.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
- [Prissammenstilling](prissammenstilling.md) — adjudication: proposed
|
||||||
|
- [Årlig kontroll av anlegget](aarlig-kontroll.md) — adjudication: adjudicated
|
||||||
|
- [verdict-lookalike](verdict-lookalike.md) — adjudication: proposed
|
||||||
|
- [Loggnotat](loggnotat.md) — adjudication: proposed
|
||||||
13
tests/fixtures/consume-bundle/krav/loggnotat.md
vendored
Normal file
13
tests/fixtures/consume-bundle/krav/loggnotat.md
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
type: Log
|
||||||
|
title: Loggnotat
|
||||||
|
source_file: Logg.md
|
||||||
|
source_sha256: 4444444444444444444444444444444444444444444444444444444444444444
|
||||||
|
ingested_at: 2026-09-01T00:00:00Z
|
||||||
|
adjudication: proposed
|
||||||
|
bundle_id: consume-fixture
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loggnotat
|
||||||
|
|
||||||
|
Typen er skrevet med stor L. Den finnes slik i den ekte K2-bundelen.
|
||||||
15
tests/fixtures/consume-bundle/krav/prissammenstilling.md
vendored
Normal file
15
tests/fixtures/consume-bundle/krav/prissammenstilling.md
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
type: reference
|
||||||
|
title: Prissammenstilling
|
||||||
|
source_file: Prisskjema.xlsx
|
||||||
|
source_sha256: 1111111111111111111111111111111111111111111111111111111111111111
|
||||||
|
ingested_at: 2026-09-01T00:00:00Z
|
||||||
|
adjudication: proposed
|
||||||
|
bundle_id: consume-fixture
|
||||||
|
verified: [{ by: process:okf-check, at: 2026-09-01T00:00:00Z }]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prissammenstilling
|
||||||
|
|
||||||
|
Prisene fylles ut i dette skjemaet. Summen av alle poster overfoeres til
|
||||||
|
tilbudsbrevet.
|
||||||
14
tests/fixtures/consume-bundle/krav/verdict-lookalike.md
vendored
Normal file
14
tests/fixtures/consume-bundle/krav/verdict-lookalike.md
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
type: reference
|
||||||
|
title: verdict-lookalike
|
||||||
|
source_file: Notat.md
|
||||||
|
source_sha256: 3333333333333333333333333333333333333333333333333333333333333333
|
||||||
|
ingested_at: 2026-09-01T00:00:00Z
|
||||||
|
adjudication: proposed
|
||||||
|
bundle_id: consume-fixture
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ikke en dom
|
||||||
|
|
||||||
|
Filen heter noe som ligner, men typen er `reference`. Et stifilter ville
|
||||||
|
utelukket den; en typesjekk slipper den gjennom.
|
||||||
14
tests/fixtures/consume-bundle/scope.md
vendored
Normal file
14
tests/fixtures/consume-bundle/scope.md
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
type: reference
|
||||||
|
title: Omfang og formaal
|
||||||
|
source_file: Fiktivt grunnlag.md
|
||||||
|
source_sha256: 0000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
ingested_at: 2026-09-01T00:00:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
## Omfang
|
||||||
|
|
||||||
|
Dette er en syntetisk bundel. Den finnes fordi den ekte K2-bundelen har
|
||||||
|
null konsepter med `verified:`, null med `type: verdict` og null med
|
||||||
|
`adjudication: adjudicated` -- tre nevnere paa null, som ingen test kan
|
||||||
|
oeve mot.
|
||||||
|
|
@ -191,3 +191,65 @@ def test_bundle_id_falls_back_to_the_root_index_and_says_that_it_did(tmp_path: P
|
||||||
assert concept.bundle_id == ROOT_BUNDLE_ID
|
assert concept.bundle_id == ROOT_BUNDLE_ID
|
||||||
assert concept.bundle_id_inherited is True
|
assert concept.bundle_id_inherited is True
|
||||||
assert _read(PROPOSED_CONCEPT).bundle_id_inherited is False
|
assert _read(PROPOSED_CONCEPT).bundle_id_inherited is False
|
||||||
|
|
||||||
|
|
||||||
|
# --- Step 3: trust_tier, and the refusal to tier what cannot be read ----------
|
||||||
|
|
||||||
|
FIXTURE = PROJECT_ROOT / "tests" / "fixtures" / "consume-bundle"
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_fixture_bundle_carries_what_the_real_corpus_has_none_of() -> None:
|
||||||
|
# The control on every assertion below. K2 has 0 `verified:` keys, 0
|
||||||
|
# `type: verdict` and 0 `adjudication: adjudicated` over 629 concepts, so a
|
||||||
|
# fixture missing any of them would make its tests pass over an empty set.
|
||||||
|
text = "\n".join(path.read_text(encoding="utf-8") for path in sorted(FIXTURE.rglob("*.md")))
|
||||||
|
assert "type: verdict" in text
|
||||||
|
assert "adjudication: adjudicated" in text
|
||||||
|
assert "by: human:" in text
|
||||||
|
assert "by: process:" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_verified_key_reads_as_unverified() -> None:
|
||||||
|
# SS 6.3: a concept carrying no trust frontmatter is still consumable.
|
||||||
|
assert okf_consume.trust_tier(None) == "unverified"
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_human_actor_reads_as_human_reviewed() -> None:
|
||||||
|
assert okf_consume.trust_tier("[{ by: human:ktg, at: 2026-09-01T00:00:00Z }]") == (
|
||||||
|
"human-reviewed"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_process_actor_reads_as_machine_confirmed() -> None:
|
||||||
|
assert okf_consume.trust_tier("[{ by: process:okf-check, at: 2026-09-01T00:00:00Z }]") == (
|
||||||
|
"machine-confirmed"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_human_test_is_a_prefix_and_never_a_substring() -> None:
|
||||||
|
# `bot/human:2` is a MACHINE actor whose id contains the string `human:`.
|
||||||
|
# A substring test would promote it to the highest tier -- fabricated
|
||||||
|
# provenance produced by a matching bug.
|
||||||
|
assert okf_consume.trust_tier("[{ by: bot/human:2 }]") == "machine-confirmed"
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_entry_naming_no_actor_is_refused_rather_than_tiered() -> None:
|
||||||
|
with pytest.raises(okf_consume.ConsumeError) as raised:
|
||||||
|
okf_consume.trust_tier("[{ at: 2026-09-01T00:00:00Z }]")
|
||||||
|
assert raised.value.code == "verified_actorless"
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_block_form_verified_is_not_a_tier_at_all() -> None:
|
||||||
|
# Measured 2026-09-07: this library's line-oriented `parse_frontmatter`
|
||||||
|
# returns `''` for a block-form `verified:` and the full string for a flow
|
||||||
|
# one, so PRESENT-BUT-UNREADABLE is distinguishable from ABSENT. Emitting
|
||||||
|
# `unverified` here would assert a fact nobody measured (SS 6.4).
|
||||||
|
assert okf_consume.trust_tier("") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_block_form_case_is_real_in_the_fixture_and_not_only_in_the_unit_test() -> None:
|
||||||
|
# The control: without this, `trust_tier("") is None` could be true of a
|
||||||
|
# string no bundle ever produces.
|
||||||
|
frontmatter = parse_frontmatter(FIXTURE / "dyp" / "nivaa" / "blokkform-verifisert.md")
|
||||||
|
assert frontmatter["verified"] == ""
|
||||||
|
assert okf_consume.trust_tier(frontmatter["verified"]) is None
|
||||||
|
|
|
||||||
|
|
@ -304,3 +304,102 @@ def _body(path: Path) -> str:
|
||||||
if line.strip() == "---":
|
if line.strip() == "---":
|
||||||
return "\n".join(lines[offset:])
|
return "\n".join(lines[offset:])
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
#: SS 6.2, from SPEC SS 5.3, lowest to highest. Imported from the checker's own
|
||||||
|
#: constant would be circular (the checker is a separate tool); spelled here and
|
||||||
|
#: held to the checker's set by the anti-drift test.
|
||||||
|
TrustTier = Literal["unverified", "machine-confirmed", "human-reviewed"]
|
||||||
|
|
||||||
|
#: The prefix that makes an actor a person. A PREFIX, never a substring: an
|
||||||
|
#: actor id `bot/human:2` contains the literal and is a machine, and promoting
|
||||||
|
#: it would be fabricated provenance produced by a matching bug.
|
||||||
|
HUMAN_ACTOR_PREFIX = "human:"
|
||||||
|
|
||||||
|
|
||||||
|
def trust_tier(verified_raw: str | None) -> TrustTier | None:
|
||||||
|
"""The tier `verified` implies, or `None` when the value cannot be read.
|
||||||
|
|
||||||
|
Three inputs, three different facts, and collapsing any two is the defect:
|
||||||
|
|
||||||
|
- `None` -- the key is ABSENT. SS 6.2: no `verified` key means
|
||||||
|
`unverified`, and SS 6.3 forbids rejecting a concept for it.
|
||||||
|
- `""` -- the key is PRESENT and this library cannot read it. Measured
|
||||||
|
2026-09-07: the line-oriented `parse_frontmatter` returns `''` for a
|
||||||
|
block-form value and the full string for a flow one, so the two are
|
||||||
|
distinguishable. Returning `None` here is not a tier; the caller withholds
|
||||||
|
the concept under a named rule. Emitting `unverified` instead would assert
|
||||||
|
a fact nobody measured, which is exactly what SS 6.4 forbids.
|
||||||
|
- a flow value -- decoded, and the tier follows the actors.
|
||||||
|
"""
|
||||||
|
if verified_raw is None:
|
||||||
|
return "unverified"
|
||||||
|
if not verified_raw.strip():
|
||||||
|
return None
|
||||||
|
entries = _parse_flow_mappings(verified_raw)
|
||||||
|
if entries is None:
|
||||||
|
return None
|
||||||
|
actors: list[str] = []
|
||||||
|
for entry in entries:
|
||||||
|
actor = entry.get("by")
|
||||||
|
if not actor:
|
||||||
|
raise ConsumeError(
|
||||||
|
f"a `verified` entry names no `by` actor ({verified_raw!r}); "
|
||||||
|
"refusing to tier it, because the tier IS a claim about who "
|
||||||
|
"checked and there is nobody to name",
|
||||||
|
code="verified_actorless",
|
||||||
|
)
|
||||||
|
actors.append(actor)
|
||||||
|
if not actors:
|
||||||
|
return None
|
||||||
|
if any(actor.startswith(HUMAN_ACTOR_PREFIX) for actor in actors):
|
||||||
|
return "human-reviewed"
|
||||||
|
return "machine-confirmed"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_flow_mappings(value: str) -> list[dict[str, str]] | None:
|
||||||
|
"""A YAML flow sequence of flow mappings, or `None` when it is not one.
|
||||||
|
|
||||||
|
Modelled on `structure._parse_flow_list` -- flow form only, because this
|
||||||
|
library's standing rule is that a value it can write is a value it can read
|
||||||
|
back. Not reused: that one splits on every comma, and `{ by: x, at: y }`
|
||||||
|
carries a comma INSIDE a mapping, so it would return four fragments where
|
||||||
|
there are two pairs.
|
||||||
|
"""
|
||||||
|
stripped = value.strip()
|
||||||
|
if not (stripped.startswith("[") and stripped.endswith("]")):
|
||||||
|
return None
|
||||||
|
body = stripped[1:-1].strip()
|
||||||
|
if not body:
|
||||||
|
return []
|
||||||
|
mappings: list[dict[str, str]] = []
|
||||||
|
for chunk in _split_top_level(body, "{", "}"):
|
||||||
|
item = chunk.strip()
|
||||||
|
if not (item.startswith("{") and item.endswith("}")):
|
||||||
|
return None
|
||||||
|
pairs: dict[str, str] = {}
|
||||||
|
for field in item[1:-1].split(","):
|
||||||
|
key, separator, raw = field.partition(":")
|
||||||
|
if separator:
|
||||||
|
pairs[key.strip()] = raw.strip()
|
||||||
|
mappings.append(pairs)
|
||||||
|
return mappings
|
||||||
|
|
||||||
|
|
||||||
|
def _split_top_level(body: str, opener: str, closer: str) -> list[str]:
|
||||||
|
"""Split on commas that are not inside a `{...}`."""
|
||||||
|
parts: list[str] = []
|
||||||
|
depth = 0
|
||||||
|
current: list[str] = []
|
||||||
|
for character in body:
|
||||||
|
if character == opener:
|
||||||
|
depth += 1
|
||||||
|
elif character == closer:
|
||||||
|
depth -= 1
|
||||||
|
if character == "," and depth == 0:
|
||||||
|
parts.append("".join(current))
|
||||||
|
current = []
|
||||||
|
continue
|
||||||
|
current.append(character)
|
||||||
|
parts.append("".join(current))
|
||||||
|
return [part for part in parts if part.strip()]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue