resource-control/tests/test_basis.py
tegwick 7a196b6265 feat: adopt canon 0.4.0-0.6.0 — EvidenceBasis is canon, uses_provisions is canon
Both demands were accepted. Adopting what landed.

EVIDENCE BASIS IS NOW ITC-GOV CANON (0.4.0)
tools/basis.py reads infospace/models/governance/evidence-basis.yaml instead of
defining its own vocabulary — same discipline we already applied to the
capability catalog. Two semantic changes came back that we did not have:

- estimated and assumed are peers in tier "judgement". We had them separately
  ranked, which asserted a difference the canon does not.
- derived belongs to no tier at all; asking for its tier before resolving it is
  now an error rather than a silent rank.

Tier membership is read from tiers[].members, not bases[].tier: the latter
labels invoiced/measured/quoted all as "evidenced" while the tier list splits
them across "observed" and "quoted". tiers[] is authoritative; reported upstream.

USES_PROVISIONS IS NOW CANON (0.5.0, CAP-R11)
Dropped the proposed_extensions marker. Renamed relation "uses" to "may_use" per
their migration note. tools/capability.py now enforces CAP-R11: relation must be
depends_on or may_use, a provider must be named, and a depends_on entry MUST be
declared between those capabilities in the catalog. data.backup gained catalog
may_use: security.secrets from our restatement, so our entry now checks out.

Also in 0.4.0: §10.3 changed so a joinable consumer record counts as promotion
proof, met by our restatement; ITC-CAP is now 0.4.0 / canon 0.6.0, status draft.
Record and tests updated to those versions.

196 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 19:57:46 +02:00

194 lines
8 KiB
Python

import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parents[1] / "tools"))
from basis import (
bases as catalog_bases,
load_catalog,
tier_of,
decision_grade,
is_evidenced,
profile,
rank,
resolve,
strongest,
validate_value,
weakest,
)
def value(basis="measured", **overrides):
result = {"name": "v", "basis": basis, "value": 1}
if basis == "unknown":
result = {"name": "v", "basis": basis, "value": None, "gap": "not measured (owner: x)"}
result.update(overrides)
return result
class CanonBindingTest(unittest.TestCase):
"""The vocabulary is owned by ITC-GOV; we read it, we do not vendor it."""
def test_catalog_is_read_from_the_canon(self):
catalog = load_catalog()
self.assertEqual("0.1.0", catalog["version"])
self.assertIn("info-tech-canon", catalog["source"])
def test_every_basis_comes_from_the_canon_catalog(self):
self.assertEqual(
["invoiced", "measured", "quoted", "derived", "projected", "estimated",
"assumed", "unknown"],
catalog_bases(),
)
class TierTest(unittest.TestCase):
def test_order_runs_strongest_to_weakest(self):
self.assertLess(rank("measured"), rank("projected"))
self.assertLess(rank("projected"), rank("estimated"))
self.assertLess(rank("estimated"), rank("unknown"))
def test_invoiced_and_measured_are_peers_not_ranked(self):
"""An invoice is authoritative for a payment, a measurement for a
quantity. Neither outranks the other outside its own domain."""
self.assertEqual("observed", tier_of("invoiced"))
self.assertEqual("observed", tier_of("measured"))
self.assertEqual(rank("invoiced"), rank("measured"))
self.assertLess(rank("measured"), rank("quoted"))
def test_estimated_and_assumed_are_peers_in_the_judgement_tier(self):
"""Canon 0.4.0 groups them; neither is stronger than the other."""
self.assertEqual("judgement", tier_of("estimated"))
self.assertEqual("judgement", tier_of("assumed"))
self.assertEqual(rank("estimated"), rank("assumed"))
def test_quoted_is_below_observed_for_propagation_but_still_evidenced(self):
self.assertGreater(rank("quoted"), rank("measured"))
self.assertTrue(is_evidenced("quoted"))
def test_derived_has_no_resolved_tier(self):
with self.assertRaisesRegex(ValueError, "no resolved tier"):
tier_of("derived")
def test_a_peer_pair_does_not_report_a_false_weakest(self):
self.assertEqual(rank("invoiced"), rank(weakest(["invoiced", "measured"])))
self.assertEqual(weakest(["invoiced", "measured"]), weakest(["measured", "invoiced"]))
self.assertEqual(weakest(["estimated", "assumed"]), weakest(["assumed", "estimated"]))
def test_weakest_and_strongest_pick_opposite_ends(self):
bases = ["measured", "assumed", "quoted"]
self.assertEqual("assumed", weakest(bases))
self.assertEqual("measured", strongest(bases))
def test_empty_collection_is_unknown_not_an_error(self):
self.assertEqual("unknown", weakest([]))
def test_only_observed_or_contracted_bases_count_as_evidenced(self):
for basis in ("invoiced", "measured", "quoted"):
self.assertTrue(is_evidenced(basis))
for basis in ("projected", "estimated", "assumed", "unknown"):
self.assertFalse(is_evidenced(basis))
# derived is not evidenced or unevidenced until it is resolved
with self.assertRaises(ValueError):
is_evidenced("derived")
def test_unknown_basis_name_is_rejected(self):
with self.assertRaises(ValueError):
rank("vibes")
class PropagationTest(unittest.TestCase):
"""A derived value is only as strong as its weakest input."""
def test_derivation_from_measured_inputs_stays_measured(self):
derived = value("derived", derived_from=[value("measured"), value("measured")])
self.assertEqual("measured", resolve(derived))
def test_one_assumed_input_drags_the_result_down(self):
derived = value("derived", derived_from=[value("quoted"), value("assumed")])
self.assertEqual("judgement", tier_of(resolve(derived)))
def test_propagation_is_recursive(self):
inner = value("derived", derived_from=[value("measured"), value("estimated")])
outer = value("derived", derived_from=[value("measured"), inner])
self.assertEqual("judgement", tier_of(resolve(outer)))
def test_derived_without_inputs_is_rejected(self):
with self.assertRaisesRegex(ValueError, "derived_from"):
resolve(value("derived"))
def test_non_derived_value_resolves_to_itself(self):
self.assertEqual("quoted", resolve(value("quoted")))
class ValidationTest(unittest.TestCase):
def test_unknown_must_not_carry_a_quantity(self):
with self.assertRaises(ValueError):
validate_value({"basis": "unknown", "value": 0, "gap": "g"})
def test_unknown_must_name_the_gap_and_owner(self):
with self.assertRaisesRegex(ValueError, "name the gap"):
validate_value({"basis": "unknown", "value": None})
def test_known_basis_must_carry_a_quantity(self):
with self.assertRaisesRegex(ValueError, "must carry a quantity"):
validate_value({"basis": "measured", "value": None})
def test_zero_is_a_measurement_not_an_absence(self):
validate_value({"basis": "measured", "value": 0})
def test_derived_from_only_allowed_on_derived(self):
with self.assertRaisesRegex(ValueError, "only a derived value"):
validate_value(value("measured", derived_from=[value()]))
def test_empty_proxy_target_is_rejected_but_absent_one_is_fine(self):
with self.assertRaisesRegex(ValueError, "proxy_for"):
validate_value(value("measured", proxy_for=" "))
validate_value(value("measured", proxy_for=None))
validate_value(value("measured", proxy_for="cost of the shared host"))
class DecisionGradeTest(unittest.TestCase):
def test_all_evidenced_values_grade_evidenced(self):
result = decision_grade([value("measured"), value("invoiced"), value("quoted")])
self.assertEqual("evidenced", result["grade"])
self.assertEqual(1.0, result["evidenced_ratio"])
def test_a_single_assumption_makes_the_whole_decision_indicative(self):
result = decision_grade([value("measured"), value("measured"), value("assumed")])
self.assertEqual("indicative", result["grade"])
self.assertEqual("judgement", result["weakest_tier"])
def test_an_unknown_makes_the_decision_insufficient(self):
result = decision_grade([value("measured"), value("unknown")])
self.assertEqual("insufficient", result["grade"])
def test_precise_arithmetic_does_not_upgrade_weak_inputs(self):
"""The provider comparison: a two-decimal euro figure built on assumptions."""
euros = value(
"derived",
name="labour_eur_month",
value=60.0,
derived_from=[value("assumed", name="hours"), value("assumed", name="rate")],
)
result = decision_grade([value("quoted", name="price"), euros])
self.assertEqual("judgement", result["weakest_tier"])
self.assertEqual("indicative", result["grade"])
def test_projected_values_grade_separately_from_estimates(self):
result = decision_grade([value("measured"), value("projected")])
self.assertEqual("projected", result["grade"])
def test_proxies_are_reported_in_the_note(self):
result = decision_grade([value("measured", proxy_for="cost of the shared host")])
self.assertEqual(1, len(result["proxies"]))
self.assertIn("proxy", result["note"])
def test_profile_counts_by_basis_in_canonical_order(self):
result = profile([value("assumed"), value("measured"), value("measured")])
self.assertEqual(["measured", "assumed"], list(result["by_basis"]))
self.assertEqual(2, result["by_basis"]["measured"])
if __name__ == "__main__":
unittest.main()