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>
This commit is contained in:
parent
b8081f6c2d
commit
7a196b6265
6 changed files with 292 additions and 131 deletions
|
|
@ -4,7 +4,9 @@ from pathlib import Path
|
|||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "tools"))
|
||||
from basis import (
|
||||
BASIS_ORDER,
|
||||
bases as catalog_bases,
|
||||
load_catalog,
|
||||
tier_of,
|
||||
decision_grade,
|
||||
is_evidenced,
|
||||
profile,
|
||||
|
|
@ -24,22 +26,54 @@ def value(basis="measured", **overrides):
|
|||
return result
|
||||
|
||||
|
||||
class OrderTest(unittest.TestCase):
|
||||
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.assertEqual("invoiced", BASIS_ORDER[0])
|
||||
self.assertEqual("unknown", BASIS_ORDER[-1])
|
||||
self.assertLess(rank("measured"), rank("estimated"))
|
||||
self.assertLess(rank("estimated"), rank("assumed"))
|
||||
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"]
|
||||
|
|
@ -52,8 +86,11 @@ class OrderTest(unittest.TestCase):
|
|||
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 ("derived", "projected", "estimated", "assumed", "unknown"):
|
||||
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):
|
||||
|
|
@ -69,12 +106,12 @@ class PropagationTest(unittest.TestCase):
|
|||
|
||||
def test_one_assumed_input_drags_the_result_down(self):
|
||||
derived = value("derived", derived_from=[value("quoted"), value("assumed")])
|
||||
self.assertEqual("assumed", resolve(derived))
|
||||
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("estimated", resolve(outer))
|
||||
self.assertEqual("judgement", tier_of(resolve(outer)))
|
||||
|
||||
def test_derived_without_inputs_is_rejected(self):
|
||||
with self.assertRaisesRegex(ValueError, "derived_from"):
|
||||
|
|
@ -120,7 +157,7 @@ class DecisionGradeTest(unittest.TestCase):
|
|||
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("assumed", result["weakest"])
|
||||
self.assertEqual("judgement", result["weakest_tier"])
|
||||
|
||||
def test_an_unknown_makes_the_decision_insufficient(self):
|
||||
result = decision_grade([value("measured"), value("unknown")])
|
||||
|
|
@ -135,7 +172,7 @@ class DecisionGradeTest(unittest.TestCase):
|
|||
derived_from=[value("assumed", name="hours"), value("assumed", name="rate")],
|
||||
)
|
||||
result = decision_grade([value("quoted", name="price"), euros])
|
||||
self.assertEqual("assumed", result["weakest"])
|
||||
self.assertEqual("judgement", result["weakest_tier"])
|
||||
self.assertEqual("indicative", result["grade"])
|
||||
|
||||
def test_projected_values_grade_separately_from_estimates(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue