diff --git a/data/capability/platform-audit-storage.json b/data/capability/platform-audit-storage.json index 26ed2a1..dda29ce 100644 --- a/data/capability/platform-audit-storage.json +++ b/data/capability/platform-audit-storage.json @@ -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" + } } diff --git a/tests/test_basis.py b/tests/test_basis.py index f70b31c..d4c608b 100644 --- a/tests/test_basis.py +++ b/tests/test_basis.py @@ -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)) diff --git a/tools/basis.py b/tools/basis.py index 9cd259a..0bf0bcc 100644 --- a/tools/basis.py +++ b/tools/basis.py @@ -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: