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
231
tools/basis.py
231
tools/basis.py
|
|
@ -1,13 +1,15 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Evidence basis: how a value was obtained, and how far it can be trusted.
|
||||
|
||||
Every quantity in this repository is one of a small number of epistemic kinds.
|
||||
A counted object and an assumed hourly rate are both numbers; they are not both
|
||||
knowledge. This module names the difference and propagates it.
|
||||
The vocabulary is **owned by ITC-GOV** and read from
|
||||
`infospace/models/governance/evidence-basis.yaml` in info-tech-canon. This
|
||||
module does not vendor it. resource-control originated the concept, filed it as
|
||||
demand, and now consumes the canon's version of it — drift in either repository
|
||||
fails here rather than diverging quietly.
|
||||
|
||||
The central rule is that a derived value is only as strong as its weakest
|
||||
input. Without it, a precise-looking figure launders weak assumptions: EUR 30.00
|
||||
of monthly labour reads like a measurement, when it is another repository's
|
||||
The central rule is that a derived value is only as strong as the weakest
|
||||
*tier* among its inputs. Without it, arithmetic launders assumptions: EUR 30.00
|
||||
of monthly labour reads like a measurement when it is another repository's
|
||||
estimate of hours multiplied by a rate we chose.
|
||||
"""
|
||||
|
||||
|
|
@ -15,94 +17,135 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
# Ordered strongest to weakest. The order is the whole point: it is what makes
|
||||
# "weakest input wins" computable.
|
||||
BASIS_ORDER = (
|
||||
"invoiced", # a booked financial fact, authoritative from fin-hub
|
||||
"measured", # directly observed from the authoritative system
|
||||
"quoted", # stated by a provider or counterparty in a citable source
|
||||
"derived", # computed from other values by a stated rule
|
||||
"projected", # interpolated between, or extrapolated beyond, observations
|
||||
"estimated", # human judgement, neither observed nor computed
|
||||
"assumed", # a modelling constant we chose
|
||||
"unknown", # no value exists
|
||||
CATALOG_PATHS = (
|
||||
Path("/home/worsch/info-tech-canon/infospace/models/governance/evidence-basis.yaml"),
|
||||
Path("../info-tech-canon/infospace/models/governance/evidence-basis.yaml"),
|
||||
)
|
||||
BASES = frozenset(BASIS_ORDER)
|
||||
|
||||
# Strength is a tier, not a total order. `invoiced` and `measured` are peers:
|
||||
# an invoice is the authoritative record of a payment, a measurement is the
|
||||
# authoritative record of a quantity, and neither outranks the other outside
|
||||
# its own domain. Asserting an order between them would make the weakest-input
|
||||
# rule claim something it cannot know.
|
||||
_TIER = {
|
||||
"invoiced": 0, "measured": 0,
|
||||
"quoted": 1,
|
||||
"derived": 2,
|
||||
"projected": 3,
|
||||
"estimated": 4,
|
||||
"assumed": 5,
|
||||
"unknown": 6,
|
||||
}
|
||||
_ORDER = {name: index for index, name in enumerate(BASIS_ORDER)}
|
||||
|
||||
# Bases that assert an observed or contracted fact about the world.
|
||||
EVIDENCED = frozenset({"invoiced", "measured", "quoted"})
|
||||
|
||||
|
||||
def rank(basis: str) -> int:
|
||||
"""Strength tier; lower is stronger. Peers share a tier."""
|
||||
if basis not in _TIER:
|
||||
@lru_cache(maxsize=1)
|
||||
def load_catalog(paths: tuple = CATALOG_PATHS) -> dict:
|
||||
"""Read the ITC-GOV EvidenceBasis catalog."""
|
||||
import yaml
|
||||
|
||||
for path in paths:
|
||||
if path.exists():
|
||||
raw = yaml.safe_load(path.read_text())
|
||||
tiers = {tier["id"]: tier for tier in raw["tiers"]}
|
||||
bases = {b["id"]: b for b in raw["bases"]}
|
||||
# Membership comes from tiers[].members, which is authoritative.
|
||||
# The coarser bases[].tier label is not usable as a tier id: it
|
||||
# reads "evidenced" for invoiced/measured/quoted while the tier
|
||||
# list splits those across "observed" and "quoted".
|
||||
tier_of = {
|
||||
member: tier["id"]
|
||||
for tier in raw["tiers"]
|
||||
for member in tier["members"]
|
||||
}
|
||||
# A basis in no tier (derived) has no resolved tier by design.
|
||||
tier_of.update({b: None for b in bases if b not in tier_of})
|
||||
return {
|
||||
"version": raw["canon"]["version"],
|
||||
"canon_version": raw["canon"]["canon_version"],
|
||||
"source": str(path),
|
||||
"bases": bases,
|
||||
"order": [b["id"] for b in raw["bases"]],
|
||||
"tiers": tiers,
|
||||
"tier_of": tier_of,
|
||||
"tier_rank": {t["id"]: t["rank"] for t in raw["tiers"]},
|
||||
"evidenced_tiers": {t["id"] for t in raw["tiers"] if t.get("evidenced")},
|
||||
"grades": {g["id"]: g["when"] for g in raw["decision_grades"]},
|
||||
}
|
||||
raise FileNotFoundError(
|
||||
"ITC-GOV EvidenceBasis catalog not found; is info-tech-canon checked out?"
|
||||
)
|
||||
|
||||
|
||||
def _catalog(catalog: dict | None = None) -> dict:
|
||||
return catalog or load_catalog()
|
||||
|
||||
|
||||
def bases(catalog: dict | None = None) -> list[str]:
|
||||
return list(_catalog(catalog)["order"])
|
||||
|
||||
|
||||
def tier_of(basis: str, catalog: dict | None = None) -> str:
|
||||
"""The resolved strength tier of a basis.
|
||||
|
||||
`derived` deliberately has none: resolve it against its inputs first.
|
||||
"""
|
||||
cat = _catalog(catalog)
|
||||
if basis not in cat["tier_of"]:
|
||||
raise ValueError(f"unknown evidence basis {basis!r}")
|
||||
return _TIER[basis]
|
||||
tier = cat["tier_of"][basis]
|
||||
if tier is None:
|
||||
raise ValueError(
|
||||
f"{basis!r} has no resolved tier; resolve it against derived_from first"
|
||||
)
|
||||
return tier
|
||||
|
||||
|
||||
def weakest(bases) -> str:
|
||||
"""The weakest basis in a collection. Empty means nothing is known.
|
||||
def rank(basis: str, catalog: dict | None = None) -> int:
|
||||
"""Tier rank; lower is stronger. Members of one tier share a rank."""
|
||||
cat = _catalog(catalog)
|
||||
return cat["tier_rank"][tier_of(basis, cat)]
|
||||
|
||||
|
||||
def weakest(names, catalog: dict | None = None) -> str:
|
||||
"""The weakest basis in a collection, compared by tier.
|
||||
|
||||
Ties within a tier resolve by catalog order so the result is deterministic
|
||||
without implying a strength difference that does not exist.
|
||||
without implying a strength difference the canon does not assert.
|
||||
"""
|
||||
bases = list(bases)
|
||||
if not bases:
|
||||
cat = _catalog(catalog)
|
||||
names = list(names)
|
||||
if not names:
|
||||
return "unknown"
|
||||
return min(bases, key=lambda b: (-rank(b), _ORDER[b]))
|
||||
order = cat["order"]
|
||||
return min(names, key=lambda b: (-rank(b, cat), order.index(b)))
|
||||
|
||||
|
||||
def strongest(bases) -> str:
|
||||
bases = list(bases)
|
||||
if not bases:
|
||||
def strongest(names, catalog: dict | None = None) -> str:
|
||||
cat = _catalog(catalog)
|
||||
names = list(names)
|
||||
if not names:
|
||||
return "unknown"
|
||||
return min(bases, key=lambda b: (rank(b), _ORDER[b]))
|
||||
order = cat["order"]
|
||||
return min(names, key=lambda b: (rank(b, cat), order.index(b)))
|
||||
|
||||
|
||||
def is_evidenced(basis: str) -> bool:
|
||||
"""True when the value asserts an observed or contracted fact."""
|
||||
return basis in EVIDENCED
|
||||
def is_evidenced(basis: str, catalog: dict | None = None) -> bool:
|
||||
"""True when the value asserts an observed or contracted fact.
|
||||
|
||||
|
||||
def resolve(value: dict) -> str:
|
||||
"""Effective basis of a value, propagating through derivation.
|
||||
|
||||
A `derived` value resolves to the weakest basis among its inputs: deriving
|
||||
GB from measured bytes stays measured, while deriving euros from estimated
|
||||
hours and an assumed rate is no better than assumed.
|
||||
`quoted` is weaker than `observed` for propagation and still counts as
|
||||
evidenced for a decision grade — the canon separates those two uses.
|
||||
"""
|
||||
cat = _catalog(catalog)
|
||||
return tier_of(basis, cat) in cat["evidenced_tiers"]
|
||||
|
||||
|
||||
def resolve(value: dict, catalog: dict | None = None) -> str:
|
||||
"""Effective basis of a value, propagating through derivation."""
|
||||
cat = _catalog(catalog)
|
||||
basis = value.get("basis", "unknown")
|
||||
if basis not in BASES:
|
||||
if basis not in cat["bases"]:
|
||||
raise ValueError(f"unknown evidence basis {basis!r}")
|
||||
if basis != "derived":
|
||||
return basis
|
||||
inputs = value.get("derived_from") or []
|
||||
if not inputs:
|
||||
raise ValueError("a derived value must record derived_from")
|
||||
return weakest(resolve(item) for item in inputs)
|
||||
return weakest([resolve(item, cat) for item in inputs], cat)
|
||||
|
||||
|
||||
def validate_value(value: dict) -> None:
|
||||
def validate_value(value: dict, catalog: dict | None = None) -> None:
|
||||
"""Enforce the ITC-GOV rules: unknown-is-not-zero and derived-names-inputs."""
|
||||
cat = _catalog(catalog)
|
||||
basis = value.get("basis")
|
||||
if basis not in BASES:
|
||||
if basis not in cat["bases"]:
|
||||
raise ValueError(f"unknown evidence basis {basis!r}")
|
||||
if basis == "derived" and not value.get("derived_from"):
|
||||
raise ValueError("a derived value must record derived_from")
|
||||
|
|
@ -115,55 +158,65 @@ def validate_value(value: dict) -> None:
|
|||
raise ValueError("an unknown value must name the gap and its owner")
|
||||
elif value.get("value") is None:
|
||||
raise ValueError("a known basis must carry a quantity; use basis unknown instead")
|
||||
# A proxy measures a different quantity than the one named. That does not
|
||||
# weaken the measurement, but it does weaken the inference drawn from it.
|
||||
proxy_for = value.get("proxy_for")
|
||||
if proxy_for is not None and not str(proxy_for).strip():
|
||||
raise ValueError("proxy_for must name the quantity actually wanted")
|
||||
for item in value.get("derived_from") or []:
|
||||
validate_value(item)
|
||||
validate_value(item, cat)
|
||||
|
||||
|
||||
def profile(values) -> dict:
|
||||
def profile(values, catalog: dict | None = None) -> dict:
|
||||
"""Summarize a set of values for decision review."""
|
||||
resolved = []
|
||||
proxies = []
|
||||
cat = _catalog(catalog)
|
||||
resolved, proxies = [], []
|
||||
for value in values:
|
||||
validate_value(value)
|
||||
effective = resolve(value)
|
||||
resolved.append(effective)
|
||||
validate_value(value, cat)
|
||||
resolved.append(resolve(value, cat))
|
||||
if value.get("proxy_for"):
|
||||
proxies.append({"name": value.get("name"), "proxy_for": value["proxy_for"]})
|
||||
counts: dict[str, int] = {}
|
||||
for basis in resolved:
|
||||
counts[basis] = counts.get(basis, 0) + 1
|
||||
evidenced = [b for b in resolved if is_evidenced(b)]
|
||||
evidenced = [b for b in resolved if is_evidenced(b, cat)]
|
||||
weakest_basis = weakest(resolved, cat)
|
||||
return {
|
||||
"count": len(resolved),
|
||||
"by_basis": {k: counts[k] for k in BASIS_ORDER if k in counts},
|
||||
"weakest": weakest(resolved),
|
||||
"by_basis": {k: counts[k] for k in cat["order"] if k in counts},
|
||||
"weakest": weakest_basis,
|
||||
"weakest_tier": tier_of(weakest_basis, cat) if resolved else "unknown",
|
||||
"evidenced": len(evidenced),
|
||||
"evidenced_ratio": round(len(evidenced) / len(resolved), 4) if resolved else None,
|
||||
"proxies": proxies,
|
||||
}
|
||||
|
||||
|
||||
def decision_grade(values) -> dict:
|
||||
"""Grade a decision by the weakest evidence it actually rests on.
|
||||
# Decision grade per tier, from the catalog's decision_grades.
|
||||
_GRADE_BY_TIER = {
|
||||
"observed": "evidenced",
|
||||
"quoted": "evidenced",
|
||||
"projected": "projected",
|
||||
"judgement": "indicative",
|
||||
"unknown": "insufficient",
|
||||
}
|
||||
_GRADE_NOTE = {
|
||||
"evidenced": "every load-bearing value is observed, invoiced, or quoted",
|
||||
"projected": "the conclusion rests on values projected from observations",
|
||||
"indicative": "the conclusion is no stronger than an estimated or assumed value",
|
||||
"insufficient": "at least one load-bearing value is unknown",
|
||||
}
|
||||
|
||||
|
||||
def decision_grade(values, catalog: dict | None = None) -> dict:
|
||||
"""Grade a decision by the weakest tier it actually rests on.
|
||||
|
||||
A conclusion is not stronger than its weakest load-bearing input, however
|
||||
precise the arithmetic between them looks.
|
||||
"""
|
||||
summary = profile(values)
|
||||
basis = summary["weakest"]
|
||||
if basis == "unknown":
|
||||
grade, note = "insufficient", "at least one load-bearing value is unknown"
|
||||
elif is_evidenced(basis):
|
||||
grade, note = "evidenced", "every load-bearing value is observed, invoiced, or quoted"
|
||||
elif basis == "projected":
|
||||
grade, note = "projected", "the conclusion rests on values projected from observations"
|
||||
else:
|
||||
grade, note = "indicative", f"the conclusion is no stronger than an {basis} value" if basis[0] in "aeiou" else f"the conclusion is no stronger than a {basis} value"
|
||||
cat = _catalog(catalog)
|
||||
summary = profile(values, cat)
|
||||
tier = summary["weakest_tier"]
|
||||
grade = _GRADE_BY_TIER[tier]
|
||||
note = _GRADE_NOTE[grade]
|
||||
if summary["proxies"]:
|
||||
note += f"; {len(summary['proxies'])} value(s) measure a proxy rather than the quantity named"
|
||||
return {**summary, "grade": grade, "note": note}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue