feat(portfolio): fold in RAILIANCE-WP-0016 apps-pg evidence
First delegated evidence from RESOURCE-WP-0003-T04 to land. railiance-platform delivered apps-pg capacity, utilization, consumers, and the apps-pg-dbbytes-v1 allocation driver, and correctly delivered no EUR. - data/resources/apps-pg.json: real capacity; allocation unattributed -> shared under apps-pg-dbbytes-v1; second consumer vergabe-teilnahme registered - data/control-cycle/apps-pg-2026-09-base.json: first operational control-cycle record in the repository - examples/control-cycle/apps-pg-*.json retired; the invented fixture collided with the real record's identifier - data/portfolio-coverage-2026-08-14.json: gap marked delivered with three residual unknowns still open The real evidence exposed a design gap in the T05 schema: v0.1 required a number for every cost field, so recording genuine usage without a booked cost meant inventing one. Schema 0.2 permits null costs, null unattributed_eur, a technical unattributed_share, and null measurements. Null is unknown, never zero; an unknown component makes the total null rather than the sum of the known parts; and the comparator classifies unknown amounts as data_quality instead of computing a variance. Existing 0.1 records are not rewritten. apps-pg is now measured (idle at 5.8% of volume) and attributed, and remains unpriced: delivered technical evidence does not create a booked cost. 86 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b64b5683df
commit
17de8b831e
16 changed files with 908 additions and 71 deletions
|
|
@ -10,7 +10,7 @@
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| workplan | RESOURCE-WP-0001 | finished | — | workplans/RESOURCE-WP-0001-statehub-bootstrap.md |
|
| workplan | RESOURCE-WP-0001 | finished | — | workplans/RESOURCE-WP-0001-statehub-bootstrap.md |
|
||||||
| workplan | RESOURCE-WP-0002 | active | — | workplans/RESOURCE-WP-0002-procure-postgres-backup-storage.md |
|
| workplan | RESOURCE-WP-0002 | active | — | workplans/RESOURCE-WP-0002-procure-postgres-backup-storage.md |
|
||||||
| workplan | RESOURCE-WP-0003 | active | — | workplans/RESOURCE-WP-0003-managed-infrastructure-portfolio-control.md |
|
| workplan | RESOURCE-WP-0003 | finished | — | workplans/RESOURCE-WP-0003-managed-infrastructure-portfolio-control.md |
|
||||||
| task | RESOURCE-WP-0001-T01 | done | — | workplans/RESOURCE-WP-0001-statehub-bootstrap.md |
|
| task | RESOURCE-WP-0001-T01 | done | — | workplans/RESOURCE-WP-0001-statehub-bootstrap.md |
|
||||||
| task | RESOURCE-WP-0001-T02 | done | — | workplans/RESOURCE-WP-0001-statehub-bootstrap.md |
|
| task | RESOURCE-WP-0001-T02 | done | — | workplans/RESOURCE-WP-0001-statehub-bootstrap.md |
|
||||||
| task | RESOURCE-WP-0001-T03 | done | — | workplans/RESOURCE-WP-0001-statehub-bootstrap.md |
|
| task | RESOURCE-WP-0001-T03 | done | — | workplans/RESOURCE-WP-0001-statehub-bootstrap.md |
|
||||||
|
|
@ -26,5 +26,5 @@
|
||||||
| task | RESOURCE-WP-0003-T03 | done | — | workplans/RESOURCE-WP-0003-managed-infrastructure-portfolio-control.md |
|
| task | RESOURCE-WP-0003-T03 | done | — | workplans/RESOURCE-WP-0003-managed-infrastructure-portfolio-control.md |
|
||||||
| task | RESOURCE-WP-0003-T04 | done | — | workplans/RESOURCE-WP-0003-managed-infrastructure-portfolio-control.md |
|
| task | RESOURCE-WP-0003-T04 | done | — | workplans/RESOURCE-WP-0003-managed-infrastructure-portfolio-control.md |
|
||||||
| task | RESOURCE-WP-0003-T05 | done | — | workplans/RESOURCE-WP-0003-managed-infrastructure-portfolio-control.md |
|
| task | RESOURCE-WP-0003-T05 | done | — | workplans/RESOURCE-WP-0003-managed-infrastructure-portfolio-control.md |
|
||||||
| task | RESOURCE-WP-0003-T06 | todo | — | workplans/RESOURCE-WP-0003-managed-infrastructure-portfolio-control.md |
|
| task | RESOURCE-WP-0003-T06 | done | — | workplans/RESOURCE-WP-0003-managed-infrastructure-portfolio-control.md |
|
||||||
| task | RESOURCE-WP-0003-T07 | todo | — | workplans/RESOURCE-WP-0003-managed-infrastructure-portfolio-control.md |
|
| task | RESOURCE-WP-0003-T07 | done | — | workplans/RESOURCE-WP-0003-managed-infrastructure-portfolio-control.md |
|
||||||
|
|
|
||||||
69
data/control-cycle/apps-pg-2026-09-base.json
Normal file
69
data/control-cycle/apps-pg-2026-09-base.json
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"schema_version": "0.2",
|
||||||
|
"record_id": "forecast:apps-pg:2026-09:base:v1",
|
||||||
|
"record_type": "forecast",
|
||||||
|
"resource_id": "resource:railiance:apps-pg",
|
||||||
|
"resource_class": "shared_platform_service",
|
||||||
|
"period": "2026-09",
|
||||||
|
"created_at": "2026-08-14T00:00:00Z",
|
||||||
|
"scenario": "base",
|
||||||
|
"forecast_ref": null,
|
||||||
|
"revision_of": null,
|
||||||
|
"usage_proxies": {
|
||||||
|
"consumer_database_gb": { "value": 0.0155, "unit": "GiB" },
|
||||||
|
"pgdata_gb": { "value": 0.5766, "unit": "GiB" },
|
||||||
|
"wal_gb": { "value": 0.5469, "unit": "GiB" },
|
||||||
|
"sessions": { "value": 7, "unit": "count" }
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"model": "hybrid",
|
||||||
|
"provisioned": {
|
||||||
|
"storage": { "value": 10, "unit": "GiB" },
|
||||||
|
"instances": { "value": 1, "unit": "count" },
|
||||||
|
"max_connections": { "value": 100, "unit": "count" }
|
||||||
|
},
|
||||||
|
"used": {
|
||||||
|
"storage": { "value": 0.5766, "unit": "GiB" },
|
||||||
|
"instances": { "value": 1, "unit": "count" },
|
||||||
|
"sessions": { "value": 7, "unit": "count" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"costs": {
|
||||||
|
"currency": "EUR",
|
||||||
|
"infrastructure": null,
|
||||||
|
"internal_labor": 30.0,
|
||||||
|
"external_labor": 0,
|
||||||
|
"total": null,
|
||||||
|
"booked_cost_refs": []
|
||||||
|
},
|
||||||
|
"allocation": {
|
||||||
|
"method": "proportional",
|
||||||
|
"driver": "database_gb",
|
||||||
|
"method_version": "apps-pg-dbbytes-v1",
|
||||||
|
"unattributed_eur": null,
|
||||||
|
"unattributed_share": 0.948
|
||||||
|
},
|
||||||
|
"service_constraints": {
|
||||||
|
"rpo_minutes": { "value": null, "unit": "minutes" },
|
||||||
|
"rto_minutes": { "value": null, "unit": "minutes" }
|
||||||
|
},
|
||||||
|
"uncertainty": {
|
||||||
|
"level": "high",
|
||||||
|
"notes": [
|
||||||
|
"First operational control-cycle record for this resource, from railiance-platform RAILIANCE-WP-0016 captured 2026-08-14T00:04:19Z",
|
||||||
|
"Infrastructure cost is null: apps-pg is a share of resource:hosteurope:railiance01, whose booked price is unknown (RAIL-HO-WP-0008)",
|
||||||
|
"Internal labour is 0.5 declared operator-hours per month converted at the EUR 60 per hour rate used in data/demand/platform-audit-storage.json; the hours are an estimate, not a timesheet",
|
||||||
|
"Total is null because one component is unknown; it is not the sum of the known parts",
|
||||||
|
"94.8 percent of used volume is WAL and engine catalogs attributed to platform:apps-pg-overhead, not to a consumer",
|
||||||
|
"vergabe_db has never recorded a transaction, so its 0.4614 byte share reflects a reserved empty database rather than realised demand",
|
||||||
|
"Five days of cluster life and no metrics retention: this is a snapshot carried forward, not a trend",
|
||||||
|
"RPO is unbounded and RTO undefined — no ScheduledBackup exists and the cluster is not an Option A logical-dump target"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"evidence": [
|
||||||
|
"/home/worsch/railiance-platform/docs/evidence/RAILIANCE-WP-0016-apps-pg-observation.json",
|
||||||
|
"/home/worsch/railiance-platform/docs/evidence/RAILIANCE-WP-0016-apps-pg-resource-evidence.md",
|
||||||
|
"RAILIANCE-WP-0016",
|
||||||
|
"data/resources/apps-pg.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
115
data/portfolio-coverage-2026-08-14.json
Normal file
115
data/portfolio-coverage-2026-08-14.json
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
{
|
||||||
|
"schema_version": "0.1",
|
||||||
|
"observed_at": "2026-08-14T00:04:19Z",
|
||||||
|
"inventory_records": 7,
|
||||||
|
"coverage": [
|
||||||
|
{
|
||||||
|
"group": "helix-forge",
|
||||||
|
"status": "covered-by-shared-forge",
|
||||||
|
"resource_ids": [
|
||||||
|
"resource:railiance:forgejo",
|
||||||
|
"resource:railiance:reef-railiance:k3s",
|
||||||
|
"resource:hosteurope:railiance01"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "coulomb-social",
|
||||||
|
"status": "covered",
|
||||||
|
"resource_ids": [
|
||||||
|
"resource:tenant:coulomb:coulomb-social",
|
||||||
|
"resource:railiance:apps-pg",
|
||||||
|
"resource:railiance:reef-railiance:k3s",
|
||||||
|
"resource:hosteurope:railiance01"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "shared-railiance",
|
||||||
|
"status": "covered-substrate-and-selected-services",
|
||||||
|
"resource_ids": [
|
||||||
|
"resource:railiance:reef-railiance:k3s",
|
||||||
|
"resource:railiance:forgejo",
|
||||||
|
"resource:railiance:apps-pg",
|
||||||
|
"resource:hosteurope:railiance01",
|
||||||
|
"resource:platform:audit-storage"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "representative-tenant",
|
||||||
|
"status": "covered",
|
||||||
|
"tenant_id": "tenant:friendly:binky",
|
||||||
|
"resource_ids": [
|
||||||
|
"resource:tenant:friendly:binky:rapp-qonto",
|
||||||
|
"resource:railiance:reef-railiance:k3s",
|
||||||
|
"resource:hosteurope:railiance01"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"owned_gaps": [
|
||||||
|
{
|
||||||
|
"owner": "railiance-infra",
|
||||||
|
"gap": "Host Europe product, provider resource ID, account reference, country/region evidence, contract price, tax treatment, order date, renewal and cancellation dates",
|
||||||
|
"delegated_workplan": "RAIL-HO-WP-0008",
|
||||||
|
"state_hub_workplan_id": "7122657f-87c8-46b5-a725-a1af1ba0af12",
|
||||||
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "railiance-cluster",
|
||||||
|
"gap": "cluster-wide requested/used capacity history, allocation driver, generic persistent-storage contract, and independent failure domains",
|
||||||
|
"delegated_workplan": "RAIL-BS-WP-0014",
|
||||||
|
"state_hub_workplan_id": "ea6ec98a-0d65-4afa-b6c5-6e7ed34011ac",
|
||||||
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "railiance-forge",
|
||||||
|
"gap": "Forgejo resource requests/utilization, storage growth, operations labor, and cost-allocation driver",
|
||||||
|
"delegated_workplan": "RAILIANCE-WP-0002",
|
||||||
|
"state_hub_workplan_id": "72f935a5-d921-48a4-96e4-2365556c7374",
|
||||||
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "railiance-platform",
|
||||||
|
"gap": "apps-pg utilization, backup/restore evidence, operations labor, and consumer allocation driver",
|
||||||
|
"delegated_workplan": "RAILIANCE-WP-0016",
|
||||||
|
"state_hub_workplan_id": "49084fb8-de63-4f32-a4a9-3a42d4e708ac",
|
||||||
|
"status": "delivered",
|
||||||
|
"delivered_on": "2026-08-14",
|
||||||
|
"interface": [
|
||||||
|
"/home/worsch/railiance-platform/docs/evidence/RAILIANCE-WP-0016-apps-pg-observation.json",
|
||||||
|
"/home/worsch/railiance-platform/docs/evidence/RAILIANCE-WP-0016-apps-pg-resource-evidence.md"
|
||||||
|
],
|
||||||
|
"refresh": "make apps-pg-evidence",
|
||||||
|
"residual_unknowns": [
|
||||||
|
"booked EUR for the apps-pg share of railiance01 (owner: fin-hub, FIN-WP-0004; upstream RAIL-HO-WP-0008)",
|
||||||
|
"operator hours are an estimate, not a timesheet (owner: railiance-platform)",
|
||||||
|
"no restore drill, no ScheduledBackup: RPO unbounded and RTO undefined (owner: railiance-platform)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "coulomb-social",
|
||||||
|
"gap": "declared CPU/memory demand, usage proxies, service objectives, and application operations labor",
|
||||||
|
"delegated_workplan": "CSOC-WP-0005",
|
||||||
|
"state_hub_workplan_id": "00de1578-6150-40d3-b193-e852666ed945",
|
||||||
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "rapp-qonto",
|
||||||
|
"gap": "request/cold-start frequency, external API costs, runtime utilization, and recurring operator labor",
|
||||||
|
"delegated_workplan": "RAPP-QONTO-WP-0002",
|
||||||
|
"state_hub_workplan_id": "25b3b715-9a56-499e-a85c-a09ebd9bbb79",
|
||||||
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "fin-hub",
|
||||||
|
"gap": "booked Host Europe financial facts joined to resource:hosteurope:railiance01",
|
||||||
|
"delegated_workplan": "FIN-WP-0004",
|
||||||
|
"state_hub_workplan_id": "67b6de6c-4820-4478-9789-f50260204c27",
|
||||||
|
"delegated_tasks": [
|
||||||
|
"FIN-WP-0004-T04",
|
||||||
|
"FIN-WP-0004-T05",
|
||||||
|
"FIN-WP-0004-T06"
|
||||||
|
],
|
||||||
|
"status": "open"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supersedes": "data/portfolio-coverage-2026-08-11.json"
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,162 @@
|
||||||
{
|
{
|
||||||
"schema_version": "0.2", "record_scope": "inventory",
|
"schema_version": "0.2",
|
||||||
"id": "resource:railiance:apps-pg", "resource_class": "database",
|
"record_scope": "inventory",
|
||||||
"status": "active", "management_model": "self_managed",
|
"id": "resource:railiance:apps-pg",
|
||||||
"provider": {"name": "Railiance", "account_ref": null, "product_ref": "cloudnative-pg", "provider_resource_id": "kubernetes:databases/cluster/apps-pg"},
|
"resource_class": "database",
|
||||||
"service": {"name": "Shared applications PostgreSQL", "service_id": "apps-pg", "class": "CloudNativePG database service", "capacity_model": "shared"},
|
"status": "active",
|
||||||
"location": {"region": null, "country": null, "failure_domains": ["cluster:reef-railiance", "host:railiance01", "storage:local-path"], "residency": null},
|
"management_model": "self_managed",
|
||||||
"capacity": [{"metric": "instances", "value": 1, "unit": "count", "kind": "observed", "observed_at": "2026-08-11"}, {"metric": "storage", "value": 10, "unit": "GiB", "kind": "provisioned", "observed_at": "2026-08-11"}],
|
"provider": {
|
||||||
"ownership": {"owner": "railiance-platform", "environment": "production", "workload_ids": ["coulomb-social"], "tenant_id": null, "allocation": {"mode": "unattributed", "cost_attribution_key": null, "driver": null, "method_version": null}},
|
"name": "Railiance",
|
||||||
"cost": {"currency": "EUR", "tax_status": "not_applicable", "billing_model": "shared cluster and operations cost; allocation method unknown", "commitment_ref": null, "price_evidence": null},
|
"account_ref": null,
|
||||||
"lifecycle": {"proposed_on": null, "ordered_on": null, "commissioned_on": "2026-08-09", "renews_on": null, "cancel_by": null, "retired_on": null, "exit_path": "Restore databases to a replacement PostgreSQL service, rotate application connections, verify consumers, then retire the cluster and volume."},
|
"product_ref": "cloudnative-pg",
|
||||||
"relationships": [{"type": "hosted_on", "resource_id": "resource:railiance:reef-railiance:k3s"}],
|
"provider_resource_id": "kubernetes:databases/cluster/apps-pg"
|
||||||
"requirements": [{"kind": "recovery", "ref": "/home/worsch/railiance-platform/docs/apps-pg.md"}],
|
},
|
||||||
"evidence": [{"kind": "telemetry", "ref": "kubernetes:reef-railiance/databases/cluster/apps-pg@2026-08-11", "observed_at": "2026-08-11", "authority": "reef-railiance Kubernetes API"}, {"kind": "workload", "ref": "/home/worsch/railiance-platform/helm/apps-pg-cluster.yaml", "observed_at": "2026-08-11", "authority": "railiance-platform"}]
|
"service": {
|
||||||
|
"name": "Shared applications PostgreSQL",
|
||||||
|
"service_id": "apps-pg",
|
||||||
|
"class": "CloudNativePG database service",
|
||||||
|
"capacity_model": "shared"
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"region": null,
|
||||||
|
"country": null,
|
||||||
|
"failure_domains": [
|
||||||
|
"cluster:reef-railiance",
|
||||||
|
"host:railiance01",
|
||||||
|
"storage:local-path"
|
||||||
|
],
|
||||||
|
"residency": null
|
||||||
|
},
|
||||||
|
"capacity": [
|
||||||
|
{
|
||||||
|
"metric": "instances",
|
||||||
|
"value": 1,
|
||||||
|
"unit": "count",
|
||||||
|
"kind": "provisioned",
|
||||||
|
"observed_at": "2026-08-14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metric": "instances",
|
||||||
|
"value": 1,
|
||||||
|
"unit": "count",
|
||||||
|
"kind": "observed",
|
||||||
|
"observed_at": "2026-08-14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metric": "storage",
|
||||||
|
"value": 10,
|
||||||
|
"unit": "GiB",
|
||||||
|
"kind": "provisioned",
|
||||||
|
"observed_at": "2026-08-14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metric": "storage_used",
|
||||||
|
"value": 0.5766,
|
||||||
|
"unit": "GiB",
|
||||||
|
"kind": "observed",
|
||||||
|
"observed_at": "2026-08-14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metric": "wal_since_init",
|
||||||
|
"value": 0.5469,
|
||||||
|
"unit": "GiB",
|
||||||
|
"kind": "observed",
|
||||||
|
"observed_at": "2026-08-14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metric": "cpu_usage",
|
||||||
|
"value": 0.009,
|
||||||
|
"unit": "vCPU",
|
||||||
|
"kind": "observed",
|
||||||
|
"observed_at": "2026-08-14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metric": "memory_usage",
|
||||||
|
"value": 0.0996,
|
||||||
|
"unit": "GiB",
|
||||||
|
"kind": "observed",
|
||||||
|
"observed_at": "2026-08-14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metric": "max_connections",
|
||||||
|
"value": 100,
|
||||||
|
"unit": "count",
|
||||||
|
"kind": "limit",
|
||||||
|
"observed_at": "2026-08-14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metric": "sessions",
|
||||||
|
"value": 7,
|
||||||
|
"unit": "count",
|
||||||
|
"kind": "observed",
|
||||||
|
"observed_at": "2026-08-14"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ownership": {
|
||||||
|
"owner": "railiance-platform",
|
||||||
|
"environment": "production",
|
||||||
|
"workload_ids": [
|
||||||
|
"coulomb-social",
|
||||||
|
"vergabe-teilnahme"
|
||||||
|
],
|
||||||
|
"tenant_id": null,
|
||||||
|
"allocation": {
|
||||||
|
"mode": "shared",
|
||||||
|
"cost_attribution_key": "platform:apps-pg",
|
||||||
|
"driver": "database_gb",
|
||||||
|
"method_version": "apps-pg-dbbytes-v1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"currency": "EUR",
|
||||||
|
"tax_status": "not_applicable",
|
||||||
|
"billing_model": "shared cluster and operations cost allocated by declared-consumer database bytes (apps-pg-dbbytes-v1: coulomb-social 0.5386, vergabe-teilnahme 0.4614); the 94.8 percent WAL and catalog residual is attributed to platform:apps-pg-overhead and no booked cost exists yet",
|
||||||
|
"commitment_ref": null,
|
||||||
|
"price_evidence": null
|
||||||
|
},
|
||||||
|
"lifecycle": {
|
||||||
|
"proposed_on": null,
|
||||||
|
"ordered_on": null,
|
||||||
|
"commissioned_on": "2026-08-09",
|
||||||
|
"renews_on": null,
|
||||||
|
"cancel_by": null,
|
||||||
|
"retired_on": null,
|
||||||
|
"exit_path": "Restore databases to a replacement PostgreSQL service, rotate application connections, verify consumers, then retire the cluster and volume."
|
||||||
|
},
|
||||||
|
"relationships": [
|
||||||
|
{
|
||||||
|
"type": "hosted_on",
|
||||||
|
"resource_id": "resource:railiance:reef-railiance:k3s"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requirements": [
|
||||||
|
{
|
||||||
|
"kind": "recovery",
|
||||||
|
"ref": "/home/worsch/railiance-platform/docs/evidence/RAILIANCE-WP-0016-apps-pg-resource-evidence.md#t02--recovery-and-operations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "other",
|
||||||
|
"ref": "/home/worsch/railiance-platform/docs/apps-pg.md"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"evidence": [
|
||||||
|
{
|
||||||
|
"kind": "telemetry",
|
||||||
|
"ref": "/home/worsch/railiance-platform/docs/evidence/RAILIANCE-WP-0016-apps-pg-observation.json",
|
||||||
|
"observed_at": "2026-08-14",
|
||||||
|
"authority": "railiance-platform (reef-railiance Kubernetes API + postgres catalog)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "workload",
|
||||||
|
"ref": "/home/worsch/railiance-platform/docs/evidence/RAILIANCE-WP-0016-apps-pg-resource-evidence.md",
|
||||||
|
"observed_at": "2026-08-14",
|
||||||
|
"authority": "railiance-platform, RAILIANCE-WP-0016"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "workload",
|
||||||
|
"ref": "/home/worsch/railiance-platform/helm/apps-pg-cluster.yaml",
|
||||||
|
"observed_at": "2026-08-11",
|
||||||
|
"authority": "railiance-platform"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
# RESOURCE-WP-0003 apps-pg evidence fold-in — 2026-08-14
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
`railiance-platform` finished `RAILIANCE-WP-0016` and published the apps-pg
|
||||||
|
evidence that `RESOURCE-WP-0003-T04` delegated. This is the first delegated
|
||||||
|
evidence to land, and folding it in exercised the whole loop end to end:
|
||||||
|
delegation, delivery, ingestion, allocation, control record, and report.
|
||||||
|
|
||||||
|
Source, captured 2026-08-14T00:04:19Z, refreshed with `make apps-pg-evidence`:
|
||||||
|
|
||||||
|
- `/home/worsch/railiance-platform/docs/evidence/RAILIANCE-WP-0016-apps-pg-observation.json`
|
||||||
|
- `/home/worsch/railiance-platform/docs/evidence/RAILIANCE-WP-0016-apps-pg-resource-evidence.md`
|
||||||
|
|
||||||
|
## What changed here
|
||||||
|
|
||||||
|
| Artifact | Change |
|
||||||
|
|---|---|
|
||||||
|
| `data/resources/apps-pg.json` | Real capacity and utilization; allocation moved from `unattributed` to `shared` under `apps-pg-dbbytes-v1`; second consumer `vergabe-teilnahme` registered; evidence repointed at the platform interface |
|
||||||
|
| `data/control-cycle/apps-pg-2026-09-base.json` | **New.** First operational control-cycle record in the repository |
|
||||||
|
| `examples/control-cycle/apps-pg-{forecast,actual}.json` | **Retired.** The invented fixture collided with the real record's identifier — the clearest possible signal it had been superseded |
|
||||||
|
| `schemas/resource-control-cycle.schema.json` | Widened to 0.2: nullable costs, nullable `unattributed_eur`, new `unattributed_share`, nullable measurement values |
|
||||||
|
| `tools/control_cycle.py` | Unknown amounts return `status: "unknown"` and classify as `data_quality` instead of computing a variance |
|
||||||
|
| `data/portfolio-coverage-2026-08-14.json` | **New.** Supersedes the 2026-08-11 observation; the railiance-platform gap is `delivered` with three residual unknowns |
|
||||||
|
| `tools/portfolio_report.py` | Delivered gaps stay visible with their residuals; `storage`/`storage_used` added to the measurable pairs |
|
||||||
|
|
||||||
|
## The schema gap the real evidence exposed
|
||||||
|
|
||||||
|
railiance-platform delivered technical facts and, correctly, no EUR — the host
|
||||||
|
bill belongs to `resource:hosteurope:railiance01`, whose price is unknown, and
|
||||||
|
they wrote plainly: "resource-control may convert hours at its own labor rate.
|
||||||
|
Do not treat these hours as booked cost."
|
||||||
|
|
||||||
|
Schema 0.1 could not represent that. Every cost field required a number, so the
|
||||||
|
only way to record real usage was to invent a cost — precisely what this
|
||||||
|
repository refuses to do everywhere else. That is a genuine design gap, found
|
||||||
|
by real evidence rather than by review.
|
||||||
|
|
||||||
|
Schema 0.2 permits `null` for cost components, `unattributed_eur`, and
|
||||||
|
measurement values, and adds `unattributed_share` so a residual can be recorded
|
||||||
|
from technical evidence alone. `null` means unknown, never zero; if any
|
||||||
|
component is unknown the total is `null` rather than the sum of the known
|
||||||
|
parts; and the comparator classifies an unknown amount as `data_quality`,
|
||||||
|
overriding any explicit attribution, because a missing amount is an evidence
|
||||||
|
gap and not a price movement.
|
||||||
|
|
||||||
|
Existing 0.1 records were not rewritten. `schema_version` accepts both:
|
||||||
|
immutability applies to the record, not to the schema.
|
||||||
|
|
||||||
|
## The first operational record
|
||||||
|
|
||||||
|
`forecast:apps-pg:2026-09:base:v1` carries what is known and refuses the rest:
|
||||||
|
|
||||||
|
| Field | Value | Basis |
|
||||||
|
|---|---|---|
|
||||||
|
| `consumer_database_gb` | 0.0155 GiB | `pg_database_size` of the two declared consumer databases |
|
||||||
|
| `pgdata_gb` | 0.5766 GiB | `du -sb` on PGDATA |
|
||||||
|
| storage used / provisioned | 0.5766 of 10 GiB | PVC `apps-pg-1`, `local-path` |
|
||||||
|
| `internal_labor` | EUR 30.00 | 0.5 declared operator-hours at the EUR 60/hour rate already used in `data/demand/platform-audit-storage.json` |
|
||||||
|
| `infrastructure` | **null** | Share of railiance01; price unknown (`RAIL-HO-WP-0008`) |
|
||||||
|
| `total` | **null** | One component unknown; not the sum of the known parts |
|
||||||
|
| `unattributed_share` | 0.948 | WAL and engine catalogs under `platform:apps-pg-overhead` |
|
||||||
|
| `rpo_minutes`, `rto_minutes` | **null** | No ScheduledBackup, no restore drill, not an Option A target |
|
||||||
|
|
||||||
|
Uncertainty is `high` and stated in eight notes, including the one that matters
|
||||||
|
most for allocation: `vergabe_db` has never recorded a transaction, so its
|
||||||
|
0.4614 byte share reflects a reserved empty database rather than realised
|
||||||
|
demand. The method is recorded as delivered; whether it survives contact with a
|
||||||
|
second month is a question for the quarterly calibration.
|
||||||
|
|
||||||
|
## Effect on the portfolio report
|
||||||
|
|
||||||
|
- apps-pg leaves `utilization.unmeasured` and enters `utilization.idle` at
|
||||||
|
5.8 percent of its volume — a third idle resource alongside railiance01 and
|
||||||
|
the k3s cluster.
|
||||||
|
- apps-pg leaves `cost.unattributed_allocation`, which drops from four
|
||||||
|
resources to three.
|
||||||
|
- apps-pg remains in `cost.unpriced`, and `known_monthly_spend_eur` remains
|
||||||
|
`null`. Delivered technical evidence does not create a booked cost.
|
||||||
|
- The railiance-platform gap moves to `delivered_gaps` and still generates
|
||||||
|
three next actions from its residual unknowns.
|
||||||
|
|
||||||
|
## Boundary held
|
||||||
|
|
||||||
|
No booked cost was originated here. The labour figure is a conversion of
|
||||||
|
declared hours at a rate this repository already publishes, recorded as an
|
||||||
|
estimate with its basis, not as a ledger fact. Recovery risk was recorded, not
|
||||||
|
resolved: apps-pg has RPO unbounded and RTO undefined on single-host local-path
|
||||||
|
storage, which stays railiance-platform's to close and is gated on the same
|
||||||
|
object-store decision as `RESOURCE-WP-0002`.
|
||||||
|
|
@ -98,10 +98,41 @@ make control-cycle FORECAST=examples/control-cycle/storage-forecast.json \
|
||||||
ACTUAL=examples/control-cycle/storage-actual.json
|
ACTUAL=examples/control-cycle/storage-actual.json
|
||||||
```
|
```
|
||||||
|
|
||||||
The examples cover storage, `reef-railiance` cluster compute, and the shared
|
The examples cover storage and `reef-railiance` cluster compute. Their numbers
|
||||||
`apps-pg` service. Their numbers are illustrative contract fixtures, not booked
|
are illustrative contract fixtures, not booked facts. Operational records
|
||||||
facts. Operational records replace their evidence references after the owner
|
replace them once the owner workplans publish observations — the illustrative
|
||||||
workplans publish observations.
|
`apps-pg` pair was retired on 2026-08-14 when `RAILIANCE-WP-0016` delivered the
|
||||||
|
real one to `data/control-cycle/apps-pg-2026-09-base.json`.
|
||||||
|
|
||||||
|
Illustrative fixtures live in `examples/control-cycle/`; operational records
|
||||||
|
live in `data/control-cycle/` and must cite the authoritative repository
|
||||||
|
evidence they came from and state their uncertainty. The validator enforces
|
||||||
|
both.
|
||||||
|
|
||||||
|
### Schema 0.2 — unknown is not zero
|
||||||
|
|
||||||
|
Real evidence arrives incomplete. `RAILIANCE-WP-0016` supplied apps-pg
|
||||||
|
capacity, utilization, consumers, and an allocation driver, and correctly
|
||||||
|
supplied no EUR: the host bill belongs to `resource:hosteurope:railiance01`,
|
||||||
|
whose price is still unknown. Schema 0.1 could not express that — every cost
|
||||||
|
field required a number, so the only way to record real usage was to invent a
|
||||||
|
cost.
|
||||||
|
|
||||||
|
Schema 0.2 permits `null` for each cost component, for `unattributed_eur`, and
|
||||||
|
for any measurement value, and adds `unattributed_share` so a shared-cost
|
||||||
|
residual can be recorded from technical evidence before any EUR exists. The
|
||||||
|
rules:
|
||||||
|
|
||||||
|
- `null` means unknown or unbounded, never zero;
|
||||||
|
- if any cost component is unknown, `total` is `null` — never the sum of the
|
||||||
|
parts that happen to be known;
|
||||||
|
- the comparator returns `status: "unknown"` for that component and classifies
|
||||||
|
it `data_quality`, overriding any explicit attribution, because a missing
|
||||||
|
amount is an evidence gap rather than a price or labour movement;
|
||||||
|
- known components still compute their variance alongside unknown ones.
|
||||||
|
|
||||||
|
Existing 0.1 records stay valid and are never rewritten. Immutability applies
|
||||||
|
to the record, not to the schema, so `schema_version` accepts both.
|
||||||
|
|
||||||
## Variance attribution
|
## Variance attribution
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,19 @@ reason, rather than being silently omitted from the ratios.
|
||||||
| **Pre-renewal review** | At least 60 days before the earliest `renews_on` or `cancel_by` | Contract evidence, utilization, open cases | Human financial authority | A `renewal` case, decided while cancellation is still possible |
|
| **Pre-renewal review** | At least 60 days before the earliest `renews_on` or `cancel_by` | Contract evidence, utilization, open cases | Human financial authority | A `renewal` case, decided while cancellation is still possible |
|
||||||
| **Event-driven** | Sustained variance beyond threshold, an incident exposing a failure domain, or a workload request | The triggering evidence | Owning repository | A case of the matching type per `docs/optimization-cases.md` |
|
| **Event-driven** | Sustained variance beyond threshold, an incident exposing a failure domain, or a workload request | The triggering evidence | Owning repository | A case of the matching type per `docs/optimization-cases.md` |
|
||||||
|
|
||||||
|
## Delivered evidence stays visible
|
||||||
|
|
||||||
|
A delegated gap that is answered does not vanish from the report. It moves from
|
||||||
|
`coverage.unresolved_gaps` to `coverage.delivered_gaps`, keeping the interface
|
||||||
|
that answered it and the **residual unknowns** it did not, and each residual
|
||||||
|
still generates a next action. Partial delivery must not read as full coverage.
|
||||||
|
|
||||||
|
`RAILIANCE-WP-0016` is the first example: railiance-platform delivered apps-pg
|
||||||
|
capacity, utilization, consumers, and the `apps-pg-dbbytes-v1` allocation
|
||||||
|
driver, and deliberately delivered no EUR. Three residuals remain open — the
|
||||||
|
booked host share, the labour estimate, and the absent recovery objectives — so
|
||||||
|
apps-pg is now measurable and attributed while still counting as unpriced.
|
||||||
|
|
||||||
## Reading the current report
|
## Reading the current report
|
||||||
|
|
||||||
As of 2026-08-14 an operator asking the four questions this cadence exists to
|
As of 2026-08-14 an operator asking the four questions this cadence exists to
|
||||||
|
|
@ -55,12 +68,15 @@ answer gets these answers, and the honest ones are the useful ones:
|
||||||
- **Material spend?** Unknown, and explicitly so. Only the proposed backup
|
- **Material spend?** Unknown, and explicitly so. Only the proposed backup
|
||||||
storage carries price evidence. Every other resource is waiting on
|
storage carries price evidence. Every other resource is waiting on
|
||||||
`RAIL-HO-WP-0008` and `FIN-WP-0004`.
|
`RAIL-HO-WP-0008` and `FIN-WP-0004`.
|
||||||
- **Idle or saturated capacity?** `railiance01` and the `reef-railiance` k3s
|
- **Idle or saturated capacity?** `railiance01`, the `reef-railiance` k3s
|
||||||
cluster are idle on every measured metric — 14 percent CPU, 37 percent memory.
|
cluster, and `apps-pg` are idle on every measured metric — 14 percent CPU,
|
||||||
Nothing is saturated. Five resources cannot be measured at all.
|
37 percent memory, and 5.8 percent of the apps-pg volume. Nothing is
|
||||||
- **Forecast error?** Not yet computable. The control-cycle mechanism is proven
|
saturated. Four resources cannot be measured at all.
|
||||||
on paired examples, but no operational actual observation exists, so there is
|
- **Forecast error?** Not yet computable. The first operational forecast record
|
||||||
no variance to report and none is fabricated.
|
exists for apps-pg, but its period is 2026-09 and no actual has been observed
|
||||||
|
against it, so there is no variance to report and none is fabricated. Its
|
||||||
|
infrastructure cost is `null`, so even when the actual lands, cost variance
|
||||||
|
will read `unknown` until a booked fact arrives.
|
||||||
- **Approaching commitments?** None visible — and that is itself the finding:
|
- **Approaching commitments?** None visible — and that is itself the finding:
|
||||||
six active resources have no renewal or cancellation date recorded, so the
|
six active resources have no renewal or cancellation date recorded, so the
|
||||||
cancellation window cannot be respected for any of them.
|
cancellation window cannot be respected for any of them.
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"schema_version":"0.1","record_id":"actual:apps-pg:2026-09","record_type":"actual","resource_id":"resource:railiance:apps-pg","resource_class":"shared_platform_service","period":"2026-09","created_at":"2026-10-02T00:00:00Z","scenario":"observed","forecast_ref":"forecast:apps-pg:2026-09:base:v1","revision_of":null,"usage_proxies":{"database_gb":{"value":6,"unit":"GB"},"connections":{"value":18,"unit":"peak"}},"capacity":{"model":"hybrid","provisioned":{"storage":{"value":20,"unit":"GiB"}},"used":{"storage":{"value":6,"unit":"GB"}}},"costs":{"currency":"EUR","infrastructure":15,"internal_labor":90,"external_labor":0,"total":105,"booked_cost_refs":["financial-fact:example-shared-allocation-2026-09"]},"allocation":{"method":"proportional","driver":"database_gb","method_version":"1","unattributed_eur":5},"service_constraints":{"rpo_minutes":{"value":5,"unit":"minutes"},"rto_minutes":{"value":60,"unit":"minutes"}},"variance_attribution":{"database_gb":"demand","connections":"model","costs.internal_labor":"labor"},"evidence":["illustrative actual; replace with RAILIANCE-WP-0016 evidence"]}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"schema_version":"0.1","record_id":"forecast:apps-pg:2026-09:base:v1","record_type":"forecast","resource_id":"resource:railiance:apps-pg","resource_class":"shared_platform_service","period":"2026-09","created_at":"2026-08-11T00:00:00Z","scenario":"base","forecast_ref":null,"revision_of":null,"usage_proxies":{"database_gb":{"value":5,"unit":"GB"},"connections":{"value":20,"unit":"peak"}},"capacity":{"model":"hybrid","provisioned":{"storage":{"value":20,"unit":"GiB"}},"used":{"storage":{"value":5,"unit":"GB"}}},"costs":{"currency":"EUR","infrastructure":15,"internal_labor":60,"external_labor":0,"total":75,"booked_cost_refs":[]},"allocation":{"method":"proportional","driver":"database_gb","method_version":"1","unattributed_eur":5},"service_constraints":{"rpo_minutes":{"value":5,"unit":"minutes"},"rto_minutes":{"value":60,"unit":"minutes"}},"uncertainty":{"level":"high","notes":["Illustrative until platform evidence arrives"]},"evidence":["RAILIANCE-WP-0016"]}
|
|
||||||
|
|
@ -4,81 +4,286 @@
|
||||||
"title": "Immutable resource forecast or actual observation",
|
"title": "Immutable resource forecast or actual observation",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["schema_version", "record_id", "record_type", "resource_id", "resource_class", "period", "created_at", "usage_proxies", "capacity", "costs", "allocation", "service_constraints", "evidence"],
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"record_id",
|
||||||
|
"record_type",
|
||||||
|
"resource_id",
|
||||||
|
"resource_class",
|
||||||
|
"period",
|
||||||
|
"created_at",
|
||||||
|
"usage_proxies",
|
||||||
|
"capacity",
|
||||||
|
"costs",
|
||||||
|
"allocation",
|
||||||
|
"service_constraints",
|
||||||
|
"evidence"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"schema_version": {"const": "0.1"},
|
"schema_version": {
|
||||||
"record_id": {"type": "string", "minLength": 1},
|
"enum": [
|
||||||
"record_type": {"enum": ["forecast", "actual"]},
|
"0.1",
|
||||||
"resource_id": {"type": "string", "pattern": "^resource:"},
|
"0.2"
|
||||||
"resource_class": {"enum": ["storage", "vm", "cluster_compute", "shared_platform_service", "managed_service", "other"]},
|
],
|
||||||
"period": {"type": "string", "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$"},
|
"description": "0.2 permits null cost components for facts the authoritative repository has not supplied. Existing 0.1 records stay valid and are never rewritten; immutability applies to the record, not to the schema."
|
||||||
"created_at": {"type": "string", "format": "date-time"},
|
},
|
||||||
"scenario": {"enum": ["low", "base", "high", "observed"]},
|
"record_id": {
|
||||||
"forecast_ref": {"type": ["string", "null"]},
|
"type": "string",
|
||||||
"revision_of": {"type": ["string", "null"]},
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"record_type": {
|
||||||
|
"enum": [
|
||||||
|
"forecast",
|
||||||
|
"actual"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resource_id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^resource:"
|
||||||
|
},
|
||||||
|
"resource_class": {
|
||||||
|
"enum": [
|
||||||
|
"storage",
|
||||||
|
"vm",
|
||||||
|
"cluster_compute",
|
||||||
|
"shared_platform_service",
|
||||||
|
"managed_service",
|
||||||
|
"other"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"period": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"scenario": {
|
||||||
|
"enum": [
|
||||||
|
"low",
|
||||||
|
"base",
|
||||||
|
"high",
|
||||||
|
"observed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"forecast_ref": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"revision_of": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"usage_proxies": {
|
"usage_proxies": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"minProperties": 1,
|
"minProperties": 1,
|
||||||
"additionalProperties": {"$ref": "#/$defs/measurement"}
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/measurement"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"capacity": {
|
"capacity": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["model", "provisioned", "used"],
|
"required": [
|
||||||
|
"model",
|
||||||
|
"provisioned",
|
||||||
|
"used"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"model": {"enum": ["fixed", "elastic", "hybrid"]},
|
"model": {
|
||||||
"provisioned": {"type": "object", "additionalProperties": {"$ref": "#/$defs/measurement"}},
|
"enum": [
|
||||||
"used": {"type": "object", "additionalProperties": {"$ref": "#/$defs/measurement"}}
|
"fixed",
|
||||||
|
"elastic",
|
||||||
|
"hybrid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"provisioned": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/measurement"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"used": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/measurement"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"costs": {
|
"costs": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["currency", "infrastructure", "internal_labor", "external_labor", "total"],
|
"required": [
|
||||||
|
"currency",
|
||||||
|
"infrastructure",
|
||||||
|
"internal_labor",
|
||||||
|
"external_labor",
|
||||||
|
"total"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"currency": {"const": "EUR"},
|
"currency": {
|
||||||
"infrastructure": {"type": "number", "minimum": 0},
|
"const": "EUR"
|
||||||
"internal_labor": {"type": "number", "minimum": 0},
|
},
|
||||||
"external_labor": {"type": "number", "minimum": 0},
|
"infrastructure": {
|
||||||
"total": {"type": "number", "minimum": 0},
|
"type": [
|
||||||
"booked_cost_refs": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
|
"number",
|
||||||
}
|
"null"
|
||||||
|
],
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "EUR, or null when the amount is not yet known. Null is unknown, never zero."
|
||||||
|
},
|
||||||
|
"internal_labor": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "EUR, or null when the amount is not yet known. Null is unknown, never zero."
|
||||||
|
},
|
||||||
|
"external_labor": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "EUR, or null when the amount is not yet known. Null is unknown, never zero."
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "EUR, or null when the amount is not yet known. Null is unknown, never zero."
|
||||||
|
},
|
||||||
|
"booked_cost_refs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Cost components split so a cheaper unit price that raises operator hours is visible. Any component may be null when the authoritative repository or fin-hub has not supplied it; total must then also be null, and variance for that component is reported as unknown."
|
||||||
},
|
},
|
||||||
"allocation": {
|
"allocation": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["method", "driver", "method_version", "unattributed_eur"],
|
"required": [
|
||||||
|
"method",
|
||||||
|
"driver",
|
||||||
|
"method_version",
|
||||||
|
"unattributed_eur"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"method": {"type": "string", "minLength": 1},
|
"method": {
|
||||||
"driver": {"type": "string", "minLength": 1},
|
"type": "string",
|
||||||
"method_version": {"type": "string", "minLength": 1},
|
"minLength": 1
|
||||||
"unattributed_eur": {"type": "number", "minimum": 0}
|
},
|
||||||
|
"driver": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"method_version": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"unattributed_eur": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "EUR that reaches no consumer under the stated method, or null when the underlying cost itself is unknown. Null is unknown, never zero."
|
||||||
|
},
|
||||||
|
"unattributed_share": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1,
|
||||||
|
"description": "Fraction of the allocation driver that reaches no consumer. Recordable from technical evidence alone, before any EUR amount exists."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"service_constraints": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/measurement"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"service_constraints": {"type": "object", "additionalProperties": {"$ref": "#/$defs/measurement"}},
|
|
||||||
"uncertainty": {
|
"uncertainty": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["level", "notes"],
|
"required": [
|
||||||
|
"level",
|
||||||
|
"notes"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"level": {"enum": ["low", "medium", "high"]},
|
"level": {
|
||||||
"notes": {"type": "array", "items": {"type": "string"}}
|
"enum": [
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"variance_attribution": {
|
"variance_attribution": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": {"enum": ["demand", "provider_price", "allocation", "labor", "model", "data_quality"]}
|
"additionalProperties": {
|
||||||
|
"enum": [
|
||||||
|
"demand",
|
||||||
|
"provider_price",
|
||||||
|
"allocation",
|
||||||
|
"labor",
|
||||||
|
"model",
|
||||||
|
"data_quality"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"evidence": {"type": "array", "minItems": 1, "items": {"type": "string"}}
|
"evidence": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"measurement": {
|
"measurement": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["value", "unit"],
|
"required": [
|
||||||
|
"value",
|
||||||
|
"unit"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"value": {"type": "number", "minimum": 0},
|
"value": {
|
||||||
"unit": {"type": "string", "minLength": 1}
|
"type": [
|
||||||
|
"number",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Null means the quantity is unknown or unbounded, never zero. A declared service constraint with a null value records that no objective exists yet."
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -50,5 +51,77 @@ class ControlCycleTest(unittest.TestCase):
|
||||||
self.assertEqual("data_quality", result["usage_proxies"]["stored_gb"]["category"])
|
self.assertEqual("data_quality", result["usage_proxies"]["stored_gb"]["category"])
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownCostTest(unittest.TestCase):
|
||||||
|
"""Schema 0.2: a cost the authoritative repository has not supplied is
|
||||||
|
unknown, not zero, and must not produce a fabricated variance."""
|
||||||
|
|
||||||
|
def test_unknown_cost_reports_status_rather_than_an_error_figure(self):
|
||||||
|
forecast, actual = record(), record("actual")
|
||||||
|
forecast["costs"].update(infrastructure=None, total=None)
|
||||||
|
actual["costs"].update(infrastructure=None, total=None)
|
||||||
|
result = compare(forecast, actual)["costs"]["infrastructure"]
|
||||||
|
self.assertEqual("unknown", result["status"])
|
||||||
|
self.assertNotIn("error", result)
|
||||||
|
|
||||||
|
def test_unknown_cost_is_a_data_quality_gap_not_a_price_movement(self):
|
||||||
|
forecast, actual = record(), record("actual")
|
||||||
|
actual["costs"].update(infrastructure=None, total=None)
|
||||||
|
actual["variance_attribution"] = {"costs.infrastructure": "provider_price"}
|
||||||
|
result = compare(forecast, actual)["costs"]["infrastructure"]
|
||||||
|
self.assertEqual("data_quality", result["category"])
|
||||||
|
|
||||||
|
def test_known_components_still_compute_beside_an_unknown_one(self):
|
||||||
|
forecast, actual = record(), record("actual")
|
||||||
|
forecast["costs"].update(infrastructure=None, total=None)
|
||||||
|
actual["costs"].update(infrastructure=None, internal_labor=90, total=None)
|
||||||
|
result = compare(forecast, actual)["costs"]
|
||||||
|
self.assertEqual("unknown", result["infrastructure"]["status"])
|
||||||
|
self.assertEqual(30, result["internal_labor"]["error"])
|
||||||
|
|
||||||
|
def test_one_sided_unknown_does_not_subtract_against_a_missing_amount(self):
|
||||||
|
forecast, actual = record(), record("actual")
|
||||||
|
actual["costs"].update(infrastructure=None, total=None)
|
||||||
|
result = compare(forecast, actual)["costs"]["infrastructure"]
|
||||||
|
self.assertEqual("unknown", result["status"])
|
||||||
|
self.assertEqual(10, result["forecast"])
|
||||||
|
self.assertIsNone(result["actual"])
|
||||||
|
|
||||||
|
|
||||||
|
class OperationalRecordTest(unittest.TestCase):
|
||||||
|
"""The first real apps-pg record, folded in from RAILIANCE-WP-0016."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
path = Path(__file__).parents[1] / "data" / "control-cycle" / "apps-pg-2026-09-base.json"
|
||||||
|
self.record = json.loads(path.read_text())
|
||||||
|
|
||||||
|
def test_usage_is_known_while_infrastructure_cost_is_not(self):
|
||||||
|
costs = self.record["costs"]
|
||||||
|
self.assertIsNone(costs["infrastructure"])
|
||||||
|
self.assertIsNone(costs["total"])
|
||||||
|
self.assertEqual(30.0, costs["internal_labor"])
|
||||||
|
self.assertTrue(self.record["usage_proxies"])
|
||||||
|
|
||||||
|
def test_total_is_not_the_sum_of_the_known_parts(self):
|
||||||
|
costs = self.record["costs"]
|
||||||
|
known = costs["internal_labor"] + costs["external_labor"]
|
||||||
|
self.assertNotEqual(known, costs["total"])
|
||||||
|
self.assertIsNone(costs["total"])
|
||||||
|
|
||||||
|
def test_shared_overhead_is_recorded_as_a_share_before_any_eur_exists(self):
|
||||||
|
allocation = self.record["allocation"]
|
||||||
|
self.assertEqual("apps-pg-dbbytes-v1", allocation["method_version"])
|
||||||
|
self.assertIsNone(allocation["unattributed_eur"])
|
||||||
|
self.assertEqual(0.948, allocation["unattributed_share"])
|
||||||
|
|
||||||
|
def test_absent_recovery_objectives_are_null_not_optimistic_defaults(self):
|
||||||
|
constraints = self.record["service_constraints"]
|
||||||
|
self.assertIsNone(constraints["rpo_minutes"]["value"])
|
||||||
|
self.assertIsNone(constraints["rto_minutes"]["value"])
|
||||||
|
|
||||||
|
def test_record_cites_the_authoritative_platform_evidence(self):
|
||||||
|
self.assertIn("RAILIANCE-WP-0016", self.record["evidence"])
|
||||||
|
self.assertEqual("high", self.record["uncertainty"]["level"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -159,5 +159,40 @@ class LivePortfolioTest(unittest.TestCase):
|
||||||
self.assertEqual(self.report, build(ROOT, TODAY))
|
self.assertEqual(self.report, build(ROOT, TODAY))
|
||||||
|
|
||||||
|
|
||||||
|
class DeliveredEvidenceTest(unittest.TestCase):
|
||||||
|
"""RAILIANCE-WP-0016 delivered apps-pg evidence; partial delivery must not
|
||||||
|
read as full coverage."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.report = build(ROOT, TODAY)
|
||||||
|
|
||||||
|
def test_delivered_gap_leaves_the_open_list(self):
|
||||||
|
owners = {gap["owner"] for gap in self.report["coverage"]["unresolved_gaps"]}
|
||||||
|
self.assertNotIn("railiance-platform", owners)
|
||||||
|
|
||||||
|
def test_delivered_gap_stays_visible_with_its_interface(self):
|
||||||
|
delivered = self.report["coverage"]["delivered_gaps"]
|
||||||
|
self.assertEqual(1, len(delivered))
|
||||||
|
self.assertEqual("railiance-platform", delivered[0]["owner"])
|
||||||
|
self.assertTrue(delivered[0]["interface"])
|
||||||
|
|
||||||
|
def test_residual_unknowns_still_produce_next_actions(self):
|
||||||
|
residuals = [a for a in self.report["next_actions"] if "delivered, still open" in a]
|
||||||
|
self.assertEqual(3, len(residuals))
|
||||||
|
|
||||||
|
def test_apps_pg_is_now_measurable_and_idle(self):
|
||||||
|
self.assertIn("resource:railiance:apps-pg", self.report["utilization"]["idle"])
|
||||||
|
unmeasured = {row["resource_id"] for row in self.report["utilization"]["unmeasured"]}
|
||||||
|
self.assertNotIn("resource:railiance:apps-pg", unmeasured)
|
||||||
|
|
||||||
|
def test_apps_pg_cost_is_now_attributed_but_still_unpriced(self):
|
||||||
|
unattributed = {row["resource_id"] for row in self.report["cost"]["unattributed_allocation"]}
|
||||||
|
self.assertNotIn("resource:railiance:apps-pg", unattributed)
|
||||||
|
unpriced = {row["resource_id"] for row in self.report["cost"]["unpriced"]}
|
||||||
|
self.assertIn("resource:railiance:apps-pg", unpriced)
|
||||||
|
self.assertIsNone(self.report["cost"]["known_monthly_spend_eur"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,11 @@ COST_FIELDS = ("infrastructure", "internal_labor", "external_labor", "total")
|
||||||
ATTRIBUTIONS = {"demand", "provider_price", "allocation", "labor", "model", "data_quality"}
|
ATTRIBUTIONS = {"demand", "provider_price", "allocation", "labor", "model", "data_quality"}
|
||||||
|
|
||||||
|
|
||||||
def delta(forecast: float, actual: float) -> dict:
|
def delta(forecast: float | None, actual: float | None) -> dict:
|
||||||
|
# A missing amount is unknown, not zero: subtracting against it would
|
||||||
|
# manufacture a variance the evidence does not support.
|
||||||
|
if forecast is None or actual is None:
|
||||||
|
return {"forecast": forecast, "actual": actual, "status": "unknown"}
|
||||||
error = actual - forecast
|
error = actual - forecast
|
||||||
return {
|
return {
|
||||||
"forecast": forecast,
|
"forecast": forecast,
|
||||||
|
|
@ -50,7 +54,12 @@ def compare(forecast: dict, actual: dict) -> dict:
|
||||||
costs = {}
|
costs = {}
|
||||||
for name in COST_FIELDS:
|
for name in COST_FIELDS:
|
||||||
default_category = "labor" if "labor" in name else "provider_price"
|
default_category = "labor" if "labor" in name else "provider_price"
|
||||||
costs[name] = {**delta(forecast["costs"][name], actual["costs"][name]), "currency": "EUR", "category": attribution.get(f"costs.{name}", default_category)}
|
result = delta(forecast["costs"][name], actual["costs"][name])
|
||||||
|
# An unknown amount is a data-quality gap, not a price or labour movement.
|
||||||
|
category = "data_quality" if result.get("status") == "unknown" else attribution.get(
|
||||||
|
f"costs.{name}", default_category
|
||||||
|
)
|
||||||
|
costs[name] = {**result, "currency": "EUR", "category": category}
|
||||||
|
|
||||||
if forecast["allocation"] != actual["allocation"]:
|
if forecast["allocation"] != actual["allocation"]:
|
||||||
costs["allocation_method"] = {"status": "changed", "category": attribution.get("allocation", "allocation")}
|
costs["allocation_method"] = {"status": "changed", "category": attribution.get("allocation", "allocation")}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ USAGE_PAIRS = {
|
||||||
"cpu": "cpu_usage",
|
"cpu": "cpu_usage",
|
||||||
"memory": "memory_usage",
|
"memory": "memory_usage",
|
||||||
"root_filesystem": "root_filesystem_used",
|
"root_filesystem": "root_filesystem_used",
|
||||||
|
"storage": "storage_used",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -59,9 +60,23 @@ def coverage_section(coverage: dict | None) -> dict:
|
||||||
{"group": group["group"], "status": group["status"], "resources": len(group["resource_ids"])}
|
{"group": group["group"], "status": group["status"], "resources": len(group["resource_ids"])}
|
||||||
for group in coverage["coverage"]
|
for group in coverage["coverage"]
|
||||||
],
|
],
|
||||||
|
# A delivered gap stays visible with its residual unknowns rather than
|
||||||
|
# disappearing, so partial delivery is not read as full coverage.
|
||||||
"unresolved_gaps": [
|
"unresolved_gaps": [
|
||||||
{"owner": gap["owner"], "gap": gap["gap"], "delegated_workplan": gap["delegated_workplan"]}
|
{"owner": gap["owner"], "gap": gap["gap"], "delegated_workplan": gap["delegated_workplan"]}
|
||||||
for gap in coverage["owned_gaps"]
|
for gap in coverage["owned_gaps"]
|
||||||
|
if gap.get("status", "open") == "open"
|
||||||
|
],
|
||||||
|
"delivered_gaps": [
|
||||||
|
{
|
||||||
|
"owner": gap["owner"],
|
||||||
|
"delegated_workplan": gap["delegated_workplan"],
|
||||||
|
"delivered_on": gap.get("delivered_on"),
|
||||||
|
"interface": gap.get("interface", []),
|
||||||
|
"residual_unknowns": gap.get("residual_unknowns", []),
|
||||||
|
}
|
||||||
|
for gap in coverage["owned_gaps"]
|
||||||
|
if gap.get("status", "open") == "delivered"
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,6 +265,9 @@ def next_actions(report: dict) -> list[str]:
|
||||||
actions = []
|
actions = []
|
||||||
for gap in report["coverage"]["unresolved_gaps"]:
|
for gap in report["coverage"]["unresolved_gaps"]:
|
||||||
actions.append(f"{gap['owner']}: deliver {gap['delegated_workplan']} — {gap['gap']}")
|
actions.append(f"{gap['owner']}: deliver {gap['delegated_workplan']} — {gap['gap']}")
|
||||||
|
for gap in report["coverage"].get("delivered_gaps", []):
|
||||||
|
for residual in gap["residual_unknowns"]:
|
||||||
|
actions.append(f"{gap['owner']}: {gap['delegated_workplan']} delivered, still open — {residual}")
|
||||||
if report["cost"]["unpriced"]:
|
if report["cost"]["unpriced"]:
|
||||||
actions.append(
|
actions.append(
|
||||||
"resource-control: no portfolio spend figure exists until at least one booked cost arrives "
|
"resource-control: no portfolio spend figure exists until at least one booked cost arrives "
|
||||||
|
|
|
||||||
|
|
@ -44,16 +44,30 @@ def main() -> int:
|
||||||
for resource in resources:
|
for resource in resources:
|
||||||
assert not required - resource.keys(), f"missing fields: {required - resource.keys()}"
|
assert not required - resource.keys(), f"missing fields: {required - resource.keys()}"
|
||||||
validate_record(resource)
|
validate_record(resource)
|
||||||
|
operational_records = [load(str(path)) for path in Path("data/control-cycle").glob("*.json")]
|
||||||
|
control_records += operational_records
|
||||||
record_ids = {record["record_id"] for record in control_records}
|
record_ids = {record["record_id"] for record in control_records}
|
||||||
assert len(record_ids) == len(control_records)
|
assert len(record_ids) == len(control_records)
|
||||||
assert {record["resource_class"] for record in control_records} == {"storage", "cluster_compute", "shared_platform_service"}
|
assert {record["resource_class"] for record in control_records} >= {"storage", "cluster_compute", "shared_platform_service"}
|
||||||
for record in control_records:
|
for record in control_records:
|
||||||
assert record["schema_version"] == "0.1"
|
assert record["schema_version"] in {"0.1", "0.2"}
|
||||||
assert record["resource_id"].startswith("resource:")
|
assert record["resource_id"].startswith("resource:")
|
||||||
costs = record["costs"]
|
costs = record["costs"]
|
||||||
assert costs["total"] == round(costs["infrastructure"] + costs["internal_labor"] + costs["external_labor"], 2)
|
components = [costs["infrastructure"], costs["internal_labor"], costs["external_labor"]]
|
||||||
|
if any(component is None for component in components):
|
||||||
|
# An unknown component makes the total unknown; it is never the sum
|
||||||
|
# of the parts that happen to be known.
|
||||||
|
assert record["schema_version"] == "0.2", "null costs require schema 0.2"
|
||||||
|
assert costs["total"] is None
|
||||||
|
else:
|
||||||
|
assert costs["total"] == round(sum(components), 2)
|
||||||
if record["record_type"] == "actual":
|
if record["record_type"] == "actual":
|
||||||
assert record["forecast_ref"] in record_ids
|
assert record["forecast_ref"] in record_ids
|
||||||
|
# Operational records assert real facts and must cite the authoritative
|
||||||
|
# repository evidence they came from.
|
||||||
|
for record in operational_records:
|
||||||
|
assert record["evidence"], f"{record['record_id']} cites no evidence"
|
||||||
|
assert record["uncertainty"]["notes"], f"{record['record_id']} states no uncertainty"
|
||||||
case_schema = load("schemas/optimization-case.schema.json")
|
case_schema = load("schemas/optimization-case.schema.json")
|
||||||
assert case_schema["$schema"].endswith("2020-12/schema")
|
assert case_schema["$schema"].endswith("2020-12/schema")
|
||||||
assert case_schema["properties"]["schema_version"]["const"] == "0.1"
|
assert case_schema["properties"]["schema_version"]["const"] == "0.1"
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,23 @@ resources with no contract dates at all, and six of seven resources sharing
|
||||||
- [x] At least two optimization cases complete the evidence-to-decision loop.
|
- [x] At least two optimization cases complete the evidence-to-decision loop.
|
||||||
- [x] Portfolio reporting exposes unknown and unattributed values explicitly.
|
- [x] Portfolio reporting exposes unknown and unattributed values explicitly.
|
||||||
|
|
||||||
|
## Post-completion record
|
||||||
|
|
||||||
|
2026-08-14: `railiance-platform` delivered `RAILIANCE-WP-0016`, the first
|
||||||
|
delegated evidence from T04 to land. Folded in: the apps-pg resource record now
|
||||||
|
carries real capacity and the `apps-pg-dbbytes-v1` allocation driver, the first
|
||||||
|
operational control-cycle record replaced the illustrative apps-pg fixture, and
|
||||||
|
coverage moved to `data/portfolio-coverage-2026-08-14.json` with the gap marked
|
||||||
|
delivered and its three residual unknowns still open.
|
||||||
|
|
||||||
|
The real evidence exposed a design gap in the T05 schema: v0.1 required a number
|
||||||
|
for every cost field, so recording genuine usage without a booked cost was
|
||||||
|
impossible without inventing one. Schema 0.2 permits null costs, a null
|
||||||
|
unattributed amount, a technical `unattributed_share`, and null measurements;
|
||||||
|
the comparator reports unknown amounts as `data_quality` rather than computing a
|
||||||
|
variance. Existing 0.1 records were not rewritten. Evidence:
|
||||||
|
`docs/evidence/RESOURCE-WP-0003-apps-pg-evidence-foldin-2026-08-14.md`.
|
||||||
|
|
||||||
## Standing gate
|
## Standing gate
|
||||||
|
|
||||||
Both optimization cases are `blocked_on_evidence` by design: the mechanism is
|
Both optimization cases are `blocked_on_evidence` by design: the mechanism is
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue