fix: correct the P-row misuse and the basis tier ordering
Both from info-tech-canon's review of the restatement. STRAIN 1 — credential custody was recorded as one unit of class P consumption. P is purchased platform capacity, and using security.secrets buys none. Removed the row; the relationship now sits in provisions[].uses_provisions alongside the object-store dependency, marked explicitly as a proposed extension because ITC-CAP declares no provision-to-provision relation. Filed as info-tech-canon/demand/ProvisionRelationships.md (their commit ce17dc4). BASIS TIERS — their point about invoiced being a fin-hub fact we name rather than originate exposed a real bug: a strict list order made weakest(["invoiced", "measured"]) return "measured", implying an invoice outranks a measurement. It does not outside its own domain. Strength is now a tier — invoiced and measured are peers, quoted below both — with ties broken deterministically by catalog order without implying a difference that does not exist. Also filed info-tech-canon/demand/EvidenceBasis.md at their request, proposing ITC-GOV as owner rather than ITC-CAP. 187 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
13c2b82281
commit
b8081f6c2d
3 changed files with 146 additions and 42 deletions
|
|
@ -11,7 +11,6 @@
|
|||
"created_at": "2026-08-15T00:00:00Z",
|
||||
"subject": "PostgreSQL backup for rapp-postgres/platform-pg, restated in canon terms",
|
||||
"note": "Real provisioned resource. Every value carries an evidence basis; a derived value is only as strong as its weakest input.",
|
||||
|
||||
"requires": [
|
||||
{
|
||||
"consumer": "rapp-postgres/platform-pg",
|
||||
|
|
@ -19,24 +18,38 @@
|
|||
"profile": "database",
|
||||
"minimum_maturity": "D5",
|
||||
"targets": {
|
||||
"rpo": { "value": 5, "unit": "minutes" },
|
||||
"rto": { "value": 60, "unit": "minutes" },
|
||||
"retention": { "value": 30, "unit": "days" }
|
||||
"rpo": {
|
||||
"value": 5,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"rto": {
|
||||
"value": 60,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"retention": {
|
||||
"value": 30,
|
||||
"unit": "days"
|
||||
}
|
||||
},
|
||||
"constraints": [
|
||||
{
|
||||
"dimension": "geographical_separation",
|
||||
"predicate": "not_in",
|
||||
"of": [
|
||||
{ "kind": "host", "id": "railiance01" },
|
||||
{ "kind": "provider", "id": "host-europe" }
|
||||
{
|
||||
"kind": "host",
|
||||
"id": "railiance01"
|
||||
},
|
||||
{
|
||||
"kind": "provider",
|
||||
"id": "host-europe"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"requirement_note": "The constraint is the one that decided the procurement: the backup must not share a failure domain with the host it protects. Before ITC-CAP 0.2.0 this lived only in prose."
|
||||
}
|
||||
],
|
||||
|
||||
"provisions": [
|
||||
{
|
||||
"provider": "resource:platform:audit-storage",
|
||||
|
|
@ -50,7 +63,10 @@
|
|||
{
|
||||
"class": "S",
|
||||
"name": "stored objects",
|
||||
"quantity": { "value": 0.0066, "unit": "GB" },
|
||||
"quantity": {
|
||||
"value": 0.0066,
|
||||
"unit": "GB"
|
||||
},
|
||||
"period": "month",
|
||||
"basis": "measured",
|
||||
"evidence": "docs/evidence/RESOURCE-WP-0002-T05-restore-2026-08-14.json#archive",
|
||||
|
|
@ -60,7 +76,10 @@
|
|||
{
|
||||
"class": "N",
|
||||
"name": "restore egress",
|
||||
"quantity": { "value": null, "unit": "GB" },
|
||||
"quantity": {
|
||||
"value": null,
|
||||
"unit": "GB"
|
||||
},
|
||||
"period": "month",
|
||||
"basis": "unknown",
|
||||
"gap": "two scratch restores read from the bucket but bytes transferred were not recorded (owner: resource-control, add to the next drill)"
|
||||
|
|
@ -68,7 +87,10 @@
|
|||
{
|
||||
"class": "P",
|
||||
"name": "object-store service",
|
||||
"quantity": { "value": 1, "unit": "unit" },
|
||||
"quantity": {
|
||||
"value": 1,
|
||||
"unit": "unit"
|
||||
},
|
||||
"period": "month",
|
||||
"supply": "external",
|
||||
"basis": "quoted",
|
||||
|
|
@ -78,7 +100,10 @@
|
|||
{
|
||||
"class": "H",
|
||||
"name": "object-store operation",
|
||||
"quantity": { "value": 1.0, "unit": "hour" },
|
||||
"quantity": {
|
||||
"value": 1.0,
|
||||
"unit": "hour"
|
||||
},
|
||||
"period": "month",
|
||||
"supply": "internal",
|
||||
"basis": "assumed",
|
||||
|
|
@ -88,7 +113,10 @@
|
|||
{
|
||||
"class": "I",
|
||||
"name": "intelligence",
|
||||
"quantity": { "value": null, "unit": "token" },
|
||||
"quantity": {
|
||||
"value": null,
|
||||
"unit": "token"
|
||||
},
|
||||
"period": "month",
|
||||
"basis": "unknown",
|
||||
"gap": "resource-control does not yet meter token consumption against a provision (owner: resource-control)"
|
||||
|
|
@ -102,7 +130,6 @@
|
|||
"environment": "production",
|
||||
"maturity": "D4",
|
||||
"implements": "CloudNativePG 1.25 / Barman, PostgreSQL 16.10",
|
||||
"depends_on_provision": "resource:platform:audit-storage provides data.object",
|
||||
"maturity_rationale": "D4 and not D5. The requirement asks for D5 (reliability measured and actively controlled). Thresholds are declared in data/thresholds/platform-audit-storage.json, but the evidence behind them is one backup, one full restore, one PITR, and about four hours of operation. wal_archive_gap_minutes is declared and not yet emitted. Reliability is therefore claimed and not yet substantiated. The requirement is not met; the gap is stated rather than the maturity inflated.",
|
||||
"evidence": [
|
||||
{
|
||||
|
|
@ -139,35 +166,45 @@
|
|||
{
|
||||
"class": "H",
|
||||
"name": "backup operation",
|
||||
"quantity": { "value": null, "unit": "hour" },
|
||||
"quantity": {
|
||||
"value": null,
|
||||
"unit": "hour"
|
||||
},
|
||||
"period": "month",
|
||||
"supply": "internal",
|
||||
"basis": "unknown",
|
||||
"gap": "real operator time was spent on procurement, credential custody, and two restore drills in this period, but no time record was kept (owner: resource-control)"
|
||||
},
|
||||
{
|
||||
"class": "P",
|
||||
"name": "credential custody",
|
||||
"quantity": { "value": 1, "unit": "unit" },
|
||||
"period": "month",
|
||||
"supply": "internal",
|
||||
"basis": "measured",
|
||||
"evidence": "OpenBao ClusterSecretStore openbao-backup-object-storage, ExternalSecret synced to databases/platform-pg-backup-s3",
|
||||
"observed_at": "2026-08-14",
|
||||
"measurement_note": "security.secrets is a separate capability; this row records only that this provision consumes it"
|
||||
},
|
||||
{
|
||||
"class": "I",
|
||||
"name": "intelligence",
|
||||
"quantity": { "value": null, "unit": "token" },
|
||||
"quantity": {
|
||||
"value": null,
|
||||
"unit": "token"
|
||||
},
|
||||
"period": "month",
|
||||
"basis": "unknown",
|
||||
"gap": "not yet metered (owner: resource-control)"
|
||||
}
|
||||
],
|
||||
"uses_provisions": [
|
||||
{
|
||||
"capability": "data.object",
|
||||
"provider": "resource:platform:audit-storage",
|
||||
"relation": "depends_on",
|
||||
"note": "data.backup depends_on data.object in the catalog; this names which provision satisfies it."
|
||||
},
|
||||
{
|
||||
"capability": "security.secrets",
|
||||
"provider": "OpenBao / external-secrets on reef-railiance",
|
||||
"relation": "uses",
|
||||
"note": "ClusterSecretStore openbao-backup-object-storage; ExternalSecret synced to databases/platform-pg-backup-s3. Not a consumption row: no purchased platform capacity is bought here, another capability is used.",
|
||||
"evidence_basis": "measured",
|
||||
"observed_at": "2026-08-14"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"modelled_alternatives": {
|
||||
"note": "The provider comparison that produced the decision, graded by evidence basis. The euro figures look precise and are not evidenced: they are derived from quoted list prices, projected demand, and an assumed labour rate.",
|
||||
"review_period": "2026-08",
|
||||
|
|
@ -177,8 +214,18 @@
|
|||
"value": 7.35,
|
||||
"basis": "derived",
|
||||
"derived_from": [
|
||||
{ "name": "scaleway.storage_eur_per_gb_month", "value": 0.01606, "basis": "quoted", "evidence": "data/providers/object-storage.json#scaleway-standard-multi-az" },
|
||||
{ "name": "stored_gb.month12", "value": 457.968, "basis": "projected", "evidence": "data/demand/platform-audit-storage.json base scenario, month 12" }
|
||||
{
|
||||
"name": "scaleway.storage_eur_per_gb_month",
|
||||
"value": 0.01606,
|
||||
"basis": "quoted",
|
||||
"evidence": "data/providers/object-storage.json#scaleway-standard-multi-az"
|
||||
},
|
||||
{
|
||||
"name": "stored_gb.month12",
|
||||
"value": 457.968,
|
||||
"basis": "projected",
|
||||
"evidence": "data/demand/platform-audit-storage.json base scenario, month 12"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -186,8 +233,18 @@
|
|||
"value": 60.0,
|
||||
"basis": "derived",
|
||||
"derived_from": [
|
||||
{ "name": "scaleway.operator_hours_per_month", "value": 1.0, "basis": "assumed", "evidence": "data/providers/object-storage.json#scaleway-standard-multi-az" },
|
||||
{ "name": "operator_hourly_eur", "value": 60, "basis": "assumed", "evidence": "data/demand/platform-audit-storage.json scenarios[*].operator_hourly_eur" }
|
||||
{
|
||||
"name": "scaleway.operator_hours_per_month",
|
||||
"value": 1.0,
|
||||
"basis": "assumed",
|
||||
"evidence": "data/providers/object-storage.json#scaleway-standard-multi-az"
|
||||
},
|
||||
{
|
||||
"name": "operator_hourly_eur",
|
||||
"value": 60,
|
||||
"basis": "assumed",
|
||||
"evidence": "data/demand/platform-audit-storage.json scenarios[*].operator_hourly_eur"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -201,17 +258,34 @@
|
|||
"value": 90.0,
|
||||
"basis": "derived",
|
||||
"derived_from": [
|
||||
{ "name": "hetzner.operator_hours_per_month", "value": 1.5, "basis": "assumed", "evidence": "data/providers/object-storage.json#hetzner-object-storage" },
|
||||
{ "name": "operator_hourly_eur", "value": 60, "basis": "assumed", "evidence": "data/demand/platform-audit-storage.json scenarios[*].operator_hourly_eur" }
|
||||
{
|
||||
"name": "hetzner.operator_hours_per_month",
|
||||
"value": 1.5,
|
||||
"basis": "assumed",
|
||||
"evidence": "data/providers/object-storage.json#hetzner-object-storage"
|
||||
},
|
||||
{
|
||||
"name": "operator_hourly_eur",
|
||||
"value": 60,
|
||||
"basis": "assumed",
|
||||
"evidence": "data/demand/platform-audit-storage.json scenarios[*].operator_hourly_eur"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"open_items": [
|
||||
"The requirement asks for data.backup at D5; the provision is D4. Closing it needs a drill cadence, an emitted wal_archive_gap_minutes, and more than one backup.",
|
||||
"Class I consumption is unknown on both provisions. resource-control does not meter tokens against a provision yet.",
|
||||
"Class H on the data.backup provision is unknown: effort was spent and not recorded. A time record starts in 2026-09.",
|
||||
"No invoiced basis exists anywhere in this record. The first booked Scaleway cost from fin-hub (FIN-WP-0004) would be the first."
|
||||
]
|
||||
"No invoiced basis exists anywhere in this record. The first booked Scaleway cost from fin-hub (FIN-WP-0004) would be the first.",
|
||||
"provisions[].uses_provisions is a proposed extension, not canon. It replaced a consumes:P row for credential custody, which was the wrong kind: P is purchased platform capacity, and using security.secrets buys none."
|
||||
],
|
||||
"proposed_extensions": {
|
||||
"note": "uses_provisions is NOT canon. ITC-CAP 0.2.0 declares capability-to-capability relations and landscape-to-capability relations, but no provision-to-provision relation. info-tech-canon identified the gap on 2026-08-15 and asked us to file it as demand rather than have them invent the field from a message.",
|
||||
"fields": [
|
||||
"provisions[].uses_provisions"
|
||||
],
|
||||
"demand": "info-tech-canon/demand/ProvisionRelationships.md"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,16 @@ class OrderTest(unittest.TestCase):
|
|||
self.assertLess(rank("measured"), rank("estimated"))
|
||||
self.assertLess(rank("estimated"), rank("assumed"))
|
||||
|
||||
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(rank("invoiced"), rank("measured"))
|
||||
self.assertLess(rank("measured"), rank("quoted"))
|
||||
|
||||
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"]))
|
||||
|
||||
def test_weakest_and_strongest_pick_opposite_ends(self):
|
||||
bases = ["measured", "assumed", "quoted"]
|
||||
self.assertEqual("assumed", weakest(bases))
|
||||
|
|
|
|||
|
|
@ -30,31 +30,51 @@ BASIS_ORDER = (
|
|||
"unknown", # no value exists
|
||||
)
|
||||
BASES = frozenset(BASIS_ORDER)
|
||||
_RANK = {name: index for index, name in enumerate(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:
|
||||
if basis not in _RANK:
|
||||
"""Strength tier; lower is stronger. Peers share a tier."""
|
||||
if basis not in _TIER:
|
||||
raise ValueError(f"unknown evidence basis {basis!r}")
|
||||
return _RANK[basis]
|
||||
return _TIER[basis]
|
||||
|
||||
|
||||
def weakest(bases) -> str:
|
||||
"""The weakest basis in a collection. Empty means nothing is known."""
|
||||
"""The weakest basis in a collection. Empty means nothing is known.
|
||||
|
||||
Ties within a tier resolve by catalog order so the result is deterministic
|
||||
without implying a strength difference that does not exist.
|
||||
"""
|
||||
bases = list(bases)
|
||||
if not bases:
|
||||
return "unknown"
|
||||
return max(bases, key=rank)
|
||||
return min(bases, key=lambda b: (-rank(b), _ORDER[b]))
|
||||
|
||||
|
||||
def strongest(bases) -> str:
|
||||
bases = list(bases)
|
||||
if not bases:
|
||||
return "unknown"
|
||||
return min(bases, key=rank)
|
||||
return min(bases, key=lambda b: (rank(b), _ORDER[b]))
|
||||
|
||||
|
||||
def is_evidenced(basis: str) -> bool:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue