Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb56574824 | |||
| 8363c8bdc9 | |||
| b375f263dc | |||
| e1fe419028 | |||
| 9ec4f26874 | |||
| 80eea82398 | |||
| e39cf656b2 | |||
| c4f56d9e85 | |||
| c1dbaad88d | |||
| d93184d41d | |||
| de3cec6a23 | |||
| 0a4cddafc4 | |||
| 6d2b1401aa | |||
| 0a8fb5dd1b | |||
| a8c90107b5 | |||
| ced653bfc0 | |||
| 153014be9d | |||
| 8d4b3b67fb |
11
.env.example
11
.env.example
@@ -1,10 +1,19 @@
|
||||
# file: .env.example
|
||||
# version: 10
|
||||
# version: 13
|
||||
|
||||
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
|
||||
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
|
||||
KSP_LOGS_DIRECTORY=logs
|
||||
|
||||
# PostgreSQL URI for the Devnet Store target.
|
||||
KSP_SECRET_STORE_DEVNET_POSTGRES_URI=postgresql://...
|
||||
|
||||
# PostgreSQL URI for the Mainnet Store target.
|
||||
KSP_SECRET_STORE_MAINNET_POSTGRES_URI=postgresql://...
|
||||
|
||||
# PostgreSQL URI for the Testnet Store target.
|
||||
KSP_SECRET_STORE_TESTNET_POSTGRES_URI=postgresql://...
|
||||
|
||||
# KSP Wallet root directory. Used by config/std.wallet.json before an optional profile subdirectory is appended.
|
||||
# The committed Wallet document falls back to "wallets" when neither the process environment nor .env defines this variable.
|
||||
KSP_WALLETS_DIRECTORY=wallets
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,8 +1,18 @@
|
||||
<!-- file: CHANGELOG.md -->
|
||||
<!-- version: 19 -->
|
||||
<!-- version: 20 -->
|
||||
|
||||
# Changelog KSP
|
||||
|
||||
## 0.3.2 — Store/PostgreSQL runtime foundation — 2026-08-30
|
||||
|
||||
`0.3.2` introduit ensemble `ksp-store-lib` et `ksp-store-postgres-lib` comme fondation runtime/backend PostgreSQL au-dessus des contrats backend-agnostic de `ksp-store-api`, sans encore implémenter de capability RAW métier. La façade `Store` conserve un backend connu `Postgres` activé par défaut via Cargo feature, reste compilable avec `--no-default-features`, lie chaque instance à exactement un `RawNetworkId` et n'expose aucun `Pool`, `Client`, `Row`, `Statement`, driver ou SQL physique. `ksp-config-lib` possède désormais `std.store` avec trois targets nommés `devnet`, `mainnet` et `testnet`, chacun associé à un réseau explicite et à une URI PostgreSQL Secret indépendante ; Store/backend ne lisent directement ni `.env`, ni `KSP_*`, ni `PG*`, ni `.pgpass`.
|
||||
|
||||
Le backend de référence utilise `tokio-postgres 0.7.18`, `deadpool-postgres 0.14.2`, `tokio-postgres-rustls 0.14.0`, Rustls 0.23 avec roots système/AWS-LC et des modes TLS KSP limités à `Disabled` et `VerifyFull`. Les URI sont parsées puis normalisées sous la policy typée KSP, le pool et les deadlines connect/wait/create/recycle/shutdown sont bornés, et une ouverture réussie exige une connexion physique puis un bootstrap valide. Le moteur de migrations privé commence par `V000__bootstrap.sql`, enregistre version/nom/SHA-256 dans `ksp_store_schema_migrations`, sérialise les runners par advisory transaction lock borné et refuse checksum mismatch ou schema plus récent sans down automatique. La façade fournit également `runtime_snapshot()` et `health().await` avec une projection portable/redacted de readiness et des compteurs pool sûrs.
|
||||
|
||||
Les canaris de hardening verrouillent 84 exports crate-root côté façade, la frontière backend/Config, l'absence de bypass environnement, la redaction des URI/erreurs, la matrice de features et l'absence de persistence `RawTransaction`/`RawAccountState`. Le gate technique final a été rejoué après `cargo clean` : audits Rust/Markdown, `cargo check --workspace`, Clippy, tests ciblés de toutes les crates, `cargo test --workspace`, graphes Cargo et les trois builds Tauri Linux passent. Le smoke PostgreSQL réel passe sur **PostgreSQL 17** et prouve bootstrap initial/idempotent/concurrent, mismatch/recovery checksum, rollback transactionnel, health `Ready` et fermeture bornée ; la policy de fondation reste PostgreSQL >= 15 sans maximum KSP artificiel.
|
||||
|
||||
`prompts/022-V0_3_3_START_PROMPT.md` ouvre ensuite uniquement la vertical slice PostgreSQL `RawTransaction` complète sur les mêmes crates et la même fondation : six capabilities transaction/observation/rétention, atomicité, idempotence/conflit, get/list cursorisé, tombstone et `ForceRehydrate`. `RawAccountState` PostgreSQL et la complétude RAW restent réservés à `0.3.4`. L'archive historique `khadhroony-bot3_v0.5.3-pre.005-fix010.zip` reste obligatoire au `pre.001` de `0.3.3`, mais seulement pour réauditer l'ancien schéma/repository RAW transaction comme source d'héritage, jamais comme autorité KSP.
|
||||
|
||||
## 0.3.1 — Store API RAW foundation — 2026-08-29
|
||||
|
||||
`0.3.1` introduit `ksp-store-api` comme contrat backend-agnostic de persistence N1 RAW, sans runtime Store ni backend physique. La release stabilise deux familles réellement convergentes : `RawTransaction` avec payload canonique opaque/versionné, identité réseau+signature et observations d’acquisition séparées, puis `RawAccountState` avec bytes complets, identité réseau+pubkey+slot+hash et observations pouvant conserver les enrichissements Yellowstone sans les confondre avec l’état canonique. `TransactionStatusObservation` reste reporté faute de convergence sémantique suffisante entre snapshot HTTP, transition WebSocket et update Yellowstone ; `logsSubscribe`, slot/root/slotsUpdates et vote restent event-only candidats, `RawBlock` reste une idée conditionnelle et Yellowstone `Entry` reste rejeté de la taxonomie active.
|
||||
|
||||
16
Cargo.toml
16
Cargo.toml
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 331
|
||||
# version: 347
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-wallet-lib"]
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
@@ -14,20 +14,24 @@ authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
argon2 = { version = "^0.5", default-features = false }
|
||||
argon2 = { version = "^0.6", default-features = false }
|
||||
base64 = { version = "^0.23" }
|
||||
chacha20poly1305 = { version = "^0.11", default-features = false }
|
||||
chrono = { version = "^0.4", default-features = false }
|
||||
deadpool-postgres = { version = "^0.14", default-features = false }
|
||||
directories = { version = "^6.0" }
|
||||
ed25519-dalek = { version = "^3.0", default-features = false }
|
||||
fs2 = { version = "^0.4" }
|
||||
futures-util = { version = "^0.3", default-features = false }
|
||||
getrandom = { version = "^0.4", default-features = false }
|
||||
http = { version = "^1.5", default-features = false }
|
||||
jsonschema = { version = "^0.51", default-features = false }
|
||||
jsonschema = { version = "^0.52", default-features = false }
|
||||
reqwest = { version = "^0.13", default-features = false }
|
||||
rustls = { version = "^0.23", default-features = false }
|
||||
rustls-native-certs = { version = "^0.8", default-features = false }
|
||||
serde = { version = "^1.0" }
|
||||
serde_json = { version = "^1.0" }
|
||||
sha2 = { version = "^0.11", default-features = false }
|
||||
solana-keypair = { version = "^3.1", default-features = false }
|
||||
solana-pubkey = { version = "^4.3", default-features = false }
|
||||
tauri = { version = "^2.11" }
|
||||
@@ -39,6 +43,8 @@ tracing = { version = "^0.1", default-features = false }
|
||||
tracing-subscriber = { version = "^0.3", default-features = false }
|
||||
tracing-appender = { version = "^0.2", default-features = false }
|
||||
tokio = { version = "^1.53", default-features = false }
|
||||
tokio-postgres = { version = "^0.7", default-features = false }
|
||||
tokio-postgres-rustls = { version = "^0.14", default-features = false }
|
||||
tokio-tungstenite = { version = "^0.30", default-features = false }
|
||||
tonic = { version = "^0.14", default-features = false }
|
||||
tonic-prost = { version = "^0.14", default-features = false }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 94 -->
|
||||
<!-- version: 95 -->
|
||||
|
||||
# Roadmap KSP
|
||||
|
||||
@@ -94,8 +94,8 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||
## 0.3.x — RAW / acquisition persistée
|
||||
|
||||
- [X] `0.3.1` — `ksp-store-api` stable : modèles N1 RAW backend-agnostic `RawTransaction` et `RawAccountState` avec observations, provenance, payload/hash/timestamps bornés, 10 capabilities object-safe, queries cursorisées sans plafond métier arbitraire, outcomes idempotence/conflit et lifecycle logique rétention/tombstone/force-rehydrate ; aucun backend physique, Config, runtime Store, notification dédiée ni surface STRUCTURAL/DECODED/DOMAIN.
|
||||
- [ ] `0.3.2` — Introduire ensemble `ksp-store-lib` et `ksp-store-postgres-lib` pour la **fondation runtime/backend PostgreSQL uniquement** : façade Store, feature `postgres` par défaut, dispatch des backends compilés, Config `std.store`/secrets, connexion/pool/TLS à réauditer, bootstrap/migrations privés et health/readiness seulement si un contrat portable est réellement justifié. Aucun schéma `RawTransaction`/`RawAccountState` n’est ajouté dans cette slice.
|
||||
- [ ] `0.3.3` — Étendre le même couple `ksp-store-lib` + `ksp-store-postgres-lib` avec la vertical slice PostgreSQL `RawTransaction` complète : persistence/observation atomiques, get/list cursorisé, idempotence/conflit, rétention/tombstone/force-rehydrate, concurrence et rollback validés sur PostgreSQL réel.
|
||||
- [X] `0.3.2` — `ksp-store-lib` + `ksp-store-postgres-lib` stables comme fondation runtime/backend PostgreSQL : feature `postgres` par défaut, `Store` lié à un unique `RawNetworkId`, Config `std.store` avec targets/bases `devnet`/`mainnet`/`testnet`, pool Deadpool borné, `tokio-postgres`, TLS Rustls `Disabled`/`VerifyFull`, moteur de migrations privé `V000` + SHA-256/advisory lock, health/readiness portable et close borné. Gate complet + PostgreSQL réel major 17 verts ; aucune table/capability `RawTransaction`/`RawAccountState` métier n'est encore ajoutée.
|
||||
- [ ] `0.3.3` — Étendre le même couple avec la vertical slice PostgreSQL `RawTransaction` complète : les six capabilities transaction/observation/rétention de `ksp-store-api`, persistence acquisition atomique, get/list cursorisé, idempotence/conflit, binding réseau, rétention/tombstone/force-rehydrate, concurrence et rollback validés sur PostgreSQL réel.
|
||||
- [ ] `0.3.4` — Étendre le même couple avec `RawAccountState` + observation, puis fermer la complétude/conformance RAW cross-family, les indexes/migrations physiques nécessaires et le hardening PostgreSQL final.
|
||||
- [ ] `0.3.5` — Étendre `ksp-interface-lib` uniquement avec les modèles passifs/events réellement partagés par les premiers consumers d’acquisition, sans dupliquer les modèles persistants de `ksp-store-api`.
|
||||
- [ ] `0.3.6` — Introduire `ksp-job-api` et un premier job de backfill historique concret consommant `ksp-store-lib`, avec policy/batch-size/progression possédés par le job et non par Store.
|
||||
|
||||
78
config/examples/std.store.example.json
Normal file
78
config/examples/std.store.example.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"default_profile": "devnet",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "devnet",
|
||||
"network": "devnet",
|
||||
"backend": "postgres",
|
||||
"postgres": {
|
||||
"connection_uri": "${KSP_SECRET_STORE_DEVNET_POSTGRES_URI:-postgresql://localhost/ksp_devnet}",
|
||||
"pool": {
|
||||
"max_connections": 8,
|
||||
"connect_timeout_ms": 10000,
|
||||
"wait_timeout_ms": 5000,
|
||||
"create_timeout_ms": 10000,
|
||||
"recycle_timeout_ms": 5000
|
||||
},
|
||||
"tls": {
|
||||
"mode": "verify_full"
|
||||
},
|
||||
"bootstrap": {
|
||||
"auto_migrate": true,
|
||||
"migration_timeout_ms": 30000,
|
||||
"migration_lock_timeout_ms": 10000
|
||||
},
|
||||
"shutdown_timeout_ms": 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile_id": "mainnet",
|
||||
"network": "mainnet-beta",
|
||||
"backend": "postgres",
|
||||
"postgres": {
|
||||
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
||||
"pool": {
|
||||
"max_connections": 8,
|
||||
"connect_timeout_ms": 10000,
|
||||
"wait_timeout_ms": 5000,
|
||||
"create_timeout_ms": 10000,
|
||||
"recycle_timeout_ms": 5000
|
||||
},
|
||||
"tls": {
|
||||
"mode": "verify_full"
|
||||
},
|
||||
"bootstrap": {
|
||||
"auto_migrate": true,
|
||||
"migration_timeout_ms": 30000,
|
||||
"migration_lock_timeout_ms": 10000
|
||||
},
|
||||
"shutdown_timeout_ms": 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile_id": "testnet",
|
||||
"network": "testnet",
|
||||
"backend": "postgres",
|
||||
"postgres": {
|
||||
"connection_uri": "${KSP_SECRET_STORE_TESTNET_POSTGRES_URI:-postgresql://localhost/ksp_testnet}",
|
||||
"pool": {
|
||||
"max_connections": 8,
|
||||
"connect_timeout_ms": 10000,
|
||||
"wait_timeout_ms": 5000,
|
||||
"create_timeout_ms": 10000,
|
||||
"recycle_timeout_ms": 5000
|
||||
},
|
||||
"tls": {
|
||||
"mode": "verify_full"
|
||||
},
|
||||
"bootstrap": {
|
||||
"auto_migrate": true,
|
||||
"migration_timeout_ms": 30000,
|
||||
"migration_lock_timeout_ms": 10000
|
||||
},
|
||||
"shutdown_timeout_ms": 5000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
168
config/schemas/std.store.schema.json
Normal file
168
config/schemas/std.store.schema.json
Normal file
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "urn:ksp:schema:std.store:v1",
|
||||
"title": "KSP standard Store configuration",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"format_version",
|
||||
"default_profile",
|
||||
"profiles"
|
||||
],
|
||||
"properties": {
|
||||
"format_version": {
|
||||
"const": 1
|
||||
},
|
||||
"default_profile": {
|
||||
"$ref": "#/$defs/profileId"
|
||||
},
|
||||
"profiles": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/profile"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"profileId": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9][a-z0-9._-]*$"
|
||||
},
|
||||
"networkId": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 128,
|
||||
"pattern": "^[A-Za-z0-9_.:-]+$"
|
||||
},
|
||||
"duration100To60000": {
|
||||
"type": "integer",
|
||||
"minimum": 100,
|
||||
"maximum": 60000
|
||||
},
|
||||
"profile": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"profile_id",
|
||||
"network",
|
||||
"backend",
|
||||
"postgres"
|
||||
],
|
||||
"properties": {
|
||||
"profile_id": {
|
||||
"$ref": "#/$defs/profileId"
|
||||
},
|
||||
"network": {
|
||||
"$ref": "#/$defs/networkId"
|
||||
},
|
||||
"backend": {
|
||||
"const": "postgres"
|
||||
},
|
||||
"postgres": {
|
||||
"$ref": "#/$defs/postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
"postgres": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"connection_uri",
|
||||
"pool",
|
||||
"tls",
|
||||
"bootstrap",
|
||||
"shutdown_timeout_ms"
|
||||
],
|
||||
"properties": {
|
||||
"connection_uri": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"pool": {
|
||||
"$ref": "#/$defs/pool"
|
||||
},
|
||||
"tls": {
|
||||
"$ref": "#/$defs/tls"
|
||||
},
|
||||
"bootstrap": {
|
||||
"$ref": "#/$defs/bootstrap"
|
||||
},
|
||||
"shutdown_timeout_ms": {
|
||||
"type": "integer",
|
||||
"minimum": 100,
|
||||
"maximum": 30000
|
||||
}
|
||||
}
|
||||
},
|
||||
"pool": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"max_connections",
|
||||
"connect_timeout_ms",
|
||||
"wait_timeout_ms",
|
||||
"create_timeout_ms",
|
||||
"recycle_timeout_ms"
|
||||
],
|
||||
"properties": {
|
||||
"max_connections": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 64
|
||||
},
|
||||
"connect_timeout_ms": {
|
||||
"$ref": "#/$defs/duration100To60000"
|
||||
},
|
||||
"wait_timeout_ms": {
|
||||
"$ref": "#/$defs/duration100To60000"
|
||||
},
|
||||
"create_timeout_ms": {
|
||||
"$ref": "#/$defs/duration100To60000"
|
||||
},
|
||||
"recycle_timeout_ms": {
|
||||
"$ref": "#/$defs/duration100To60000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"mode"
|
||||
],
|
||||
"properties": {
|
||||
"mode": {
|
||||
"enum": [
|
||||
"disabled",
|
||||
"verify_full"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"bootstrap": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"auto_migrate",
|
||||
"migration_timeout_ms",
|
||||
"migration_lock_timeout_ms"
|
||||
],
|
||||
"properties": {
|
||||
"auto_migrate": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"migration_timeout_ms": {
|
||||
"type": "integer",
|
||||
"minimum": 1000,
|
||||
"maximum": 300000
|
||||
},
|
||||
"migration_lock_timeout_ms": {
|
||||
"type": "integer",
|
||||
"minimum": 100,
|
||||
"maximum": 120000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
config/std.store.json
Normal file
78
config/std.store.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"default_profile": "devnet",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "devnet",
|
||||
"network": "devnet",
|
||||
"backend": "postgres",
|
||||
"postgres": {
|
||||
"connection_uri": "${KSP_SECRET_STORE_DEVNET_POSTGRES_URI:-postgresql://localhost/ksp_devnet}",
|
||||
"pool": {
|
||||
"max_connections": 8,
|
||||
"connect_timeout_ms": 10000,
|
||||
"wait_timeout_ms": 5000,
|
||||
"create_timeout_ms": 10000,
|
||||
"recycle_timeout_ms": 5000
|
||||
},
|
||||
"tls": {
|
||||
"mode": "verify_full"
|
||||
},
|
||||
"bootstrap": {
|
||||
"auto_migrate": true,
|
||||
"migration_timeout_ms": 30000,
|
||||
"migration_lock_timeout_ms": 10000
|
||||
},
|
||||
"shutdown_timeout_ms": 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile_id": "mainnet",
|
||||
"network": "mainnet-beta",
|
||||
"backend": "postgres",
|
||||
"postgres": {
|
||||
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
||||
"pool": {
|
||||
"max_connections": 8,
|
||||
"connect_timeout_ms": 10000,
|
||||
"wait_timeout_ms": 5000,
|
||||
"create_timeout_ms": 10000,
|
||||
"recycle_timeout_ms": 5000
|
||||
},
|
||||
"tls": {
|
||||
"mode": "verify_full"
|
||||
},
|
||||
"bootstrap": {
|
||||
"auto_migrate": true,
|
||||
"migration_timeout_ms": 30000,
|
||||
"migration_lock_timeout_ms": 10000
|
||||
},
|
||||
"shutdown_timeout_ms": 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile_id": "testnet",
|
||||
"network": "testnet",
|
||||
"backend": "postgres",
|
||||
"postgres": {
|
||||
"connection_uri": "${KSP_SECRET_STORE_TESTNET_POSTGRES_URI:-postgresql://localhost/ksp_testnet}",
|
||||
"pool": {
|
||||
"max_connections": 8,
|
||||
"connect_timeout_ms": 10000,
|
||||
"wait_timeout_ms": 5000,
|
||||
"create_timeout_ms": 10000,
|
||||
"recycle_timeout_ms": 5000
|
||||
},
|
||||
"tls": {
|
||||
"mode": "verify_full"
|
||||
},
|
||||
"bootstrap": {
|
||||
"auto_migrate": true,
|
||||
"migration_timeout_ms": 30000,
|
||||
"migration_lock_timeout_ms": 10000
|
||||
},
|
||||
"shutdown_timeout_ms": 5000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -56,11 +56,13 @@
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
|
||||
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
//! Desktop build/shell contract audits for Config Desk.
|
||||
|
||||
@@ -110,7 +110,7 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_activates_shared_writab
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some(), "packaged Config resources map must exist");
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 11);
|
||||
assert_eq!(resources.len(), 13);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
|
||||
@@ -124,6 +124,11 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_activates_shared_writab
|
||||
resources.get("../../config/schemas/std.offchain_transport.schema.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/schemas/std.offchain_transport.schema.json"),
|
||||
);
|
||||
assert_eq!(resources.get("../../config/std.store.json").and_then(serde_json::Value::as_str), std::option::Option::Some("config/std.store.json"),);
|
||||
assert_eq!(
|
||||
resources.get("../../config/schemas/std.store.schema.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/schemas/std.store.schema.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/schemas/std.wallet.schema.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/schemas/std.wallet.schema.json"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
#[test]
|
||||
fn profile_inventory_exposes_registered_profile_documents() {
|
||||
@@ -15,6 +15,9 @@ fn profile_inventory_exposes_registered_profile_documents() {
|
||||
assert!(inventory.iter().any(|document| -> bool {
|
||||
return document.file_id == ksp_config_lib::FILE_ID_STD_TRANSPORT;
|
||||
}));
|
||||
assert!(inventory.iter().any(|document| -> bool {
|
||||
return document.file_id == ksp_config_lib::FILE_ID_STD_STORE;
|
||||
}));
|
||||
assert!(inventory.iter().any(|document| -> bool {
|
||||
return document.file_id == ksp_config_lib::FILE_ID_STD_WALLET;
|
||||
}));
|
||||
|
||||
@@ -56,11 +56,13 @@
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
|
||||
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Desktop scaffold, shared-template and Config packaging contract audits for SOL Prices Desk `0.2.12`.
|
||||
|
||||
@@ -85,19 +85,21 @@ fn pre_002_package_is_mixed_lib_bin_and_frontend_is_scaffold_only() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_packaging_contains_current_eleven_config_resources() {
|
||||
fn pre_004_packaging_contains_current_thirteen_config_resources() {
|
||||
let root = app_root();
|
||||
let tauri = read_json(root.join("tauri.conf.json").as_path());
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some());
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 11);
|
||||
assert_eq!(resources.len(), 13);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
|
||||
);
|
||||
assert!(resources.contains_key("../../config/std.offchain_transport.json"));
|
||||
assert!(resources.contains_key("../../config/schemas/std.offchain_transport.schema.json"));
|
||||
assert!(resources.contains_key("../../config/std.store.json"));
|
||||
assert!(resources.contains_key("../../config/schemas/std.store.schema.json"));
|
||||
}
|
||||
let tauri_source = read_text(root.join("src/tauri.rs").as_path());
|
||||
assert!(tauri_source.contains("ksp_config_lib::prepare_packaged_runtime"));
|
||||
|
||||
@@ -56,11 +56,13 @@
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
|
||||
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
|
||||
// version: 30
|
||||
// version: 31
|
||||
|
||||
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
|
||||
|
||||
@@ -432,7 +432,7 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some(), "packaged Wallet Desk Config resources map must exist");
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 11);
|
||||
assert_eq!(resources.len(), 13);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
|
||||
@@ -449,6 +449,11 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi
|
||||
resources.get("../../config/std.offchain_transport.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/std.offchain_transport.json"),
|
||||
);
|
||||
assert_eq!(resources.get("../../config/std.store.json").and_then(serde_json::Value::as_str), std::option::Option::Some("config/std.store.json"),);
|
||||
assert_eq!(
|
||||
resources.get("../../config/schemas/std.store.schema.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/schemas/std.store.schema.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/schemas/std.offchain_transport.schema.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/schemas/std.offchain_transport.schema.json"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Release-wide deterministic compliance canaries for Wallet Desk.
|
||||
|
||||
@@ -199,7 +199,7 @@ fn packaged_resources_include_only_registered_config_sources_and_schemas() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(resources.len(), 11);
|
||||
assert_eq!(resources.len(), 13);
|
||||
for (source, destination) in resources {
|
||||
let destination = destination.as_str();
|
||||
assert!(destination.is_some(), "resource destination must be textual");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-config-lib/Cargo.toml
|
||||
# version: 9
|
||||
# version: 10
|
||||
|
||||
[package]
|
||||
name = "ksp-config-lib"
|
||||
@@ -14,6 +14,7 @@ ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
ksp-offchain-transport-lib = { path = "../ksp-offchain-transport-lib" }
|
||||
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
|
||||
ksp-store-lib = { path = "../ksp-store-lib", default-features = false }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-config-lib/README.md -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 11 -->
|
||||
|
||||
# ksp-config-lib
|
||||
|
||||
@@ -25,6 +25,7 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
|
||||
- l'adapter du document Logging effectif vers `ksp_logging_lib::LoggingSettings` ;
|
||||
- l'adapter du document Transport V1/V2/V3 vers `HttpTransportSettings`, `WsTransportSettings` et, en V3, `YellowstoneGrpcTransportSettings`, y compris redaction/provenance des URLs `KSP_SECRET_*` ;
|
||||
- l'adapter de `cfg.std.offchain_transport` vers `ksp_offchain_transport_lib::MarketPriceService`, avec contrôle de provenance des credentials/public fields et sans rendre les limites provider configurables ;
|
||||
- l'adapter de `cfg.std.store` vers `ksp_store_lib::StoreSettings`, avec sélection d'un target nommé, réseau explicite et URI PostgreSQL à provenance `Secret` ;
|
||||
- la surface de management pour inspecter et réparer les sources Config enregistrées, modifier `std.logging.json`, consulter les rapports d'environnement, révéler explicitement une valeur réelle et modifier `.env` ;
|
||||
- les écritures atomiques JSON/`.env` et la protection des permissions `.env` ;
|
||||
- les audits workspace empêchant les bypass d'ownership Config et les oublis dans `.env.example`.
|
||||
@@ -34,14 +35,17 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
|
||||
Le registre par défaut connaît :
|
||||
|
||||
```text
|
||||
cfg.composite.ksp-app-wallet-desk -> config/composite.ksp-app-wallet-desk.json
|
||||
cfg.composite.ksp-app-solprices-desk -> config/composite.ksp-app-solprices-desk.json
|
||||
cfg.composite.ksp-app-wallet-desk -> config/composite.ksp-app-wallet-desk.json
|
||||
cfg.std.logging -> config/std.logging.json
|
||||
cfg.std.offchain_transport -> config/std.offchain_transport.json
|
||||
cfg.std.store -> config/std.store.json
|
||||
cfg.std.transport -> config/std.transport.json
|
||||
cfg.std.wallet -> config/std.wallet.json
|
||||
schema.composite -> config/schemas/composite.schema.json
|
||||
schema.std.logging -> config/schemas/std.logging.schema.json
|
||||
schema.std.offchain_transport -> config/schemas/std.offchain_transport.schema.json
|
||||
schema.std.store -> config/schemas/std.store.schema.json
|
||||
schema.std.transport -> config/schemas/std.transport.schema.json
|
||||
schema.std.wallet -> config/schemas/std.wallet.schema.json
|
||||
```
|
||||
@@ -50,7 +54,7 @@ schema.std.wallet -> config/schemas/std.wallet.schema.json
|
||||
|
||||
`ConfigManagement::read_source()` permet d'inspecter le texte brut d'un document Config enregistré même lorsque ce document est invalide. `save_source_candidate()` complète cette frontière : le candidat brut est parsé, validé contre son schema et les invariants sémantiques KSP, puis persisté atomiquement uniquement après validation complète. Le `file_id` doit appartenir au registre et désigner un document Config ; aucun path arbitraire n'est accepté.
|
||||
|
||||
`config/examples/composite.example.json` conserve l’exemple générique. `config/composite.ksp-app-wallet-desk.json` est le premier composite runtime concret : il sélectionne Logging, Transport et Wallet par `file_id`, sans dépendre de leurs filenames physiques.
|
||||
`config/examples/composite.example.json` conserve l’exemple générique. Les composites runtime committed restent possédés par Config : `config/composite.ksp-app-solprices-desk.json` sélectionne Logging + Off-chain Transport, tandis que `config/composite.ksp-app-wallet-desk.json` sélectionne Logging + Off-chain Transport + On-chain Transport + Wallet. Tous référencent leurs documents par `file_id`, sans dépendre de filenames physiques.
|
||||
|
||||
Le fichier local d'environnement est :
|
||||
|
||||
@@ -68,11 +72,11 @@ Les autres crates et applications KSP ne doivent pas :
|
||||
- parser ou écrire directement `.env` ;
|
||||
- ouvrir directement les documents Config connus par leur filename physique ;
|
||||
- réimplémenter la sélection de profils, les compositions ou les placeholders ;
|
||||
- reconstruire elles-mêmes la configuration Logging, On-chain Transport, Off-chain Transport ou Wallet depuis le JSON.
|
||||
- reconstruire elles-mêmes la configuration Logging, On-chain Transport, Off-chain Transport, Store ou Wallet depuis le JSON.
|
||||
|
||||
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, de `ksp-onchain-transport-lib` pour construire le contrat runtime On-chain Transport et de `ksp-offchain-transport-lib` pour construire le service market-price dans la direction Config -> Transport. Le document Wallet reste un contrat de chemins/profils Config et n’introduit aucune dépendance Config -> `ksp-wallet-lib`.
|
||||
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, de `ksp-onchain-transport-lib` pour construire le contrat runtime On-chain Transport et de `ksp-offchain-transport-lib` pour construire le service market-price dans la direction Config -> Transport et de `ksp-store-lib` avec `default-features = false` pour construire les settings Store dans la direction Config -> Store sans forcer un backend physique. Le document Wallet reste un contrat de chemins/profils Config et n’introduit aucune dépendance Config -> `ksp-wallet-lib`.
|
||||
|
||||
La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib`, `ksp-onchain-transport-lib` et `ksp-offchain-transport-lib` ne dépendent pas de Config.
|
||||
La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib`, `ksp-onchain-transport-lib`, `ksp-offchain-transport-lib` et les crates Store ne dépendent pas de Config.
|
||||
|
||||
Config ne possède pas le `LoggingGuard`. L'application ou le service qui orchestre le runtime construit la configuration effective puis possède le lifecycle `ksp_logging_lib::initialize/reinitialize`.
|
||||
|
||||
@@ -84,7 +88,7 @@ Un secret reste accessible au runtime ou au management lorsqu'un consumer autori
|
||||
|
||||
Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées.
|
||||
|
||||
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport accepte les valeurs secrètes pour les URLs HTTP/WebSocket et, en V3, pour `grpc_endpoints[].secret_metadata[]` : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. Les metadata gRPC publiques et secrètes sont séparées et leur provenance Config est contrôlée avant mapping. `std.offchain_transport` exige une provenance `Secret` pour les API keys effectives et une provenance `Public` pour la paire DexScreener lorsqu'elle vient de l'environnement ; il ne permet ni URL provider arbitraire ni override de rate limit. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
|
||||
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport accepte les valeurs secrètes pour les URLs HTTP/WebSocket et, en V3, pour `grpc_endpoints[].secret_metadata[]` : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. Les metadata gRPC publiques et secrètes sont séparées et leur provenance Config est contrôlée avant mapping. `std.offchain_transport` exige une provenance `Secret` pour les API keys effectives et une provenance `Public` pour la paire DexScreener lorsqu'elle vient de l'environnement ; il ne permet ni URL provider arbitraire ni override de rate limit. `std.store` exige une provenance `Secret` pour chaque URI PostgreSQL effective et conserve des targets réseau-spécifiques indépendants (`devnet`, `mainnet`, `testnet`) sans exposer l'URI dans les projections sûres. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -94,6 +98,8 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur
|
||||
- [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ;
|
||||
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard Transport V3 HTTP + WebSocket + Yellowstone gRPC, avec lecture backward des V1/V2 ;
|
||||
- [`../../config/std.offchain_transport.json`](../../config/std.offchain_transport.json) — document standard Off-chain Transport V1, actuellement limité au domaine `market_price` SOL/USD ;
|
||||
- [`../../config/std.store.json`](../../config/std.store.json) — targets Store PostgreSQL Devnet/Mainnet/Testnet et settings runtime bornés ;
|
||||
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
|
||||
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Transport/Wallet de Wallet Desk ;
|
||||
- [`../../config/composite.ksp-app-solprices-desk.json`](../../config/composite.ksp-app-solprices-desk.json) — composition Logging/Off-chain Transport de SOL Prices Desk ;
|
||||
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Off-chain Transport/On-chain Transport/Wallet de Wallet Desk ;
|
||||
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
||||
<!-- version: 13 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# Utilisation de ksp-config-lib
|
||||
|
||||
@@ -28,8 +28,11 @@ Les arguments compris par Config sont :
|
||||
```text
|
||||
--cfgpath=/path/to/config
|
||||
--schemapath=/path/to/schemas
|
||||
--filemap=cfg.composite.ksp-app-solprices-desk=my-solprices-desk.json
|
||||
--filemap=cfg.composite.ksp-app-wallet-desk=my-wallet-desk.json
|
||||
--filemap=cfg.std.logging=my-logging.json
|
||||
--filemap=cfg.std.offchain_transport=my-offchain-transport.json
|
||||
--filemap=cfg.std.store=my-store.json
|
||||
--filemap=cfg.std.transport=my-transport.json
|
||||
--filemap=cfg.std.wallet=my-wallet.json
|
||||
```
|
||||
@@ -212,7 +215,56 @@ Config ne permet pas de fournir `base_url`, `endpoint_url`, `rate_limit` ou `req
|
||||
ksp-config-lib -> ksp-offchain-transport-lib
|
||||
```
|
||||
|
||||
Off-chain Transport ne lit ni `.env`, ni `KSP_*`, ni les documents Config. Une application telle que la future `ksp-app-solprices-desk` peut recevoir le service déjà composé puis utiliser uniquement `registry()`, `refresh`, `refresh_many` et `refresh_all`.
|
||||
Off-chain Transport ne lit ni `.env`, ni `KSP_*`, ni les documents Config. `ksp-app-solprices-desk` reçoit le service déjà composé puis utilise uniquement la surface provider-neutral `registry()`, `refresh`, `refresh_many` et `refresh_all`.
|
||||
|
||||
### 4.4 Construire le Store depuis Config
|
||||
|
||||
`cfg.std.store` définit des targets nommés. Chaque target sélectionne exactement un réseau logique, un backend et une URI PostgreSQL distincte. Config résout les secrets puis construit le contrat backend-neutral `ksp_store_lib::StoreSettings` sans activer la feature PostgreSQL du consumer :
|
||||
|
||||
```rust
|
||||
let store_config = match engine.load_resolved_store_config(
|
||||
std::option::Option::Some("devnet"),
|
||||
&environment,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
let target_id = store_config.target_id();
|
||||
let network = store_config.settings().network();
|
||||
let _ = (target_id, network);
|
||||
|
||||
let store_settings = store_config.into_settings();
|
||||
let store = match ksp_store_lib::Store::open(store_settings).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let _health = store.health().await;
|
||||
let closed = store.close().await;
|
||||
if let std::result::Result::Err(error) = closed {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
```
|
||||
|
||||
Targets committed :
|
||||
|
||||
```text
|
||||
devnet -> network devnet -> KSP_SECRET_STORE_DEVNET_POSTGRES_URI
|
||||
mainnet -> network mainnet-beta -> KSP_SECRET_STORE_MAINNET_POSTGRES_URI
|
||||
testnet -> network testnet -> KSP_SECRET_STORE_TESTNET_POSTGRES_URI
|
||||
```
|
||||
|
||||
`default_profile = "devnet"` choisit un seul target. La sélection d'un autre target se fait par le `profile_id` explicite ; `ksp-store-lib` ne multiplexe pas plusieurs bases ou réseaux dans une même instance.
|
||||
|
||||
Chaque `connection_uri` doit provenir d'un placeholder `KSP_SECRET_*`/`KSPB_SECRET_*`. Une URI littérale ou issue d'une variable non secrète est rejetée par l'adapter effectif. La valeur réelle est transmise au runtime Store, mais `ResolvedStoreConfig`, `StoreSettings` et les projections sûres ne l'affichent pas.
|
||||
|
||||
La direction de dépendance reste :
|
||||
|
||||
```text
|
||||
ksp-config-lib -> ksp-store-lib (default-features = false)
|
||||
ksp-store-lib -X-> ksp-config-lib
|
||||
ksp-store-postgres-lib -X-> ksp-config-lib
|
||||
```
|
||||
|
||||
## 5. Profils et composites
|
||||
|
||||
@@ -237,7 +289,7 @@ let component = match composite.component("wallet") {
|
||||
let wallet = engine.resolve_wallet_config_profile(component.resolved(), &environment);
|
||||
```
|
||||
|
||||
La même forme existe pour Logging via `resolve_logging_config_profile`. Le composite concret `cfg.composite.ksp-app-wallet-desk` référence actuellement `logging`, `transport` et `wallet`; Wallet Desk valide ces trois frontières au bootstrap.
|
||||
La même forme existe pour Logging via `resolve_logging_config_profile`. Le composite `cfg.composite.ksp-app-solprices-desk` référence `logging` et `offchain_transport`. Le composite `cfg.composite.ksp-app-wallet-desk` référence `logging`, `offchain_transport`, `transport` et `wallet`; chaque application valide ses frontières de composition au bootstrap.
|
||||
|
||||
## 6. Management de `std.logging.json`
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 20
|
||||
// version: 21
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -9,7 +9,7 @@
|
||||
//!
|
||||
//! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and
|
||||
//! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and
|
||||
//! provenance. Standard Logging, on-chain Transport (HTTP/WebSocket/Yellowstone gRPC) and Wallet documents map explicitly to their runtime consumer
|
||||
//! provenance. Standard Logging, on-chain Transport (HTTP/WebSocket/Yellowstone gRPC), Store and Wallet documents map explicitly to their runtime consumer
|
||||
//! contracts, while the management surface provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic
|
||||
//! JSON/`.env` persistence.
|
||||
|
||||
@@ -27,6 +27,7 @@ mod persistence;
|
||||
mod profile;
|
||||
mod registry;
|
||||
mod sensitivity;
|
||||
mod store;
|
||||
mod transport;
|
||||
mod wallet;
|
||||
|
||||
@@ -164,6 +165,10 @@ pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
|
||||
pub use self::registry::DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME;
|
||||
/// Default physical filename for the standard Off-chain Transport JSON Schema document.
|
||||
pub use self::registry::DEFAULT_STD_OFFCHAIN_TRANSPORT_SCHEMA_FILENAME;
|
||||
/// Default physical filename for the standard Store configuration document.
|
||||
pub use self::registry::DEFAULT_STD_STORE_FILENAME;
|
||||
/// Default physical filename for the standard Store JSON Schema document.
|
||||
pub use self::registry::DEFAULT_STD_STORE_SCHEMA_FILENAME;
|
||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
|
||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||
@@ -182,6 +187,8 @@ pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
|
||||
/// Logical file identifier for the standard Off-chain Transport JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT;
|
||||
/// Logical file identifier for the standard Store JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_STORE;
|
||||
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
|
||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||
@@ -190,6 +197,8 @@ pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
|
||||
pub use self::registry::FILE_ID_STD_LOGGING;
|
||||
/// Logical file identifier for the standard Off-chain Transport configuration document.
|
||||
pub use self::registry::FILE_ID_STD_OFFCHAIN_TRANSPORT;
|
||||
/// Logical file identifier for the standard Store configuration document.
|
||||
pub use self::registry::FILE_ID_STD_STORE;
|
||||
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||
pub use self::registry::FILE_ID_STD_TRANSPORT;
|
||||
/// Logical file identifier for the standard Wallet configuration document.
|
||||
@@ -204,6 +213,8 @@ pub use self::sensitivity::REDACTED_CONFIG_VALUE;
|
||||
pub use self::sensitivity::ResolvedConfigJson;
|
||||
/// One resolved Config string preserving real/safe representations and provenance.
|
||||
pub use self::sensitivity::ResolvedConfigText;
|
||||
/// Effective standard Store configuration mapped to backend-neutral Store settings.
|
||||
pub use self::store::ResolvedStoreConfig;
|
||||
/// Effective standard Transport configuration mapped to HTTP plus optional WebSocket and Yellowstone gRPC runtime settings.
|
||||
pub use self::transport::ResolvedTransportConfig;
|
||||
/// Effective standard Wallet configuration resolved to validated filesystem roots.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/registry.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
||||
@@ -17,6 +17,10 @@ pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
|
||||
pub const DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME: &str = "std.offchain_transport.json";
|
||||
/// Default physical filename for the standard Off-chain Transport JSON Schema document.
|
||||
pub const DEFAULT_STD_OFFCHAIN_TRANSPORT_SCHEMA_FILENAME: &str = "std.offchain_transport.schema.json";
|
||||
/// Default physical filename for the standard Store configuration document.
|
||||
pub const DEFAULT_STD_STORE_FILENAME: &str = "std.store.json";
|
||||
/// Default physical filename for the standard Store JSON Schema document.
|
||||
pub const DEFAULT_STD_STORE_SCHEMA_FILENAME: &str = "std.store.schema.json";
|
||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
|
||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||
@@ -35,6 +39,8 @@ pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
|
||||
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
|
||||
/// Logical file identifier for the standard Off-chain Transport JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT: &str = "schema.std.offchain_transport";
|
||||
/// Logical file identifier for the standard Store JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_STORE: &str = "schema.std.store";
|
||||
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
|
||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||
@@ -43,6 +49,8 @@ pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
|
||||
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
|
||||
/// Logical file identifier for the standard Off-chain Transport configuration document.
|
||||
pub const FILE_ID_STD_OFFCHAIN_TRANSPORT: &str = "cfg.std.offchain_transport";
|
||||
/// Logical file identifier for the standard Store configuration document.
|
||||
pub const FILE_ID_STD_STORE: &str = "cfg.std.store";
|
||||
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
|
||||
/// Logical file identifier for the standard Wallet configuration document.
|
||||
@@ -214,6 +222,22 @@ impl ConfigFileRegistry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let store = ConfigFileDescriptor::new(
|
||||
FILE_ID_STD_STORE,
|
||||
ConfigFileKind::Config,
|
||||
DEFAULT_STD_STORE_FILENAME,
|
||||
std::option::Option::Some(FILE_ID_SCHEMA_STD_STORE),
|
||||
);
|
||||
let store = match store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let store_schema =
|
||||
ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_STORE, ConfigFileKind::Schema, DEFAULT_STD_STORE_SCHEMA_FILENAME, std::option::Option::None);
|
||||
let store_schema = match store_schema {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let transport = ConfigFileDescriptor::new(
|
||||
FILE_ID_STD_TRANSPORT,
|
||||
ConfigFileKind::Config,
|
||||
@@ -254,6 +278,8 @@ impl ConfigFileRegistry {
|
||||
logging_schema,
|
||||
offchain_transport,
|
||||
offchain_transport_schema,
|
||||
store,
|
||||
store_schema,
|
||||
transport,
|
||||
transport_schema,
|
||||
wallet,
|
||||
|
||||
280
crates/ksp-config-lib/src/store.rs
Normal file
280
crates/ksp-config-lib/src/store.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
// file: crates/ksp-config-lib/src/store.rs
|
||||
// version: 2
|
||||
|
||||
/// Effective standard Store configuration mapped to `ksp_store_lib::StoreSettings`.
|
||||
pub struct ResolvedStoreConfig {
|
||||
effective: crate::ResolvedConfigJson,
|
||||
file_id: crate::ConfigFileId,
|
||||
profile_id: String,
|
||||
selection_source: crate::ConfigProfileSelectionSource,
|
||||
settings: ksp_store_lib::StoreSettings,
|
||||
source_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl ResolvedStoreConfig {
|
||||
/// Returns the detailed environment-resolved Config view.
|
||||
#[must_use]
|
||||
pub const fn effective(&self) -> &crate::ResolvedConfigJson {
|
||||
return &self.effective;
|
||||
}
|
||||
|
||||
/// Returns the logical Config file identifier used by this runtime configuration.
|
||||
#[must_use]
|
||||
pub const fn file_id(&self) -> &crate::ConfigFileId {
|
||||
return &self.file_id;
|
||||
}
|
||||
|
||||
/// Returns the selected standard Store profile identifier.
|
||||
///
|
||||
/// For `std.store`, the profile identifier is also the stable named Store target identifier.
|
||||
#[must_use]
|
||||
pub fn profile_id(&self) -> &str {
|
||||
return self.profile_id.as_str();
|
||||
}
|
||||
|
||||
/// Returns the selected named Store target identifier.
|
||||
#[must_use]
|
||||
pub fn target_id(&self) -> &str {
|
||||
return self.profile_id.as_str();
|
||||
}
|
||||
|
||||
/// Returns the source that selected the standard Store profile.
|
||||
#[must_use]
|
||||
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
|
||||
return self.selection_source;
|
||||
}
|
||||
|
||||
/// Borrows the backend-neutral Store settings without exposing the connection URI.
|
||||
#[must_use]
|
||||
pub const fn settings(&self) -> &ksp_store_lib::StoreSettings {
|
||||
return &self.settings;
|
||||
}
|
||||
|
||||
/// Consumes the resolved Config and returns the Store-owned runtime settings.
|
||||
#[must_use]
|
||||
pub fn into_settings(self) -> ksp_store_lib::StoreSettings {
|
||||
return self.settings;
|
||||
}
|
||||
|
||||
/// Returns the physical source Config document path.
|
||||
#[must_use]
|
||||
pub fn source_path(&self) -> &std::path::Path {
|
||||
return self.source_path.as_path();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ResolvedStoreConfig {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("ResolvedStoreConfig")
|
||||
.field("effective", &self.effective)
|
||||
.field("file_id", &self.file_id)
|
||||
.field("profile_id", &self.profile_id)
|
||||
.field("selection_source", &self.selection_source)
|
||||
.field("settings", &self.settings)
|
||||
.field("source_path", &self.source_path)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::ConfigDocumentEngine {
|
||||
/// Loads `std.store`, resolves one profile/environment and maps it to backend-neutral Store settings.
|
||||
pub fn load_resolved_store_config(
|
||||
&self,
|
||||
requested_profile: std::option::Option<&str>,
|
||||
environment: &crate::ConfigEnvironment,
|
||||
) -> ksp_core_lib::Result<ResolvedStoreConfig> {
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_STORE);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let profile = self.load_resolved_profile(&file_id, requested_profile);
|
||||
let profile = match profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return resolve_store_profile(&profile, environment);
|
||||
}
|
||||
|
||||
/// Maps an already resolved `cfg.std.store` profile while preserving its selection provenance.
|
||||
pub fn resolve_store_config_profile(
|
||||
&self,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
environment: &crate::ConfigEnvironment,
|
||||
) -> ksp_core_lib::Result<ResolvedStoreConfig> {
|
||||
if profile.file_id().as_str() != crate::FILE_ID_STD_STORE {
|
||||
return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Store document"));
|
||||
}
|
||||
let descriptor = self.registry().descriptor(profile.file_id());
|
||||
if let std::result::Result::Err(error) = descriptor {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return resolve_store_profile(profile, environment);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveStoreSource {
|
||||
backend: String,
|
||||
format_version: u32,
|
||||
network: String,
|
||||
postgres: EffectivePostgresSource,
|
||||
profile_id: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectivePostgresSource {
|
||||
bootstrap: EffectivePostgresBootstrapSource,
|
||||
connection_uri: String,
|
||||
pool: EffectivePostgresPoolSource,
|
||||
shutdown_timeout_ms: u64,
|
||||
tls: EffectivePostgresTlsSource,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectivePostgresPoolSource {
|
||||
connect_timeout_ms: u64,
|
||||
create_timeout_ms: u64,
|
||||
max_connections: u32,
|
||||
recycle_timeout_ms: u64,
|
||||
wait_timeout_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectivePostgresTlsSource {
|
||||
mode: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectivePostgresBootstrapSource {
|
||||
auto_migrate: bool,
|
||||
migration_lock_timeout_ms: u64,
|
||||
migration_timeout_ms: u64,
|
||||
}
|
||||
|
||||
fn resolve_store_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedStoreConfig> {
|
||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapping standard Store Config profile");
|
||||
let effective = profile.resolve_effective_environment_detailed(environment);
|
||||
let effective = match effective {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let provenance = validate_connection_uri_provenance(&effective, profile);
|
||||
if let std::result::Result::Err(error) = provenance {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let source = serde_json::from_value::<EffectiveStoreSource>(effective.value().clone());
|
||||
let source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
effective_error(profile, "effective Store Config cannot be decoded into the runtime adapter contract").with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
if source.format_version != 1 {
|
||||
return std::result::Result::Err(effective_error(profile, "effective Store format_version is unsupported"));
|
||||
}
|
||||
if source.profile_id != profile.profile_id() {
|
||||
return std::result::Result::Err(effective_error(profile, "effective Store profile_id does not match the selected profile"));
|
||||
}
|
||||
if source.backend != "postgres" {
|
||||
return std::result::Result::Err(effective_error(profile, "effective Store backend is unsupported").with_context("backend", source.backend));
|
||||
}
|
||||
let tls_mode = match source.postgres.tls.mode.as_str() {
|
||||
"disabled" => ksp_store_lib::PostgresTlsMode::Disabled,
|
||||
"verify_full" => ksp_store_lib::PostgresTlsMode::VerifyFull,
|
||||
_ => return std::result::Result::Err(effective_error(profile, "effective Store PostgreSQL TLS mode is unsupported")),
|
||||
};
|
||||
let pool = ksp_store_lib::PostgresPoolSettings::new(
|
||||
source.postgres.pool.max_connections,
|
||||
std::time::Duration::from_millis(source.postgres.pool.connect_timeout_ms),
|
||||
std::time::Duration::from_millis(source.postgres.pool.wait_timeout_ms),
|
||||
std::time::Duration::from_millis(source.postgres.pool.create_timeout_ms),
|
||||
std::time::Duration::from_millis(source.postgres.pool.recycle_timeout_ms),
|
||||
);
|
||||
let bootstrap = ksp_store_lib::PostgresBootstrapSettings::new(
|
||||
source.postgres.bootstrap.auto_migrate,
|
||||
std::time::Duration::from_millis(source.postgres.bootstrap.migration_timeout_ms),
|
||||
std::time::Duration::from_millis(source.postgres.bootstrap.migration_lock_timeout_ms),
|
||||
);
|
||||
let network = ksp_store_lib::RawNetworkId::new(source.network);
|
||||
let network = match network {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(effective_error(profile, "effective Store network identifier is invalid")),
|
||||
};
|
||||
let postgres = ksp_store_lib::PostgresStoreSettings::new(source.postgres.connection_uri, pool, tls_mode, bootstrap);
|
||||
let settings = ksp_store_lib::StoreSettings::new(
|
||||
network,
|
||||
ksp_store_lib::StoreBackendSettings::Postgres(postgres),
|
||||
std::time::Duration::from_millis(source.postgres.shutdown_timeout_ms),
|
||||
);
|
||||
if let std::result::Result::Err(error) = settings.validate() {
|
||||
return std::result::Result::Err(store_contract_error(profile, &error));
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
profile_id = profile.profile_id(),
|
||||
network = settings.network().as_str(),
|
||||
backend = settings.backend_kind().code(),
|
||||
"mapped standard Store Config to Store settings"
|
||||
);
|
||||
return std::result::Result::Ok(ResolvedStoreConfig {
|
||||
effective,
|
||||
file_id: profile.file_id().clone(),
|
||||
profile_id: profile.profile_id().to_owned(),
|
||||
selection_source: profile.selection_source(),
|
||||
settings,
|
||||
source_path: profile.path().to_path_buf(),
|
||||
});
|
||||
}
|
||||
|
||||
fn validate_connection_uri_provenance(effective: &crate::ResolvedConfigJson, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result<()> {
|
||||
let provenance = match effective.provenance_at("/postgres/connection_uri") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Store PostgreSQL connection URI provenance is unavailable")),
|
||||
};
|
||||
let mut has_secret_environment = false;
|
||||
for item in provenance {
|
||||
let variable_name = match item.variable_name() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name);
|
||||
let sensitivity = match sensitivity {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !sensitivity.is_secret() {
|
||||
return std::result::Result::Err(effective_error(profile, "Store PostgreSQL connection URI may reference only secret environment variables"));
|
||||
}
|
||||
has_secret_environment = true;
|
||||
}
|
||||
if !has_secret_environment {
|
||||
return std::result::Result::Err(effective_error(profile, "Store PostgreSQL connection URI requires secret environment provenance"));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn store_contract_error(profile: &crate::ResolvedConfigProfile, error: &ksp_core_lib::Error) -> ksp_core_lib::Error {
|
||||
return effective_error(profile, "effective Store settings fail the Store runtime contract")
|
||||
.with_context("store_error_domain", error.code().domain())
|
||||
.with_context("store_error_code", error.code().code());
|
||||
}
|
||||
|
||||
fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract")
|
||||
.with_context("file_id", profile.file_id().as_str())
|
||||
.with_context("profile_id", profile.profile_id())
|
||||
.with_context("reason", reason);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/store.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/tests/ownership.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
//! Workspace ownership audits for KSP application configuration boundaries.
|
||||
|
||||
@@ -280,6 +280,49 @@ fn workspace_crates_do_not_hardcode_config_managed_physical_files() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_config_adapter_does_not_force_backend_feature_or_reverse_dependency() {
|
||||
let root = workspace_root();
|
||||
let config_manifest_path = root.join("crates/ksp-config-lib/Cargo.toml");
|
||||
let config_manifest = std::fs::read_to_string(config_manifest_path.as_path());
|
||||
assert!(config_manifest.is_ok(), "unable to read {}", config_manifest_path.display());
|
||||
if let std::result::Result::Ok(config_manifest) = config_manifest {
|
||||
assert!(
|
||||
config_manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }"),
|
||||
"Config -> Store dependency must not force a physical backend feature"
|
||||
);
|
||||
}
|
||||
for crate_name in ["ksp-store-lib", "ksp-store-postgres-lib"] {
|
||||
let manifest_path = root.join("crates").join(crate_name).join("Cargo.toml");
|
||||
let manifest = std::fs::read_to_string(manifest_path.as_path());
|
||||
assert!(manifest.is_ok(), "unable to read {}", manifest_path.display());
|
||||
if let std::result::Result::Ok(manifest) = manifest {
|
||||
assert!(!manifest.contains("ksp-config-lib"), "{} must not depend back on Config", manifest_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_runtime_sources_do_not_bypass_config_for_environment_or_libpq_files() {
|
||||
let root = workspace_root();
|
||||
for crate_name in ["ksp-store-lib", "ksp-store-postgres-lib"] {
|
||||
let source_root = root.join("crates").join(crate_name).join("src");
|
||||
let mut rust_files = std::vec::Vec::new();
|
||||
collect_rust_files(source_root.as_path(), &mut rust_files);
|
||||
for rust_file in rust_files {
|
||||
let source = std::fs::read_to_string(rust_file.as_path());
|
||||
assert!(source.is_ok(), "unable to read {}", rust_file.display());
|
||||
let source = match source {
|
||||
std::result::Result::Ok(value) => non_comment_source(value.as_str()),
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
for forbidden in ["std::env", "dotenv", "KSP_", "KSPB_", "\"PG", ".pgpass"] {
|
||||
assert!(!source.contains(forbidden), "{} bypasses Config through forbidden Store environment/libpq token {forbidden}", rust_file.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_name_scanner_ignores_namespace_labels_but_keeps_concrete_names() {
|
||||
let source = r#"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
||||
// version: 25
|
||||
// version: 26
|
||||
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
|
||||
//! Logging/Transport adapters and management contracts.
|
||||
//! Logging/Transport/Store adapters and management contracts.
|
||||
|
||||
#[test]
|
||||
fn bootstrap_contract_is_available_from_crate_root() {
|
||||
@@ -80,19 +80,21 @@ fn registry_descriptor_inventory_is_available_from_crate_root() {
|
||||
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
|
||||
assert_eq!(descriptors.len(), 11);
|
||||
assert_eq!(descriptors.len(), 13);
|
||||
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
|
||||
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[10].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
|
||||
let schema_file_id = descriptors[5].schema_file_id();
|
||||
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_STD_STORE);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[10].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_STORE);
|
||||
assert_eq!(descriptors[11].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[12].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
|
||||
let schema_file_id = descriptors[6].schema_file_id();
|
||||
assert!(schema_file_id.is_some(), "public Wallet descriptor should preserve schema association");
|
||||
if let std::option::Option::Some(schema_file_id) = schema_file_id {
|
||||
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
|
||||
@@ -211,6 +213,18 @@ fn logging_adapter_contract_is_available_from_crate_root() {
|
||||
assert!(std::mem::size_of::<ksp_config_lib::ResolvedLoggingConfig>() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_adapter_contract_is_available_from_crate_root() {
|
||||
let adapter = ksp_config_lib::ConfigDocumentEngine::load_resolved_store_config;
|
||||
let composite_adapter = ksp_config_lib::ConfigDocumentEngine::resolve_store_config_profile;
|
||||
let _ = (adapter, composite_adapter);
|
||||
assert_eq!(ksp_config_lib::FILE_ID_STD_STORE, "cfg.std.store");
|
||||
assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_STORE, "schema.std.store");
|
||||
assert_eq!(ksp_config_lib::DEFAULT_STD_STORE_FILENAME, "std.store.json");
|
||||
assert_eq!(ksp_config_lib::DEFAULT_STD_STORE_SCHEMA_FILENAME, "std.store.schema.json");
|
||||
assert!(std::mem::size_of::<ksp_config_lib::ResolvedStoreConfig>() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn management_contracts_are_available_from_crate_root() {
|
||||
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
|
||||
78
crates/ksp-config-lib/unit_tests/fixtures/std.store.json
Normal file
78
crates/ksp-config-lib/unit_tests/fixtures/std.store.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"default_profile": "devnet",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "devnet",
|
||||
"network": "devnet",
|
||||
"backend": "postgres",
|
||||
"postgres": {
|
||||
"connection_uri": "${KSP_SECRET_STORE_DEVNET_POSTGRES_URI:-postgresql://localhost/ksp_devnet}",
|
||||
"pool": {
|
||||
"max_connections": 8,
|
||||
"connect_timeout_ms": 10000,
|
||||
"wait_timeout_ms": 5000,
|
||||
"create_timeout_ms": 10000,
|
||||
"recycle_timeout_ms": 5000
|
||||
},
|
||||
"tls": {
|
||||
"mode": "verify_full"
|
||||
},
|
||||
"bootstrap": {
|
||||
"auto_migrate": true,
|
||||
"migration_timeout_ms": 30000,
|
||||
"migration_lock_timeout_ms": 10000
|
||||
},
|
||||
"shutdown_timeout_ms": 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile_id": "mainnet",
|
||||
"network": "mainnet-beta",
|
||||
"backend": "postgres",
|
||||
"postgres": {
|
||||
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
||||
"pool": {
|
||||
"max_connections": 8,
|
||||
"connect_timeout_ms": 10000,
|
||||
"wait_timeout_ms": 5000,
|
||||
"create_timeout_ms": 10000,
|
||||
"recycle_timeout_ms": 5000
|
||||
},
|
||||
"tls": {
|
||||
"mode": "verify_full"
|
||||
},
|
||||
"bootstrap": {
|
||||
"auto_migrate": true,
|
||||
"migration_timeout_ms": 30000,
|
||||
"migration_lock_timeout_ms": 10000
|
||||
},
|
||||
"shutdown_timeout_ms": 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile_id": "testnet",
|
||||
"network": "testnet",
|
||||
"backend": "postgres",
|
||||
"postgres": {
|
||||
"connection_uri": "${KSP_SECRET_STORE_TESTNET_POSTGRES_URI:-postgresql://localhost/ksp_testnet}",
|
||||
"pool": {
|
||||
"max_connections": 8,
|
||||
"connect_timeout_ms": 10000,
|
||||
"wait_timeout_ms": 5000,
|
||||
"create_timeout_ms": 10000,
|
||||
"recycle_timeout_ms": 5000
|
||||
},
|
||||
"tls": {
|
||||
"mode": "verify_full"
|
||||
},
|
||||
"bootstrap": {
|
||||
"auto_migrate": true,
|
||||
"migration_timeout_ms": 30000,
|
||||
"migration_lock_timeout_ms": 10000
|
||||
},
|
||||
"shutdown_timeout_ms": 5000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/registry.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
#[test]
|
||||
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
||||
@@ -7,7 +7,7 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect();
|
||||
assert_eq!(descriptors.len(), 11);
|
||||
assert_eq!(descriptors.len(), 13);
|
||||
assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
|
||||
assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME));
|
||||
assert_eq!(descriptors[0].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
|
||||
@@ -15,17 +15,19 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
||||
assert_eq!(descriptors[1].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME));
|
||||
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[5].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
|
||||
assert_eq!(descriptors[5].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
|
||||
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[10].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
|
||||
assert!(descriptors[0..6].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
|
||||
assert!(descriptors[6..11].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
|
||||
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_STORE);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[6].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
|
||||
assert_eq!(descriptors[6].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
|
||||
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[10].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_STORE);
|
||||
assert_eq!(descriptors[11].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[12].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
|
||||
assert!(descriptors[0..7].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
|
||||
assert!(descriptors[7..13].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +116,27 @@ fn defaults_register_offchain_transport_document_and_schema_with_distinct_roots(
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_register_store_document_and_schema_with_distinct_roots() {
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
assert!(registry.is_ok());
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let config_id = crate::ConfigFileId::new(crate::FILE_ID_STD_STORE);
|
||||
let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_STORE);
|
||||
if let (std::result::Result::Ok(config_id), std::result::Result::Ok(schema_id)) = (config_id, schema_id) {
|
||||
let config = registry.descriptor(&config_id);
|
||||
let schema = registry.descriptor(&schema_id);
|
||||
if let (std::result::Result::Ok(config), std::result::Result::Ok(schema)) = (config, schema) {
|
||||
assert_eq!(config.kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(config.filename(), std::path::Path::new(crate::DEFAULT_STD_STORE_FILENAME));
|
||||
assert_eq!(config.schema_file_id(), std::option::Option::Some(&schema_id));
|
||||
assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
|
||||
assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_STD_STORE_SCHEMA_FILENAME));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_register_transport_document_and_schema_with_distinct_roots() {
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
|
||||
199
crates/ksp-config-lib/unit_tests/store.rs
Normal file
199
crates/ksp-config-lib/unit_tests/store.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/store.rs
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn committed_store_profile_maps_exact_runtime_settings_and_secret_fallback() {
|
||||
let engine = committed_engine();
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let resolved = engine.load_resolved_store_config(std::option::Option::None, &environment);
|
||||
assert!(resolved.is_ok(), "committed Store profile should map without opening PostgreSQL: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_STORE);
|
||||
assert_eq!(resolved.profile_id(), "devnet");
|
||||
assert_eq!(resolved.target_id(), "devnet");
|
||||
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
|
||||
assert_eq!(resolved.settings().backend_kind(), ksp_store_lib::StoreBackendKind::Postgres);
|
||||
assert_eq!(resolved.settings().network().as_str(), "devnet");
|
||||
assert_eq!(resolved.settings().shutdown_timeout(), std::time::Duration::from_millis(5_000));
|
||||
let postgres = match resolved.settings().backend() {
|
||||
ksp_store_lib::StoreBackendSettings::Postgres(postgres) => std::option::Option::Some(postgres),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
assert!(postgres.is_some(), "pre.004 fixture should map to the PostgreSQL Store backend");
|
||||
if let std::option::Option::Some(postgres) = postgres {
|
||||
assert_eq!(postgres.pool().max_connections(), 8);
|
||||
assert_eq!(postgres.pool().connect_timeout(), std::time::Duration::from_millis(10_000));
|
||||
assert_eq!(postgres.pool().wait_timeout(), std::time::Duration::from_millis(5_000));
|
||||
assert_eq!(postgres.pool().create_timeout(), std::time::Duration::from_millis(10_000));
|
||||
assert_eq!(postgres.pool().recycle_timeout(), std::time::Duration::from_millis(5_000));
|
||||
assert_eq!(postgres.tls_mode(), ksp_store_lib::PostgresTlsMode::VerifyFull);
|
||||
assert!(postgres.bootstrap().auto_migrate());
|
||||
assert_eq!(postgres.bootstrap().migration_timeout(), std::time::Duration::from_millis(30_000));
|
||||
assert_eq!(postgres.bootstrap().migration_lock_timeout(), std::time::Duration::from_millis(10_000));
|
||||
}
|
||||
assert!(resolved.effective().sensitivity().is_secret());
|
||||
let safe = resolved.effective().safe_value().to_string();
|
||||
assert!(!safe.contains("postgresql://localhost/ksp_devnet"));
|
||||
assert!(safe.contains(crate::REDACTED_CONFIG_VALUE));
|
||||
let debug = format!("{resolved:?}");
|
||||
assert!(!debug.contains("postgresql://localhost/ksp_devnet"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_store_uri_wins_and_remains_redacted_in_safe_views() {
|
||||
let engine = committed_engine();
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let canary = "postgresql://secret-user:secret-pass@db.example/ksp_devnet";
|
||||
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||
process.insert("KSP_SECRET_STORE_DEVNET_POSTGRES_URI".to_owned(), canary.to_owned());
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
let resolved = engine.load_resolved_store_config(std::option::Option::None, &environment);
|
||||
assert!(resolved.is_ok(), "secret process Store URI should map: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.effective().value().pointer("/postgres/connection_uri").and_then(serde_json::Value::as_str), std::option::Option::Some(canary));
|
||||
assert!(!resolved.effective().safe_value().to_string().contains(canary));
|
||||
assert!(!format!("{resolved:?}").contains(canary));
|
||||
let provenance = resolved.effective().provenance_at("/postgres/connection_uri");
|
||||
assert!(provenance.is_some());
|
||||
if let std::option::Option::Some(provenance) = provenance {
|
||||
assert!(provenance.iter().any(|item| return item.environment_source() == std::option::Option::Some(crate::ConfigEnvironmentSource::Process)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_or_nonsecret_store_uri_is_rejected_by_effective_adapter() {
|
||||
for value in ["postgresql://literal.invalid/ksp", "${KSP_PUBLIC_STORE_POSTGRES_URI:-postgresql://public.invalid/ksp}"] {
|
||||
let fixture = tempfile::tempdir();
|
||||
assert!(fixture.is_ok());
|
||||
let fixture = match fixture {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let source = committed_document_value();
|
||||
let mut source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let profiles = source.get_mut("profiles").and_then(serde_json::Value::as_array_mut);
|
||||
if let std::option::Option::Some(profiles) = profiles
|
||||
&& let std::option::Option::Some(profile) = profiles.first_mut()
|
||||
{
|
||||
profile["postgres"]["connection_uri"] = serde_json::Value::String(value.to_owned());
|
||||
}
|
||||
let engine = fixture_engine_with_document(fixture.path(), &source);
|
||||
assert!(engine.is_ok());
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let resolved = engine.load_resolved_store_config(std::option::Option::None, &environment);
|
||||
assert!(resolved.is_err(), "Store URI without secret provenance must be rejected");
|
||||
if let std::result::Result::Err(error) = resolved {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
|
||||
assert!(!format!("{error:?}").contains("literal.invalid"));
|
||||
assert!(!format!("{error:?}").contains("public.invalid"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_store_targets_select_one_network_and_database_without_runtime_multiplexing() {
|
||||
let engine = committed_engine();
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||
process.insert("KSP_SECRET_STORE_DEVNET_POSTGRES_URI".to_owned(), "postgresql://devnet.invalid/ksp_devnet".to_owned());
|
||||
process.insert("KSP_SECRET_STORE_MAINNET_POSTGRES_URI".to_owned(), "postgresql://mainnet.invalid/ksp_mainnet".to_owned());
|
||||
process.insert("KSP_SECRET_STORE_TESTNET_POSTGRES_URI".to_owned(), "postgresql://testnet.invalid/ksp_testnet".to_owned());
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
for (target_id, network, expected_uri) in [
|
||||
("devnet", "devnet", "postgresql://devnet.invalid/ksp_devnet"),
|
||||
("mainnet", "mainnet-beta", "postgresql://mainnet.invalid/ksp_mainnet"),
|
||||
("testnet", "testnet", "postgresql://testnet.invalid/ksp_testnet"),
|
||||
] {
|
||||
let resolved = engine.load_resolved_store_config(std::option::Option::Some(target_id), &environment);
|
||||
assert!(resolved.is_ok(), "named Store target should resolve independently: {target_id}: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.target_id(), target_id);
|
||||
assert_eq!(resolved.profile_id(), target_id);
|
||||
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
|
||||
assert_eq!(resolved.settings().network().as_str(), network);
|
||||
assert_eq!(
|
||||
resolved.effective().value().pointer("/postgres/connection_uri").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some(expected_uri),
|
||||
);
|
||||
assert!(!resolved.effective().safe_value().to_string().contains(expected_uri));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||
let workspace = workspace_root();
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let registry = match registry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
|
||||
}
|
||||
|
||||
fn fixture_engine_with_document(root: &std::path::Path, document: &serde_json::Value) -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||
let config_root = root.join("config");
|
||||
if let std::result::Result::Err(error) = std::fs::create_dir_all(config_root.as_path()) {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "test Config root cannot be created").with_source(error),
|
||||
);
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(document);
|
||||
let bytes = match bytes {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_SYNTAX_INVALID, "test Store Config cannot be encoded").with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let path = config_root.join(crate::DEFAULT_STD_STORE_FILENAME);
|
||||
if let std::result::Result::Err(error) = std::fs::write(path.as_path(), bytes) {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "test Store Config cannot be written").with_source(error),
|
||||
);
|
||||
}
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(config_root, workspace_root().join("config/schemas"));
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let registry = match registry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
|
||||
}
|
||||
|
||||
fn committed_document_value() -> std::result::Result<serde_json::Value, serde_json::Error> {
|
||||
return serde_json::from_str(include_str!("../../../config/std.store.json"));
|
||||
}
|
||||
|
||||
fn workspace_root() -> std::path::PathBuf {
|
||||
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
}
|
||||
20
crates/ksp-store-lib/Cargo.toml
Normal file
20
crates/ksp-store-lib/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
# file: crates/ksp-store-lib/Cargo.toml
|
||||
# version: 2
|
||||
|
||||
[package]
|
||||
name = "ksp-store-lib"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["postgres"]
|
||||
postgres = ["dep:ksp-store-postgres-lib"]
|
||||
|
||||
[dependencies]
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
ksp-store-api = { path = "../ksp-store-api" }
|
||||
ksp-store-postgres-lib = { path = "../ksp-store-postgres-lib", optional = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
95
crates/ksp-store-lib/README.md
Normal file
95
crates/ksp-store-lib/README.md
Normal file
@@ -0,0 +1,95 @@
|
||||
<!-- file: crates/ksp-store-lib/README.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# ksp-store-lib
|
||||
|
||||
`ksp-store-lib` est la façade runtime Store commune de KSP.
|
||||
|
||||
Elle expose aux consumers une surface backend-neutral, réexporte les contrats RAW de `ksp-store-api`, sélectionne uniquement les backends compilés et masque leurs objets physiques. Le backend PostgreSQL officiel est activé par défaut via la feature `postgres` et reste implémenté dans `ksp-store-postgres-lib`.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
`ksp-store-lib` possède :
|
||||
|
||||
- `StoreSettings`, avec un réseau logique unique, un backend sélectionné et un timeout de fermeture borné ;
|
||||
- les settings PostgreSQL publics KSP-owned : pool, TLS, bootstrap/migrations et URI sensible ;
|
||||
- la feature `postgres` par défaut et le comportement explicite `backend_not_compiled` lorsque PostgreSQL est sélectionné sans cette feature ;
|
||||
- `Store::open`, qui ne retourne une instance qu'après validation, ouverture physique du backend compilé et bootstrap/history réussis ;
|
||||
- `Store::runtime_snapshot()` pour les compteurs runtime sûrs sans I/O ;
|
||||
- `Store::health().await` pour la readiness portable et bornée ;
|
||||
- `Store::close(self).await` pour la fermeture explicite bornée ;
|
||||
- le mapping des erreurs backend vers des codes Store stables sans exposer les erreurs physiques ;
|
||||
- les réexports crate-root de `ksp-store-api` nécessaires aux consumers ordinaires.
|
||||
|
||||
## Une instance = un réseau
|
||||
|
||||
Une instance `Store` représente exactement :
|
||||
|
||||
```text
|
||||
1 Store = 1 RawNetworkId + 1 backend physique sélectionné
|
||||
```
|
||||
|
||||
Le runtime Store n'est pas un multiplexeur multi-database ou multi-réseau. La sélection d'un target nommé appartient à Config. Le document `std.store` peut donc définir plusieurs targets indépendants — par exemple Devnet, Mainnet et Testnet — mais un appel à `Store::open` reçoit les settings d'un seul target.
|
||||
|
||||
Cette séparation permet d'utiliser des bases PostgreSQL distinctes par réseau tout en conservant le réseau dans l'identité logique des données RAW.
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
Avec la feature par défaut :
|
||||
|
||||
```text
|
||||
ksp-store-lib
|
||||
-> ksp-store-api
|
||||
-> ksp-logging-lib
|
||||
-> ksp-store-postgres-lib
|
||||
```
|
||||
|
||||
`ksp-store-lib` ne réexporte aucun type `tokio-postgres`, Deadpool ou Rustls.
|
||||
|
||||
Les modes TLS publics sont volontairement limités à :
|
||||
|
||||
```text
|
||||
Disabled
|
||||
VerifyFull
|
||||
```
|
||||
|
||||
`VerifyFull` impose TLS avec vérification de la chaîne et de l'identité serveur. La policy typée Store prime sur les paramètres TLS présents dans l'URI.
|
||||
|
||||
## Config et secrets
|
||||
|
||||
Store ne lit ni `.env`, ni variables `KSP_*` / `KSPB_*`, ni variables/fichiers implicites libpq (`PG*`, `.pgpass`, fichiers TLS PostgreSQL).
|
||||
|
||||
`ksp-config-lib` possède `std.store`, la résolution des secrets et la sélection du target. Il construit ensuite un `StoreSettings` backend-neutral. L'URI PostgreSQL reste nécessaire au runtime mais n'a aucun getter public dans `ksp-store-lib` et son `Debug` est redacted.
|
||||
|
||||
Les targets committed sont actuellement :
|
||||
|
||||
```text
|
||||
devnet -> network devnet -> base indépendante
|
||||
mainnet -> network mainnet-beta -> base indépendante
|
||||
testnet -> network testnet -> base indépendante
|
||||
```
|
||||
|
||||
Les credentials restent dans les variables `KSP_SECRET_STORE_*_POSTGRES_URI` ou le `.env` possédé par Config.
|
||||
|
||||
## Surface actuelle et hors périmètre
|
||||
|
||||
La fondation runtime ne fournit encore aucune implémentation PostgreSQL des capabilities métier RAW de `ksp-store-api`.
|
||||
|
||||
Sont volontairement hors de cette surface :
|
||||
|
||||
- persistence/query/rétention PostgreSQL de `RawTransaction` ;
|
||||
- persistence/query/rétention PostgreSQL de `RawAccountState` ;
|
||||
- batch-size, priorité, backlog ou policy de worker/job ;
|
||||
- transport d'acquisition, Program decoding et materialization ;
|
||||
- exposition publique de SQL, pool, client, row, statement ou transaction PostgreSQL.
|
||||
|
||||
Les premières vertical slices métier sont ajoutées séparément afin que la façade runtime reste stable et backend-neutral.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`USAGE.md`](USAGE.md) — construction des settings, ouverture, health et fermeture ;
|
||||
- [`../ksp-store-postgres-lib/README.md`](../ksp-store-postgres-lib/README.md) — responsabilité du backend PostgreSQL physique ;
|
||||
- [`../../config/std.store.json`](../../config/std.store.json) — targets Store committed ;
|
||||
- [`../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md`](../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md) — architecture durable Store ;
|
||||
- [`../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md`](../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md) — plan de fondation ;
|
||||
- [`../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md`](../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md) — matrice de validation.
|
||||
219
crates/ksp-store-lib/USAGE.md
Normal file
219
crates/ksp-store-lib/USAGE.md
Normal file
@@ -0,0 +1,219 @@
|
||||
<!-- file: crates/ksp-store-lib/USAGE.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Utilisation de ksp-store-lib
|
||||
|
||||
## 1. Dépendance et features
|
||||
|
||||
Le consumer runtime normal dépend uniquement de la façade :
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ksp-store-lib = { path = "../ksp-store-lib" }
|
||||
```
|
||||
|
||||
La feature par défaut est :
|
||||
|
||||
```text
|
||||
postgres
|
||||
```
|
||||
|
||||
Pour construire un binaire sans backend physique :
|
||||
|
||||
```toml
|
||||
ksp-store-lib = { path = "../ksp-store-lib", default-features = false }
|
||||
```
|
||||
|
||||
Dans ce mode, le type PostgreSQL reste connu par la surface de settings mais `Store::open` retourne `ERROR_CODE_BACKEND_NOT_COMPILED` avant toute I/O si PostgreSQL est sélectionné.
|
||||
|
||||
Un consumer ordinaire ne dépend pas directement de `ksp-store-postgres-lib`.
|
||||
|
||||
## 2. Construire des settings PostgreSQL programmatiquement
|
||||
|
||||
La construction directe est utile pour les tests, outils internes ou compositions qui n'utilisent pas `ksp-config-lib`.
|
||||
|
||||
```rust
|
||||
fn programmatic_store_settings(connection_uri: std::string::String) -> ksp_store_lib::Result<ksp_store_lib::StoreSettings> {
|
||||
let network = ksp_store_lib::RawNetworkId::new("devnet");
|
||||
let network = match network {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
let postgres = ksp_store_lib::PostgresStoreSettings::new(
|
||||
connection_uri,
|
||||
ksp_store_lib::PostgresPoolSettings::default(),
|
||||
ksp_store_lib::PostgresTlsMode::VerifyFull,
|
||||
ksp_store_lib::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
|
||||
let settings = ksp_store_lib::StoreSettings::with_default_shutdown(
|
||||
network,
|
||||
ksp_store_lib::StoreBackendSettings::Postgres(postgres),
|
||||
);
|
||||
|
||||
let validation = settings.validate();
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
|
||||
return std::result::Result::Ok(settings);
|
||||
}
|
||||
```
|
||||
|
||||
`PostgresStoreSettings` ne fournit volontairement aucun getter public de l'URI. Son `Debug` remplace cette valeur par `<redacted>`.
|
||||
|
||||
## 3. Ouvrir et fermer un Store
|
||||
|
||||
`Store::open` est async et ne retourne un succès qu'après que le backend compilé a prouvé sa fondation runtime.
|
||||
|
||||
```rust
|
||||
async fn use_store(settings: ksp_store_lib::StoreSettings) -> ksp_store_lib::Result<()> {
|
||||
let store = ksp_store_lib::Store::open(settings).await;
|
||||
let store = match store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
let runtime = store.runtime_snapshot();
|
||||
let _network = runtime.network();
|
||||
let _capacity = runtime.pool_capacity();
|
||||
let _size = runtime.pool_size();
|
||||
let _available = runtime.pool_available();
|
||||
let _waiting = runtime.pool_waiting();
|
||||
|
||||
let health = store.health().await;
|
||||
match health.state() {
|
||||
ksp_store_lib::StoreHealthState::Ready => {}
|
||||
ksp_store_lib::StoreHealthState::NotReady => {
|
||||
let _safe_error_code = health.last_error_code();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
return store.close().await;
|
||||
}
|
||||
```
|
||||
|
||||
`Store::close(self)` consomme l'instance afin qu'une fermeture explicite ne puisse pas être suivie d'une nouvelle opération via la même valeur.
|
||||
|
||||
## 4. Construire les settings depuis Config
|
||||
|
||||
Le chemin applicatif recommandé utilise `ksp-config-lib`, propriétaire du document `std.store`, de `.env` et des secrets.
|
||||
|
||||
Après construction du `ConfigDocumentEngine` :
|
||||
|
||||
```rust
|
||||
fn resolve_store_settings(
|
||||
engine: &ksp_config_lib::ConfigDocumentEngine,
|
||||
environment: &ksp_config_lib::ConfigEnvironment,
|
||||
target: std::option::Option<&str>,
|
||||
) -> ksp_core_lib::Result<ksp_store_lib::StoreSettings> {
|
||||
let resolved = engine.load_resolved_store_config(target, environment);
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
return std::result::Result::Ok(resolved.into_settings());
|
||||
}
|
||||
```
|
||||
|
||||
Targets committed :
|
||||
|
||||
```text
|
||||
devnet -> RawNetworkId("devnet")
|
||||
mainnet -> RawNetworkId("mainnet-beta")
|
||||
testnet -> RawNetworkId("testnet")
|
||||
```
|
||||
|
||||
Chaque target peut utiliser une URI PostgreSQL distincte. `default_profile` sélectionne un seul target ; `Store` ne route pas automatiquement entre plusieurs targets.
|
||||
|
||||
## 5. Settings disponibles
|
||||
|
||||
### `PostgresPoolSettings`
|
||||
|
||||
Valeurs par défaut :
|
||||
|
||||
```text
|
||||
max_connections 8
|
||||
connect_timeout 10 s
|
||||
wait_timeout 5 s
|
||||
create_timeout 10 s
|
||||
recycle_timeout 5 s
|
||||
```
|
||||
|
||||
Les getters sont :
|
||||
|
||||
```text
|
||||
max_connections()
|
||||
connect_timeout()
|
||||
wait_timeout()
|
||||
create_timeout()
|
||||
recycle_timeout()
|
||||
```
|
||||
|
||||
`validate()` vérifie les bornes sans I/O.
|
||||
|
||||
### `PostgresBootstrapSettings`
|
||||
|
||||
Valeurs par défaut :
|
||||
|
||||
```text
|
||||
auto_migrate true
|
||||
migration_timeout 30 s
|
||||
migration_lock_timeout 10 s
|
||||
```
|
||||
|
||||
Getters :
|
||||
|
||||
```text
|
||||
auto_migrate()
|
||||
migration_timeout()
|
||||
migration_lock_timeout()
|
||||
```
|
||||
|
||||
### `StoreSettings`
|
||||
|
||||
La surface expose :
|
||||
|
||||
```text
|
||||
backend()
|
||||
backend_kind()
|
||||
network()
|
||||
shutdown_timeout()
|
||||
validate()
|
||||
```
|
||||
|
||||
`StoreSettings::new` permet de choisir explicitement le timeout de shutdown. `StoreSettings::with_default_shutdown` utilise la borne commune par défaut de 5 secondes.
|
||||
|
||||
## 6. Health et diagnostics
|
||||
|
||||
`StoreRuntimeSnapshot` est synchrone et ne déclenche aucune I/O. Il expose uniquement :
|
||||
|
||||
```text
|
||||
backend_kind
|
||||
network
|
||||
pool_capacity
|
||||
pool_size
|
||||
pool_available
|
||||
pool_waiting
|
||||
```
|
||||
|
||||
`StoreHealthSnapshot` ajoute une probe async bornée :
|
||||
|
||||
```text
|
||||
state = Ready | NotReady
|
||||
migration_version
|
||||
pending_migration_count
|
||||
last_error_code
|
||||
runtime snapshot
|
||||
```
|
||||
|
||||
Aucun snapshot n'expose URI, host, user, database, SQL, handle backend ou texte d'erreur PostgreSQL.
|
||||
|
||||
## 7. Limite fonctionnelle actuelle
|
||||
|
||||
`ksp-store-lib` réexporte les modèles et traits RAW de `ksp-store-api`, mais le backend PostgreSQL de la fondation n'implémente encore aucune capability `RawTransaction*` ou `RawAccount*`.
|
||||
|
||||
Les consumers ne doivent donc pas interpréter la disponibilité du runtime PostgreSQL comme une persistence métier déjà présente.
|
||||
7
crates/ksp-store-lib/src/constants.rs
Normal file
7
crates/ksp-store-lib/src/constants.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// file: crates/ksp-store-lib/src/constants.rs
|
||||
// version: 2
|
||||
|
||||
//! Store facade-owned constants.
|
||||
|
||||
/// Owning tracing target reserved for events emitted by the common Store runtime facade.
|
||||
pub(crate) const TRACING_TARGET: &str = "ksp-store-lib";
|
||||
29
crates/ksp-store-lib/src/error.rs
Normal file
29
crates/ksp-store-lib/src/error.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
// file: crates/ksp-store-lib/src/error.rs
|
||||
// version: 4
|
||||
|
||||
/// Error code reserved for operations attempted after a Store backend has entered its closed state.
|
||||
pub const ERROR_CODE_BACKEND_CLOSED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_closed");
|
||||
/// Error code used when a known Store backend was selected but its Cargo feature is not compiled.
|
||||
pub const ERROR_CODE_BACKEND_NOT_COMPILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_not_compiled");
|
||||
/// Error code used when a compiled Store backend cannot complete its bounded opening lifecycle.
|
||||
pub const ERROR_CODE_BACKEND_OPEN_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_open_failed");
|
||||
/// Error code used when the PostgreSQL backend rejects or cannot normalize its physical connection configuration.
|
||||
pub const ERROR_CODE_POSTGRES_CONFIG_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_config_invalid");
|
||||
/// Error code used when PostgreSQL physical connection establishment fails without exposing remote or credential details.
|
||||
pub const ERROR_CODE_POSTGRES_CONNECT_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_connect_failed");
|
||||
/// Error code used when a lightweight PostgreSQL health/readiness probe fails safely.
|
||||
pub const ERROR_CODE_POSTGRES_HEALTH_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_health_failed");
|
||||
/// Error code used when PostgreSQL migration/bootstrap execution fails without exposing server text or SQL.
|
||||
pub const ERROR_CODE_POSTGRES_MIGRATION_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_migration_failed");
|
||||
/// Error code used when persisted PostgreSQL migration history diverges from the embedded immutable KSP history.
|
||||
pub const ERROR_CODE_POSTGRES_MIGRATION_MISMATCH: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_migration_mismatch");
|
||||
/// Error code used when a bounded PostgreSQL pool wait, create or recycle operation reaches its deadline.
|
||||
pub const ERROR_CODE_POSTGRES_POOL_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_pool_timeout");
|
||||
/// Error code used when PostgreSQL history contains a migration newer than this Store runtime understands.
|
||||
pub const ERROR_CODE_POSTGRES_SCHEMA_NEWER: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_schema_newer");
|
||||
/// Error code used when verified PostgreSQL TLS setup or negotiation cannot be completed safely.
|
||||
pub const ERROR_CODE_POSTGRES_TLS_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_tls_failed");
|
||||
/// Error code used when backend-neutral Store settings violate runtime bounds or invariants.
|
||||
pub const ERROR_CODE_SETTINGS_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "settings_invalid");
|
||||
/// Error code used when a Store cannot complete its explicit shutdown inside the configured bound.
|
||||
pub const ERROR_CODE_SHUTDOWN_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "shutdown_timeout");
|
||||
132
crates/ksp-store-lib/src/health.rs
Normal file
132
crates/ksp-store-lib/src/health.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
// file: crates/ksp-store-lib/src/health.rs
|
||||
// version: 1
|
||||
|
||||
/// Portable Store health state independent from the selected physical backend.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum StoreHealthState {
|
||||
/// The selected Store backend answered the bounded readiness probe and its migration foundation is current.
|
||||
Ready,
|
||||
/// The Store instance exists but its latest bounded readiness probe could not prove readiness.
|
||||
NotReady,
|
||||
}
|
||||
|
||||
/// Safe synchronous Store runtime snapshot without performing backend I/O.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct StoreRuntimeSnapshot {
|
||||
backend_kind: crate::StoreBackendKind,
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
pool_available: u32,
|
||||
pool_capacity: u32,
|
||||
pool_size: u32,
|
||||
pool_waiting: u32,
|
||||
}
|
||||
|
||||
impl StoreRuntimeSnapshot {
|
||||
/// Creates one portable runtime projection from backend-owned safe counters.
|
||||
#[must_use]
|
||||
pub(crate) fn new(
|
||||
backend_kind: crate::StoreBackendKind,
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
pool_capacity: u32,
|
||||
pool_size: u32,
|
||||
pool_available: u32,
|
||||
pool_waiting: u32,
|
||||
) -> Self {
|
||||
return Self { backend_kind, network, pool_available, pool_capacity, pool_size, pool_waiting };
|
||||
}
|
||||
|
||||
/// Returns the selected backend identity.
|
||||
#[must_use]
|
||||
pub const fn backend_kind(&self) -> crate::StoreBackendKind {
|
||||
return self.backend_kind;
|
||||
}
|
||||
|
||||
/// Returns the one logical network bound to this Store instance.
|
||||
#[must_use]
|
||||
pub const fn network(&self) -> &ksp_store_api::RawNetworkId {
|
||||
return &self.network;
|
||||
}
|
||||
|
||||
/// Returns the number of currently available pooled backend objects.
|
||||
#[must_use]
|
||||
pub const fn pool_available(&self) -> u32 {
|
||||
return self.pool_available;
|
||||
}
|
||||
|
||||
/// Returns the configured maximum pooled backend object count.
|
||||
#[must_use]
|
||||
pub const fn pool_capacity(&self) -> u32 {
|
||||
return self.pool_capacity;
|
||||
}
|
||||
|
||||
/// Returns the current pooled backend object count.
|
||||
#[must_use]
|
||||
pub const fn pool_size(&self) -> u32 {
|
||||
return self.pool_size;
|
||||
}
|
||||
|
||||
/// Returns the number of tasks currently waiting for a pooled backend object.
|
||||
#[must_use]
|
||||
pub const fn pool_waiting(&self) -> u32 {
|
||||
return self.pool_waiting;
|
||||
}
|
||||
}
|
||||
|
||||
/// Portable Store readiness projection containing only safe runtime and migration diagnostics.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct StoreHealthSnapshot {
|
||||
last_error_code: std::option::Option<ksp_store_api::ErrorCode>,
|
||||
migration_version: std::option::Option<u64>,
|
||||
pending_migration_count: u32,
|
||||
runtime: StoreRuntimeSnapshot,
|
||||
state: StoreHealthState,
|
||||
}
|
||||
|
||||
impl StoreHealthSnapshot {
|
||||
/// Creates one safe health projection from already classified backend diagnostics.
|
||||
#[must_use]
|
||||
pub(crate) fn new(
|
||||
state: StoreHealthState,
|
||||
runtime: StoreRuntimeSnapshot,
|
||||
migration_version: std::option::Option<u64>,
|
||||
pending_migration_count: u32,
|
||||
last_error_code: std::option::Option<ksp_store_api::ErrorCode>,
|
||||
) -> Self {
|
||||
return Self { last_error_code, migration_version, pending_migration_count, runtime, state };
|
||||
}
|
||||
|
||||
/// Returns the latest safe error code when readiness could not be proven.
|
||||
#[must_use]
|
||||
pub const fn last_error_code(&self) -> std::option::Option<ksp_store_api::ErrorCode> {
|
||||
return self.last_error_code;
|
||||
}
|
||||
|
||||
/// Returns the migration version observed by the readiness probe when available.
|
||||
#[must_use]
|
||||
pub const fn migration_version(&self) -> std::option::Option<u64> {
|
||||
return self.migration_version;
|
||||
}
|
||||
|
||||
/// Returns the number of embedded migrations newer than the observed applied version.
|
||||
#[must_use]
|
||||
pub const fn pending_migration_count(&self) -> u32 {
|
||||
return self.pending_migration_count;
|
||||
}
|
||||
|
||||
/// Returns the safe synchronous runtime projection captured for this health probe.
|
||||
#[must_use]
|
||||
pub const fn runtime(&self) -> &StoreRuntimeSnapshot {
|
||||
return &self.runtime;
|
||||
}
|
||||
|
||||
/// Returns whether this probe proved the Store ready.
|
||||
#[must_use]
|
||||
pub const fn state(&self) -> StoreHealthState {
|
||||
return self.state;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/health.rs"]
|
||||
mod tests;
|
||||
199
crates/ksp-store-lib/src/lib.rs
Normal file
199
crates/ksp-store-lib/src/lib.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
// file: crates/ksp-store-lib/src/lib.rs
|
||||
// version: 6
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Common backend-neutral Store runtime facade for KSP.
|
||||
//!
|
||||
//! `0.3.2-pre.007` closes the physical PostgreSQL runtime composition with a
|
||||
//! portable safe runtime snapshot and lightweight health/readiness projection,
|
||||
//! while retaining the private migration/bootstrap foundation and no business
|
||||
//! persistence schema.
|
||||
//!
|
||||
//! The default `postgres` feature compiles the official PostgreSQL backend as
|
||||
//! an optional implementation dependency. No backend implementation type is
|
||||
//! part of this crate's public surface.
|
||||
|
||||
mod constants;
|
||||
mod error;
|
||||
mod health;
|
||||
mod settings;
|
||||
mod store;
|
||||
|
||||
/// Error code reserved for operations attempted after a Store backend is closed.
|
||||
pub use self::error::ERROR_CODE_BACKEND_CLOSED;
|
||||
/// Error code used when a known Store backend is selected without its compiled feature.
|
||||
pub use self::error::ERROR_CODE_BACKEND_NOT_COMPILED;
|
||||
/// Error code used when a compiled Store backend cannot complete opening.
|
||||
pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED;
|
||||
/// Error code used when PostgreSQL physical configuration is malformed or unsupported.
|
||||
pub use self::error::ERROR_CODE_POSTGRES_CONFIG_INVALID;
|
||||
/// Error code used when PostgreSQL physical connection establishment fails.
|
||||
pub use self::error::ERROR_CODE_POSTGRES_CONNECT_FAILED;
|
||||
/// Error code used when a lightweight PostgreSQL health/readiness probe fails safely.
|
||||
pub use self::error::ERROR_CODE_POSTGRES_HEALTH_FAILED;
|
||||
/// Error code used when PostgreSQL migration/bootstrap execution fails safely.
|
||||
pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_FAILED;
|
||||
/// Error code used when PostgreSQL migration history diverges from the embedded immutable KSP history.
|
||||
pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH;
|
||||
/// Error code used when a bounded PostgreSQL pool operation reaches its deadline.
|
||||
pub use self::error::ERROR_CODE_POSTGRES_POOL_TIMEOUT;
|
||||
/// Error code used when PostgreSQL schema history is newer than this Store runtime.
|
||||
pub use self::error::ERROR_CODE_POSTGRES_SCHEMA_NEWER;
|
||||
/// Error code used when PostgreSQL verified TLS setup or negotiation fails.
|
||||
pub use self::error::ERROR_CODE_POSTGRES_TLS_FAILED;
|
||||
/// Error code used when Store settings violate backend-neutral bounds or invariants.
|
||||
pub use self::error::ERROR_CODE_SETTINGS_INVALID;
|
||||
/// Error code used when explicit Store shutdown exceeds its configured deadline.
|
||||
pub use self::error::ERROR_CODE_SHUTDOWN_TIMEOUT;
|
||||
/// Portable Store health/readiness projection containing only safe diagnostics.
|
||||
pub use self::health::StoreHealthSnapshot;
|
||||
/// Portable Store health state independent from physical backend types.
|
||||
pub use self::health::StoreHealthState;
|
||||
/// Safe synchronous Store runtime snapshot containing backend-neutral pool counters.
|
||||
pub use self::health::StoreRuntimeSnapshot;
|
||||
/// Bounded PostgreSQL bootstrap and migration settings owned by the Store facade.
|
||||
pub use self::settings::PostgresBootstrapSettings;
|
||||
/// Bounded PostgreSQL connection-pool settings owned by the Store facade.
|
||||
pub use self::settings::PostgresPoolSettings;
|
||||
/// PostgreSQL settings owned by the Store facade without exposing backend implementation types.
|
||||
pub use self::settings::PostgresStoreSettings;
|
||||
/// TLS policy accepted by the backend-neutral PostgreSQL settings surface.
|
||||
pub use self::settings::PostgresTlsMode;
|
||||
/// Backend identity understood independently from compiled Cargo features.
|
||||
pub use self::settings::StoreBackendKind;
|
||||
/// Backend-specific settings selected through the common Store facade.
|
||||
pub use self::settings::StoreBackendSettings;
|
||||
/// Complete backend-neutral settings consumed by the common Store runtime facade.
|
||||
pub use self::settings::StoreSettings;
|
||||
/// Opaque common Store runtime facade with consuming async shutdown.
|
||||
pub use self::store::Store;
|
||||
/// Error code used when a RAW write collides with divergent content for the same logical identity.
|
||||
pub use ksp_store_api::ERROR_CODE_RAW_CONFLICT;
|
||||
/// Error code used when a RAW Store model violates one of its backend-agnostic invariants.
|
||||
pub use ksp_store_api::ERROR_CODE_RAW_MODEL_INVALID;
|
||||
/// Error code used when a KSP-owned RAW persistence payload violates its format or admission contract.
|
||||
pub use ksp_store_api::ERROR_CODE_RAW_PAYLOAD_INVALID;
|
||||
/// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent.
|
||||
pub use ksp_store_api::ERROR_CODE_RAW_PROVENANCE_INVALID;
|
||||
/// Error code used when one RAW query or cursor violates backend-agnostic query invariants.
|
||||
pub use ksp_store_api::ERROR_CODE_RAW_QUERY_INVALID;
|
||||
/// Error code used when a RAW retention transition violates the logical lifecycle contract.
|
||||
pub use ksp_store_api::ERROR_CODE_RAW_RETENTION_INVALID;
|
||||
/// Common KSP error type used by Store-facing contracts.
|
||||
pub use ksp_store_api::Error;
|
||||
/// Stable structured code identifying a KSP error category and condition.
|
||||
pub use ksp_store_api::ErrorCode;
|
||||
/// Structured contextual field attached to a KSP error.
|
||||
pub use ksp_store_api::ErrorContext;
|
||||
/// Maximum complete RAW account-data length admitted by the Store API.
|
||||
pub use ksp_store_api::MAX_RAW_ACCOUNT_DATA_BYTES;
|
||||
/// Maximum UTF-8 byte length accepted for one safe logical RAW/provenance code.
|
||||
pub use ksp_store_api::MAX_RAW_CODE_BYTES;
|
||||
/// Maximum opaque query cursor length admitted by the Store API.
|
||||
pub use ksp_store_api::MAX_RAW_PAGE_CURSOR_BYTES;
|
||||
/// Maximum KSP-owned canonical RAW payload admitted by the Store API.
|
||||
pub use ksp_store_api::MAX_RAW_PAYLOAD_BYTES;
|
||||
/// Maximum source-wire payload size recorded as acquisition metadata.
|
||||
pub use ksp_store_api::MAX_RAW_SOURCE_PAYLOAD_BYTES;
|
||||
/// Maximum supported Unix millisecond timestamp.
|
||||
pub use ksp_store_api::MAX_RAW_UNIX_MILLIS;
|
||||
/// Canonical Solana account address primitive shared by persistent models.
|
||||
pub use ksp_store_api::Pubkey;
|
||||
/// Persistable acquisition observation linked to one complete canonical RAW account state.
|
||||
pub use ksp_store_api::RawAccountObservation;
|
||||
/// Read capability for persisted RAW account-state observations.
|
||||
pub use ksp_store_api::RawAccountObservationRead;
|
||||
/// Write capability for additional observations of already persisted RAW account states.
|
||||
pub use ksp_store_api::RawAccountObservationWrite;
|
||||
/// Canonical complete N1 RAW account state independent from acquisition transport.
|
||||
pub use ksp_store_api::RawAccountState;
|
||||
/// Backend-independent list query for complete canonical RAW account states.
|
||||
pub use ksp_store_api::RawAccountStateQuery;
|
||||
/// Read capability for complete canonical RAW account states.
|
||||
pub use ksp_store_api::RawAccountStateRead;
|
||||
/// Durable backend-independent identity of one canonical RAW account state.
|
||||
pub use ksp_store_api::RawAccountStateReference;
|
||||
/// Write capability for complete canonical RAW account-state acquisitions.
|
||||
pub use ksp_store_api::RawAccountStateWrite;
|
||||
/// Origin category describing why one acquisition was performed.
|
||||
pub use ksp_store_api::RawAcquisitionOrigin;
|
||||
/// Safe source-independent acquisition provenance attached to one persisted observation.
|
||||
pub use ksp_store_api::RawAcquisitionProvenance;
|
||||
/// Combined outcome of one atomic canonical RAW entity plus observation acquisition.
|
||||
pub use ksp_store_api::RawAcquisitionWriteOutcome;
|
||||
/// Fixed-size digest identifying canonical or source bytes without retaining them.
|
||||
pub use ksp_store_api::RawContentHash;
|
||||
/// Outcome for one canonical RAW entity in an idempotent persistence operation.
|
||||
pub use ksp_store_api::RawEntityWriteOutcome;
|
||||
/// Bounded identifier of one KSP-owned source-independent RAW persistence format.
|
||||
pub use ksp_store_api::RawFormatId;
|
||||
/// Bounded logical network/cluster identifier used in backend-independent Store identities.
|
||||
pub use ksp_store_api::RawNetworkId;
|
||||
/// Stable deterministic idempotence key for one persisted acquisition observation.
|
||||
pub use ksp_store_api::RawObservationKey;
|
||||
/// Outcome for one deterministic acquisition observation write.
|
||||
pub use ksp_store_api::RawObservationWriteOutcome;
|
||||
/// One deterministic page of backend-independent Store results.
|
||||
pub use ksp_store_api::RawPage;
|
||||
/// Opaque backend-owned cursor returned by one deterministic Store query.
|
||||
pub use ksp_store_api::RawPageCursor;
|
||||
/// Caller-requested page size without an arbitrary KSP policy ceiling.
|
||||
pub use ksp_store_api::RawPageLimit;
|
||||
/// Opaque-cursor page request used by backend-independent list operations.
|
||||
pub use ksp_store_api::RawPageRequest;
|
||||
/// Bounded source-independent KSP RAW persistence payload.
|
||||
pub use ksp_store_api::RawPayload;
|
||||
/// Bounded logical code used by acquisition provenance fields.
|
||||
pub use ksp_store_api::RawProvenanceCode;
|
||||
/// Logical availability state of one canonical RAW payload.
|
||||
pub use ksp_store_api::RawRetentionState;
|
||||
/// Outcome of one atomic RAW retention transition.
|
||||
pub use ksp_store_api::RawRetentionWriteOutcome;
|
||||
/// Optional inclusive Solana slot bounds for one Store query.
|
||||
pub use ksp_store_api::RawSlotRange;
|
||||
/// Deterministic traversal direction for Store list queries.
|
||||
pub use ksp_store_api::RawSortDirection;
|
||||
/// Bounded UTC timestamp represented as whole milliseconds since Unix epoch.
|
||||
pub use ksp_store_api::RawTimestamp;
|
||||
/// Canonical source-independent N1 RAW transaction persisted by Store backends.
|
||||
pub use ksp_store_api::RawTransaction;
|
||||
/// Explicit write mode for canonical RAW transaction acquisitions.
|
||||
pub use ksp_store_api::RawTransactionAcquisitionMode;
|
||||
/// Persistable acquisition observation linked to one canonical RAW transaction.
|
||||
pub use ksp_store_api::RawTransactionObservation;
|
||||
/// Read capability for persisted RAW transaction observations.
|
||||
pub use ksp_store_api::RawTransactionObservationRead;
|
||||
/// Write capability for additional observations of already persisted RAW transactions.
|
||||
pub use ksp_store_api::RawTransactionObservationWrite;
|
||||
/// Backend-independent list query for canonical RAW transactions.
|
||||
pub use ksp_store_api::RawTransactionQuery;
|
||||
/// Read capability for canonical RAW transactions.
|
||||
pub use ksp_store_api::RawTransactionRead;
|
||||
/// Durable backend-independent identity of one canonical RAW transaction.
|
||||
pub use ksp_store_api::RawTransactionReference;
|
||||
/// Read capability for canonical RAW transaction retention metadata.
|
||||
pub use ksp_store_api::RawTransactionRetentionRead;
|
||||
/// Requested compare-and-transition operation for one RAW transaction retention state.
|
||||
pub use ksp_store_api::RawTransactionRetentionTransition;
|
||||
/// Write capability for policy-authorized RAW transaction retention transitions.
|
||||
pub use ksp_store_api::RawTransactionRetentionWrite;
|
||||
/// Canonical 64-byte Solana transaction signature used by Store identities.
|
||||
pub use ksp_store_api::RawTransactionSignature;
|
||||
/// Minimal durable identity retained after a canonical RAW transaction payload is purged.
|
||||
pub use ksp_store_api::RawTransactionTombstone;
|
||||
/// Write capability for canonical RAW transaction acquisitions.
|
||||
pub use ksp_store_api::RawTransactionWrite;
|
||||
/// Common KSP result alias using [`Error`].
|
||||
pub use ksp_store_api::Result;
|
||||
/// Boxed async operation returned by object-safe Store capability contracts.
|
||||
pub use ksp_store_api::StoreApiFuture;
|
||||
|
||||
/// Crate-owned tracing target reserved for Store runtime behavior.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
|
||||
// Keep the mandatory crate-owned tracing target part of the compiled scaffold
|
||||
// without inventing runtime logging before the first behavioral log site.
|
||||
const _: &str = crate::TRACING_TARGET;
|
||||
395
crates/ksp-store-lib/src/settings.rs
Normal file
395
crates/ksp-store-lib/src/settings.rs
Normal file
@@ -0,0 +1,395 @@
|
||||
// file: crates/ksp-store-lib/src/settings.rs
|
||||
// version: 3
|
||||
|
||||
const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000;
|
||||
const DEFAULT_MAX_CONNECTIONS: u32 = 8;
|
||||
const DEFAULT_MIGRATION_LOCK_TIMEOUT_MS: u64 = 10_000;
|
||||
const DEFAULT_MIGRATION_TIMEOUT_MS: u64 = 30_000;
|
||||
const DEFAULT_POOL_CREATE_TIMEOUT_MS: u64 = 10_000;
|
||||
const DEFAULT_POOL_RECYCLE_TIMEOUT_MS: u64 = 5_000;
|
||||
const DEFAULT_POOL_WAIT_TIMEOUT_MS: u64 = 5_000;
|
||||
const DEFAULT_SHUTDOWN_TIMEOUT_MS: u64 = 5_000;
|
||||
const MAX_CONNECTIONS: u32 = 64;
|
||||
const MAX_CONNECT_TIMEOUT_MS: u64 = 60_000;
|
||||
const MAX_MIGRATION_LOCK_TIMEOUT_MS: u64 = 120_000;
|
||||
const MAX_MIGRATION_TIMEOUT_MS: u64 = 300_000;
|
||||
const MAX_POOL_CREATE_TIMEOUT_MS: u64 = 60_000;
|
||||
const MAX_POOL_RECYCLE_TIMEOUT_MS: u64 = 60_000;
|
||||
const MAX_POOL_WAIT_TIMEOUT_MS: u64 = 60_000;
|
||||
const MAX_SHUTDOWN_TIMEOUT_MS: u64 = 30_000;
|
||||
const MIN_CONNECTIONS: u32 = 1;
|
||||
const MIN_CONNECT_TIMEOUT_MS: u64 = 100;
|
||||
const MIN_MIGRATION_LOCK_TIMEOUT_MS: u64 = 100;
|
||||
const MIN_MIGRATION_TIMEOUT_MS: u64 = 1_000;
|
||||
const MIN_POOL_CREATE_TIMEOUT_MS: u64 = 100;
|
||||
const MIN_POOL_RECYCLE_TIMEOUT_MS: u64 = 100;
|
||||
const MIN_POOL_WAIT_TIMEOUT_MS: u64 = 100;
|
||||
const MIN_SHUTDOWN_TIMEOUT_MS: u64 = 100;
|
||||
|
||||
/// Backend identity understood by the common Store runtime independently from compiled Cargo features.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum StoreBackendKind {
|
||||
/// Official PostgreSQL Store backend.
|
||||
Postgres,
|
||||
}
|
||||
|
||||
impl StoreBackendKind {
|
||||
/// Returns the stable safe backend code used in diagnostics and configuration mapping.
|
||||
#[must_use]
|
||||
pub const fn code(&self) -> &'static str {
|
||||
return match self {
|
||||
Self::Postgres => "postgres",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// TLS policy accepted by the backend-neutral PostgreSQL settings surface.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum PostgresTlsMode {
|
||||
/// Connect without TLS.
|
||||
Disabled,
|
||||
/// Require TLS and verify both the certificate chain and requested server identity.
|
||||
VerifyFull,
|
||||
}
|
||||
|
||||
/// Bounded PostgreSQL connection-pool settings owned by the Store facade.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PostgresPoolSettings {
|
||||
connect_timeout: std::time::Duration,
|
||||
create_timeout: std::time::Duration,
|
||||
max_connections: u32,
|
||||
recycle_timeout: std::time::Duration,
|
||||
wait_timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
impl PostgresPoolSettings {
|
||||
/// Creates explicit PostgreSQL pool bounds without performing any I/O.
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
max_connections: u32,
|
||||
connect_timeout: std::time::Duration,
|
||||
wait_timeout: std::time::Duration,
|
||||
create_timeout: std::time::Duration,
|
||||
recycle_timeout: std::time::Duration,
|
||||
) -> Self {
|
||||
return Self { connect_timeout, create_timeout, max_connections, recycle_timeout, wait_timeout };
|
||||
}
|
||||
|
||||
/// Returns the timeout for establishing one physical PostgreSQL connection.
|
||||
#[must_use]
|
||||
pub const fn connect_timeout(&self) -> std::time::Duration {
|
||||
return self.connect_timeout;
|
||||
}
|
||||
|
||||
/// Returns the timeout for creating one pooled PostgreSQL object.
|
||||
#[must_use]
|
||||
pub const fn create_timeout(&self) -> std::time::Duration {
|
||||
return self.create_timeout;
|
||||
}
|
||||
|
||||
/// Returns the maximum number of physical PostgreSQL connections owned by the pool.
|
||||
#[must_use]
|
||||
pub const fn max_connections(&self) -> u32 {
|
||||
return self.max_connections;
|
||||
}
|
||||
|
||||
/// Returns the timeout for recycling one pooled PostgreSQL object.
|
||||
#[must_use]
|
||||
pub const fn recycle_timeout(&self) -> std::time::Duration {
|
||||
return self.recycle_timeout;
|
||||
}
|
||||
|
||||
/// Returns the maximum time one acquisition can wait for pool capacity.
|
||||
#[must_use]
|
||||
pub const fn wait_timeout(&self) -> std::time::Duration {
|
||||
return self.wait_timeout;
|
||||
}
|
||||
|
||||
/// Validates all pool bounds without opening a connection.
|
||||
pub fn validate(&self) -> ksp_store_api::Result<()> {
|
||||
if self.max_connections < MIN_CONNECTIONS || self.max_connections > MAX_CONNECTIONS {
|
||||
return std::result::Result::Err(
|
||||
ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "PostgreSQL pool connection bound is invalid")
|
||||
.with_context("field", "postgres.pool.max_connections")
|
||||
.with_context("minimum", MIN_CONNECTIONS.to_string())
|
||||
.with_context("maximum", MAX_CONNECTIONS.to_string()),
|
||||
);
|
||||
}
|
||||
let connect_validation = validate_duration("postgres.pool.connect_timeout", self.connect_timeout, MIN_CONNECT_TIMEOUT_MS, MAX_CONNECT_TIMEOUT_MS);
|
||||
if let std::result::Result::Err(error) = connect_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let wait_validation = validate_duration("postgres.pool.wait_timeout", self.wait_timeout, MIN_POOL_WAIT_TIMEOUT_MS, MAX_POOL_WAIT_TIMEOUT_MS);
|
||||
if let std::result::Result::Err(error) = wait_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let create_validation = validate_duration("postgres.pool.create_timeout", self.create_timeout, MIN_POOL_CREATE_TIMEOUT_MS, MAX_POOL_CREATE_TIMEOUT_MS);
|
||||
if let std::result::Result::Err(error) = create_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let recycle_validation =
|
||||
validate_duration("postgres.pool.recycle_timeout", self.recycle_timeout, MIN_POOL_RECYCLE_TIMEOUT_MS, MAX_POOL_RECYCLE_TIMEOUT_MS);
|
||||
if let std::result::Result::Err(error) = recycle_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for PostgresPoolSettings {
|
||||
fn default() -> Self {
|
||||
return Self::new(
|
||||
DEFAULT_MAX_CONNECTIONS,
|
||||
std::time::Duration::from_millis(DEFAULT_CONNECT_TIMEOUT_MS),
|
||||
std::time::Duration::from_millis(DEFAULT_POOL_WAIT_TIMEOUT_MS),
|
||||
std::time::Duration::from_millis(DEFAULT_POOL_CREATE_TIMEOUT_MS),
|
||||
std::time::Duration::from_millis(DEFAULT_POOL_RECYCLE_TIMEOUT_MS),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded PostgreSQL bootstrap settings owned by the Store facade.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PostgresBootstrapSettings {
|
||||
auto_migrate: bool,
|
||||
migration_lock_timeout: std::time::Duration,
|
||||
migration_timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
impl PostgresBootstrapSettings {
|
||||
/// Creates explicit bootstrap behavior and migration deadlines.
|
||||
#[must_use]
|
||||
pub const fn new(auto_migrate: bool, migration_timeout: std::time::Duration, migration_lock_timeout: std::time::Duration) -> Self {
|
||||
return Self { auto_migrate, migration_lock_timeout, migration_timeout };
|
||||
}
|
||||
|
||||
/// Returns whether pending KSP-owned migrations may be applied during Store opening.
|
||||
#[must_use]
|
||||
pub const fn auto_migrate(&self) -> bool {
|
||||
return self.auto_migrate;
|
||||
}
|
||||
|
||||
/// Returns the bounded wait allowed for the private PostgreSQL migration lock.
|
||||
#[must_use]
|
||||
pub const fn migration_lock_timeout(&self) -> std::time::Duration {
|
||||
return self.migration_lock_timeout;
|
||||
}
|
||||
|
||||
/// Returns the bounded duration allowed for one migration/bootstrap run.
|
||||
#[must_use]
|
||||
pub const fn migration_timeout(&self) -> std::time::Duration {
|
||||
return self.migration_timeout;
|
||||
}
|
||||
|
||||
/// Validates bootstrap and migration deadlines without contacting PostgreSQL.
|
||||
pub fn validate(&self) -> ksp_store_api::Result<()> {
|
||||
let migration_validation =
|
||||
validate_duration("postgres.bootstrap.migration_timeout", self.migration_timeout, MIN_MIGRATION_TIMEOUT_MS, MAX_MIGRATION_TIMEOUT_MS);
|
||||
if let std::result::Result::Err(error) = migration_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let lock_validation = validate_duration(
|
||||
"postgres.bootstrap.migration_lock_timeout",
|
||||
self.migration_lock_timeout,
|
||||
MIN_MIGRATION_LOCK_TIMEOUT_MS,
|
||||
MAX_MIGRATION_LOCK_TIMEOUT_MS,
|
||||
);
|
||||
if let std::result::Result::Err(error) = lock_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for PostgresBootstrapSettings {
|
||||
fn default() -> Self {
|
||||
return Self::new(
|
||||
true,
|
||||
std::time::Duration::from_millis(DEFAULT_MIGRATION_TIMEOUT_MS),
|
||||
std::time::Duration::from_millis(DEFAULT_MIGRATION_LOCK_TIMEOUT_MS),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// PostgreSQL settings owned by the Store facade and independent from Config or backend implementation types.
|
||||
pub struct PostgresStoreSettings {
|
||||
bootstrap: PostgresBootstrapSettings,
|
||||
connection_uri: std::string::String,
|
||||
pool: PostgresPoolSettings,
|
||||
tls_mode: PostgresTlsMode,
|
||||
}
|
||||
|
||||
impl PostgresStoreSettings {
|
||||
/// Creates PostgreSQL Store settings from an explicitly supplied sensitive connection URI and typed runtime bounds.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
connection_uri: impl std::convert::Into<std::string::String>,
|
||||
pool: PostgresPoolSettings,
|
||||
tls_mode: PostgresTlsMode,
|
||||
bootstrap: PostgresBootstrapSettings,
|
||||
) -> Self {
|
||||
return Self { bootstrap, connection_uri: connection_uri.into(), pool, tls_mode };
|
||||
}
|
||||
|
||||
/// Returns the PostgreSQL bootstrap settings without exposing the sensitive connection URI.
|
||||
#[must_use]
|
||||
pub const fn bootstrap(&self) -> PostgresBootstrapSettings {
|
||||
return self.bootstrap;
|
||||
}
|
||||
|
||||
/// Returns the sensitive PostgreSQL connection URI only to the compiled backend bridge.
|
||||
#[cfg(feature = "postgres")]
|
||||
#[must_use]
|
||||
pub(crate) fn connection_uri(&self) -> &str {
|
||||
return self.connection_uri.as_str();
|
||||
}
|
||||
|
||||
/// Returns the PostgreSQL pool settings without exposing the sensitive connection URI.
|
||||
#[must_use]
|
||||
pub const fn pool(&self) -> PostgresPoolSettings {
|
||||
return self.pool;
|
||||
}
|
||||
|
||||
/// Returns the selected PostgreSQL TLS policy without exposing the sensitive connection URI.
|
||||
#[must_use]
|
||||
pub const fn tls_mode(&self) -> PostgresTlsMode {
|
||||
return self.tls_mode;
|
||||
}
|
||||
|
||||
/// Validates backend-neutral PostgreSQL settings without parsing the URI or performing I/O.
|
||||
pub fn validate(&self) -> ksp_store_api::Result<()> {
|
||||
if self.connection_uri.is_empty() {
|
||||
return std::result::Result::Err(
|
||||
ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "PostgreSQL connection URI is required")
|
||||
.with_context("field", "postgres.connection_uri"),
|
||||
);
|
||||
}
|
||||
let pool_validation = self.pool.validate();
|
||||
if let std::result::Result::Err(error) = pool_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let bootstrap_validation = self.bootstrap.validate();
|
||||
if let std::result::Result::Err(error) = bootstrap_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PostgresStoreSettings {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("PostgresStoreSettings")
|
||||
.field("connection_uri", &"<redacted>")
|
||||
.field("pool", &self.pool)
|
||||
.field("tls_mode", &self.tls_mode)
|
||||
.field("bootstrap", &self.bootstrap)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Backend-specific settings selected through the common Store facade.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum StoreBackendSettings {
|
||||
/// Settings for the known PostgreSQL backend, whether or not its Cargo feature is compiled.
|
||||
Postgres(PostgresStoreSettings),
|
||||
}
|
||||
|
||||
impl StoreBackendSettings {
|
||||
/// Returns the stable backend identity represented by these settings.
|
||||
#[must_use]
|
||||
pub const fn kind(&self) -> StoreBackendKind {
|
||||
return match self {
|
||||
Self::Postgres(_) => StoreBackendKind::Postgres,
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates backend-specific settings without performing I/O.
|
||||
pub fn validate(&self) -> ksp_store_api::Result<()> {
|
||||
return match self {
|
||||
Self::Postgres(settings) => settings.validate(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete backend-neutral settings consumed by the common Store runtime facade.
|
||||
#[derive(Debug)]
|
||||
pub struct StoreSettings {
|
||||
backend: StoreBackendSettings,
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
shutdown_timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
impl StoreSettings {
|
||||
/// Creates complete Store runtime settings for one explicit logical network, backend and shutdown bound.
|
||||
#[must_use]
|
||||
pub fn new(network: ksp_store_api::RawNetworkId, backend: StoreBackendSettings, shutdown_timeout: std::time::Duration) -> Self {
|
||||
return Self { backend, network, shutdown_timeout };
|
||||
}
|
||||
|
||||
/// Returns the selected backend settings.
|
||||
#[must_use]
|
||||
pub const fn backend(&self) -> &StoreBackendSettings {
|
||||
return &self.backend;
|
||||
}
|
||||
|
||||
/// Returns the selected stable backend identity.
|
||||
#[must_use]
|
||||
pub const fn backend_kind(&self) -> StoreBackendKind {
|
||||
return self.backend.kind();
|
||||
}
|
||||
|
||||
/// Returns the single logical network bound to this Store instance.
|
||||
#[must_use]
|
||||
pub const fn network(&self) -> &ksp_store_api::RawNetworkId {
|
||||
return &self.network;
|
||||
}
|
||||
|
||||
/// Returns the maximum duration allowed for explicit Store shutdown.
|
||||
#[must_use]
|
||||
pub const fn shutdown_timeout(&self) -> std::time::Duration {
|
||||
return self.shutdown_timeout;
|
||||
}
|
||||
|
||||
/// Validates all backend-neutral Store settings before any backend I/O can start.
|
||||
pub fn validate(&self) -> ksp_store_api::Result<()> {
|
||||
let shutdown_validation = validate_duration("shutdown_timeout", self.shutdown_timeout, MIN_SHUTDOWN_TIMEOUT_MS, MAX_SHUTDOWN_TIMEOUT_MS);
|
||||
if let std::result::Result::Err(error) = shutdown_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let backend_validation = self.backend.validate();
|
||||
if let std::result::Result::Err(error) = backend_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl StoreSettings {
|
||||
/// Creates settings using the common default shutdown bound while keeping network and backend construction explicit.
|
||||
#[must_use]
|
||||
pub fn with_default_shutdown(network: ksp_store_api::RawNetworkId, backend: StoreBackendSettings) -> Self {
|
||||
return Self::new(network, backend, std::time::Duration::from_millis(DEFAULT_SHUTDOWN_TIMEOUT_MS));
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_duration(field: &'static str, value: std::time::Duration, minimum_ms: u64, maximum_ms: u64) -> ksp_store_api::Result<()> {
|
||||
let minimum = std::time::Duration::from_millis(minimum_ms);
|
||||
let maximum = std::time::Duration::from_millis(maximum_ms);
|
||||
if value < minimum || value > maximum {
|
||||
return std::result::Result::Err(
|
||||
ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "Store runtime duration is outside the supported resource bound")
|
||||
.with_context("field", field)
|
||||
.with_context("minimum_ms", minimum_ms.to_string())
|
||||
.with_context("maximum_ms", maximum_ms.to_string()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/settings.rs"]
|
||||
mod tests;
|
||||
236
crates/ksp-store-lib/src/store.rs
Normal file
236
crates/ksp-store-lib/src/store.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
// file: crates/ksp-store-lib/src/store.rs
|
||||
// version: 4
|
||||
|
||||
/// Opaque common Store runtime facade.
|
||||
///
|
||||
/// A successful value is returned only after the selected compiled backend has completed its bounded physical opening path. PostgreSQL pool, client, TLS and
|
||||
/// driver types remain private to the backend crate.
|
||||
pub struct Store {
|
||||
backend_kind: crate::StoreBackendKind,
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
#[cfg(feature = "postgres")]
|
||||
runtime: StoreRuntime,
|
||||
shutdown_timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
/// Validates settings, selects the requested backend and opens one ready Store instance for exactly one logical network.
|
||||
///
|
||||
/// A known backend whose Cargo feature is absent is rejected before any I/O. A successful PostgreSQL result proves that one physical pooled connection has
|
||||
/// been established under the typed TLS and timeout policy.
|
||||
pub async fn open(settings: crate::StoreSettings) -> ksp_store_api::Result<Self> {
|
||||
let validation = settings.validate();
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let backend_kind = settings.backend_kind();
|
||||
let network = settings.network().clone();
|
||||
let shutdown_timeout = settings.shutdown_timeout();
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
backend = backend_kind.code(),
|
||||
network = network.as_str(),
|
||||
"opening Store runtime"
|
||||
);
|
||||
return match settings.backend() {
|
||||
crate::StoreBackendSettings::Postgres(postgres) => open_postgres(backend_kind, network, shutdown_timeout, postgres).await,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns a safe synchronous runtime snapshot without performing backend I/O.
|
||||
#[must_use]
|
||||
pub fn runtime_snapshot(&self) -> crate::StoreRuntimeSnapshot {
|
||||
#[cfg(feature = "postgres")]
|
||||
{
|
||||
return match &self.runtime {
|
||||
StoreRuntime::Postgres(backend) => map_postgres_runtime_snapshot(backend.runtime_snapshot(), self.backend_kind, self.network.clone()),
|
||||
};
|
||||
}
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
{
|
||||
return crate::StoreRuntimeSnapshot::new(self.backend_kind, self.network.clone(), 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the selected backend's lightweight bounded readiness probe and returns only portable redacted diagnostics.
|
||||
pub async fn health(&self) -> crate::StoreHealthSnapshot {
|
||||
#[cfg(feature = "postgres")]
|
||||
{
|
||||
return match &self.runtime {
|
||||
StoreRuntime::Postgres(backend) => {
|
||||
let snapshot = backend.health().await;
|
||||
map_postgres_health_snapshot(snapshot, self.backend_kind, self.network.clone())
|
||||
},
|
||||
};
|
||||
}
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
{
|
||||
return crate::StoreHealthSnapshot::new(
|
||||
crate::StoreHealthState::NotReady,
|
||||
self.runtime_snapshot(),
|
||||
std::option::Option::None,
|
||||
0,
|
||||
std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Explicitly closes the Store runtime, consumes its facade handle and applies the configured bounded shutdown deadline.
|
||||
pub async fn close(self) -> ksp_store_api::Result<()> {
|
||||
let backend_kind = self.backend_kind;
|
||||
#[cfg(feature = "postgres")]
|
||||
{
|
||||
let network = self.network;
|
||||
let shutdown_timeout = self.shutdown_timeout;
|
||||
let result = match self.runtime {
|
||||
StoreRuntime::Postgres(backend) => backend.close(shutdown_timeout).await,
|
||||
};
|
||||
return match result {
|
||||
std::result::Result::Ok(()) => {
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
backend = backend_kind.code(),
|
||||
network = network.as_str(),
|
||||
"Store runtime closed"
|
||||
);
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(map_postgres_error(error, backend_kind, network.as_str())),
|
||||
};
|
||||
}
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
{
|
||||
return std::result::Result::Err(unavailable_runtime_error(backend_kind));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Store {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("Store")
|
||||
.field("backend_kind", &self.backend_kind)
|
||||
.field("network", &self.network)
|
||||
.field("shutdown_timeout", &self.shutdown_timeout)
|
||||
.finish_non_exhaustive();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
enum StoreRuntime {
|
||||
Postgres(ksp_store_postgres_lib::PostgresBackend),
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
async fn open_postgres(
|
||||
backend_kind: crate::StoreBackendKind,
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
shutdown_timeout: std::time::Duration,
|
||||
settings: &crate::PostgresStoreSettings,
|
||||
) -> ksp_store_api::Result<Store> {
|
||||
let bootstrap = settings.bootstrap();
|
||||
let pool = settings.pool();
|
||||
let tls_mode = match settings.tls_mode() {
|
||||
crate::PostgresTlsMode::Disabled => ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled,
|
||||
crate::PostgresTlsMode::VerifyFull => ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
|
||||
};
|
||||
let backend_settings = ksp_store_postgres_lib::PostgresBackendSettings::new(
|
||||
network.clone(),
|
||||
settings.connection_uri(),
|
||||
pool.max_connections(),
|
||||
pool.connect_timeout(),
|
||||
pool.wait_timeout(),
|
||||
pool.create_timeout(),
|
||||
pool.recycle_timeout(),
|
||||
tls_mode,
|
||||
bootstrap.auto_migrate(),
|
||||
bootstrap.migration_timeout(),
|
||||
bootstrap.migration_lock_timeout(),
|
||||
);
|
||||
let opened = ksp_store_postgres_lib::PostgresBackend::open(backend_settings).await;
|
||||
return match opened {
|
||||
std::result::Result::Ok(backend) => {
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
backend = backend_kind.code(),
|
||||
network = network.as_str(),
|
||||
"Store backend is physically ready"
|
||||
);
|
||||
std::result::Result::Ok(Store { backend_kind, network, runtime: StoreRuntime::Postgres(backend), shutdown_timeout })
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(map_postgres_error(error, backend_kind, network.as_str())),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
async fn open_postgres(
|
||||
backend_kind: crate::StoreBackendKind,
|
||||
_network: ksp_store_api::RawNetworkId,
|
||||
_shutdown_timeout: std::time::Duration,
|
||||
_settings: &crate::PostgresStoreSettings,
|
||||
) -> ksp_store_api::Result<Store> {
|
||||
return std::result::Result::Err(unavailable_runtime_error(backend_kind));
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
fn map_postgres_error(error: ksp_store_postgres_lib::PostgresBackendError, backend_kind: crate::StoreBackendKind, network: &str) -> ksp_store_api::Error {
|
||||
let code = postgres_error_code(error.kind());
|
||||
return ksp_store_api::Error::new(code, "PostgreSQL Store backend lifecycle operation failed")
|
||||
.with_context("backend", backend_kind.code())
|
||||
.with_context("network", network)
|
||||
.with_context("phase", error.phase());
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
fn map_postgres_runtime_snapshot(
|
||||
snapshot: ksp_store_postgres_lib::PostgresBackendRuntimeSnapshot,
|
||||
backend_kind: crate::StoreBackendKind,
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
) -> crate::StoreRuntimeSnapshot {
|
||||
return crate::StoreRuntimeSnapshot::new(
|
||||
backend_kind,
|
||||
network,
|
||||
snapshot.pool_capacity(),
|
||||
snapshot.pool_size(),
|
||||
snapshot.pool_available(),
|
||||
snapshot.pool_waiting(),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
fn map_postgres_health_snapshot(
|
||||
snapshot: ksp_store_postgres_lib::PostgresBackendHealthSnapshot,
|
||||
backend_kind: crate::StoreBackendKind,
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
) -> crate::StoreHealthSnapshot {
|
||||
let state = if snapshot.is_ready() { crate::StoreHealthState::Ready } else { crate::StoreHealthState::NotReady };
|
||||
let error_code = snapshot.error_kind().map(|kind| return postgres_error_code(kind));
|
||||
let runtime = map_postgres_runtime_snapshot(snapshot.runtime().clone(), backend_kind, network);
|
||||
return crate::StoreHealthSnapshot::new(state, runtime, snapshot.migration_version(), snapshot.pending_migration_count(), error_code);
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
fn postgres_error_code(kind: ksp_store_postgres_lib::PostgresBackendErrorKind) -> ksp_store_api::ErrorCode {
|
||||
return match kind {
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid => crate::ERROR_CODE_POSTGRES_CONFIG_INVALID,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => crate::ERROR_CODE_POSTGRES_CONNECT_FAILED,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed => crate::ERROR_CODE_POSTGRES_HEALTH_FAILED,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout => crate::ERROR_CODE_POSTGRES_POOL_TIMEOUT,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => crate::ERROR_CODE_POSTGRES_MIGRATION_FAILED,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => crate::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer => crate::ERROR_CODE_POSTGRES_SCHEMA_NEWER,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => crate::ERROR_CODE_SHUTDOWN_TIMEOUT,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => crate::ERROR_CODE_POSTGRES_TLS_FAILED,
|
||||
_ => crate::ERROR_CODE_BACKEND_OPEN_FAILED,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
fn unavailable_runtime_error(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Error {
|
||||
return ksp_store_api::Error::new(crate::ERROR_CODE_BACKEND_NOT_COMPILED, "Selected Store backend is not compiled")
|
||||
.with_context("backend", backend_kind.code());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/store.rs"]
|
||||
mod tests;
|
||||
60
crates/ksp-store-lib/tests/dependency_boundary.rs
Normal file
60
crates/ksp-store-lib/tests/dependency_boundary.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
// file: crates/ksp-store-lib/tests/dependency_boundary.rs
|
||||
// version: 6
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Cargo-feature and dependency-boundary canaries for the common Store runtime facade.
|
||||
|
||||
#[test]
|
||||
fn pre_005_manifest_keeps_backend_physical_dependencies_out_of_facade() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
assert!(manifest.contains("default = [\"postgres\"]"));
|
||||
assert!(manifest.contains("postgres = [\"dep:ksp-store-postgres-lib\"]"));
|
||||
assert!(manifest.contains("ksp-logging-lib = { path = \"../ksp-logging-lib\" }"));
|
||||
assert!(manifest.contains("ksp-store-api = { path = \"../ksp-store-api\" }"));
|
||||
assert!(manifest.contains("ksp-store-postgres-lib = { path = \"../ksp-store-postgres-lib\", optional = true }"));
|
||||
for forbidden in [
|
||||
"ksp-config-lib",
|
||||
"ksp-materializer",
|
||||
"ksp-program",
|
||||
"ksp-onchain-transport-lib",
|
||||
"ksp-offchain-transport-lib",
|
||||
"tokio-postgres",
|
||||
"deadpool-postgres",
|
||||
"rustls",
|
||||
] {
|
||||
assert!(!manifest.contains(forbidden), "forbidden Store facade dependency detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_facade_exposes_no_physical_postgres_types_or_environment_bypass() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
assert!(crate_root.contains("pub use self::settings::StoreSettings;"));
|
||||
assert!(crate_root.contains("pub use self::health::StoreHealthSnapshot;"));
|
||||
assert!(crate_root.contains("pub use self::health::StoreRuntimeSnapshot;"));
|
||||
assert!(crate_root.contains("pub use self::store::Store;"));
|
||||
assert!(crate_root.contains("pub use ksp_store_api::RawTransaction;"));
|
||||
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
|
||||
for forbidden in [
|
||||
"pub mod ",
|
||||
"pub use ksp_store_postgres_lib",
|
||||
"tokio_postgres",
|
||||
"deadpool_postgres",
|
||||
"rustls::",
|
||||
"deadpool::managed::Pool",
|
||||
"tokio_postgres::Client",
|
||||
"tokio_postgres::Row",
|
||||
"tokio_postgres::Statement",
|
||||
] {
|
||||
assert!(!crate_root.contains(forbidden), "forbidden physical backend facade surface detected: {forbidden}");
|
||||
}
|
||||
let production = format!("{}\n{}\n{}", include_str!("../src/health.rs"), include_str!("../src/settings.rs"), include_str!("../src/store.rs"));
|
||||
for forbidden in ["ksp_config_lib", "std::env", "dotenv", "PGHOST", "PGPORT", "PGUSER", "PGPASSWORD", ".pgpass", "tokio_postgres", "deadpool_postgres"] {
|
||||
assert!(!production.contains(forbidden), "forbidden Store facade ownership bypass detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
55
crates/ksp-store-lib/tests/feature_mismatch.rs
Normal file
55
crates/ksp-store-lib/tests/feature_mismatch.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
// file: crates/ksp-store-lib/tests/feature_mismatch.rs
|
||||
// version: 4
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Feature-selection and pre-I/O PostgreSQL failure canaries for the common Store facade.
|
||||
|
||||
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
|
||||
let mut future = std::boxed::Box::pin(future);
|
||||
let waker = std::task::Waker::noop();
|
||||
let mut context = std::task::Context::from_waker(waker);
|
||||
return match std::future::Future::poll(future.as_mut(), &mut context) {
|
||||
std::task::Poll::Ready(value) => value,
|
||||
std::task::Poll::Pending => panic!("Store pre-I/O feature/config rejection unexpectedly became pending"),
|
||||
};
|
||||
}
|
||||
|
||||
fn valid_network() -> ksp_store_lib::RawNetworkId {
|
||||
return match ksp_store_lib::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn settings(connection_uri: &str) -> ksp_store_lib::StoreSettings {
|
||||
let postgres = ksp_store_lib::PostgresStoreSettings::new(
|
||||
connection_uri,
|
||||
ksp_store_lib::PostgresPoolSettings::default(),
|
||||
ksp_store_lib::PostgresTlsMode::Disabled,
|
||||
ksp_store_lib::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
return ksp_store_lib::StoreSettings::with_default_shutdown(valid_network(), ksp_store_lib::StoreBackendSettings::Postgres(postgres));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
#[test]
|
||||
fn pre_005_known_postgres_without_feature_returns_stable_error_before_io() {
|
||||
let result = poll_ready(ksp_store_lib::Store::open(settings("postgresql://operator-supplied-sensitive-value@localhost/ksp")));
|
||||
let error = result.err();
|
||||
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED));
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
#[test]
|
||||
fn pre_005_compiled_postgres_rejects_malformed_uri_without_secret_leak_before_io() {
|
||||
let secret_canary = "not-a-postgresql-uri-secret-canary";
|
||||
let result = poll_ready(ksp_store_lib::Store::open(settings(secret_canary)));
|
||||
let error = result.err();
|
||||
assert_eq!(error.as_ref().map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID));
|
||||
assert!(!format!("{error:?}").contains(secret_canary));
|
||||
return;
|
||||
}
|
||||
256
crates/ksp-store-lib/tests/hardening_completeness.rs
Normal file
256
crates/ksp-store-lib/tests/hardening_completeness.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
// file: crates/ksp-store-lib/tests/hardening_completeness.rs
|
||||
// version: 1
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Hardening, exact-surface and feature-matrix canaries for the common Store facade.
|
||||
|
||||
const SECRET_CANARY: &str = "KSP-STORE-SECRET-CANARY-PRE009";
|
||||
|
||||
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
|
||||
let mut future = std::boxed::Box::pin(future);
|
||||
let waker = std::task::Waker::noop();
|
||||
let mut context = std::task::Context::from_waker(waker);
|
||||
return match std::future::Future::poll(future.as_mut(), &mut context) {
|
||||
std::task::Poll::Ready(value) => value,
|
||||
std::task::Poll::Pending => panic!("Store hardening pre-I/O rejection unexpectedly became pending"),
|
||||
};
|
||||
}
|
||||
|
||||
fn valid_network() -> ksp_store_lib::RawNetworkId {
|
||||
return match ksp_store_lib::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid hardening network rejected: {error:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn hostile_settings(connection_uri: &str) -> ksp_store_lib::StoreSettings {
|
||||
let postgres = ksp_store_lib::PostgresStoreSettings::new(
|
||||
connection_uri,
|
||||
ksp_store_lib::PostgresPoolSettings::default(),
|
||||
ksp_store_lib::PostgresTlsMode::VerifyFull,
|
||||
ksp_store_lib::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
return ksp_store_lib::StoreSettings::with_default_shutdown(valid_network(), ksp_store_lib::StoreBackendSettings::Postgres(postgres));
|
||||
}
|
||||
|
||||
fn public_reexport_names(source: &str) -> std::vec::Vec<&str> {
|
||||
let mut names = std::vec::Vec::new();
|
||||
for line in source.lines() {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.starts_with("pub use ") || !trimmed.ends_with(';') {
|
||||
continue;
|
||||
}
|
||||
let without_semicolon = trimmed.trim_end_matches(';');
|
||||
let name = match without_semicolon.rsplit("::").next() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
names.push(name);
|
||||
}
|
||||
names.sort_unstable();
|
||||
return names;
|
||||
}
|
||||
|
||||
fn manifest_dependency_names(source: &str) -> std::vec::Vec<&str> {
|
||||
let dependencies_tail = match source.split("[dependencies]").nth(1) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::vec::Vec::new(),
|
||||
};
|
||||
let dependencies = match dependencies_tail.split("[lints]").next() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::vec::Vec::new(),
|
||||
};
|
||||
let mut names = std::vec::Vec::new();
|
||||
for line in dependencies.lines() {
|
||||
let content = match line.split('#').next() {
|
||||
std::option::Option::Some(value) => value.trim(),
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
if content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let key = match content.split('=').next() {
|
||||
std::option::Option::Some(value) => value.trim().trim_end_matches(".workspace"),
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
if !key.is_empty() {
|
||||
names.push(key);
|
||||
}
|
||||
}
|
||||
names.sort_unstable();
|
||||
return names;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_facade_modules_and_crate_root_exports_are_exact() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
for required in ["mod constants;", "mod error;", "mod health;", "mod settings;", "mod store;"] {
|
||||
assert!(crate_root.contains(required), "missing Store facade module: {required}");
|
||||
}
|
||||
assert!(!crate_root.contains("pub mod "));
|
||||
let actual = public_reexport_names(crate_root);
|
||||
let mut expected = [
|
||||
"ERROR_CODE_BACKEND_CLOSED",
|
||||
"ERROR_CODE_BACKEND_NOT_COMPILED",
|
||||
"ERROR_CODE_BACKEND_OPEN_FAILED",
|
||||
"ERROR_CODE_POSTGRES_CONFIG_INVALID",
|
||||
"ERROR_CODE_POSTGRES_CONNECT_FAILED",
|
||||
"ERROR_CODE_POSTGRES_HEALTH_FAILED",
|
||||
"ERROR_CODE_POSTGRES_MIGRATION_FAILED",
|
||||
"ERROR_CODE_POSTGRES_MIGRATION_MISMATCH",
|
||||
"ERROR_CODE_POSTGRES_POOL_TIMEOUT",
|
||||
"ERROR_CODE_POSTGRES_SCHEMA_NEWER",
|
||||
"ERROR_CODE_POSTGRES_TLS_FAILED",
|
||||
"ERROR_CODE_RAW_CONFLICT",
|
||||
"ERROR_CODE_RAW_MODEL_INVALID",
|
||||
"ERROR_CODE_RAW_PAYLOAD_INVALID",
|
||||
"ERROR_CODE_RAW_PROVENANCE_INVALID",
|
||||
"ERROR_CODE_RAW_QUERY_INVALID",
|
||||
"ERROR_CODE_RAW_RETENTION_INVALID",
|
||||
"ERROR_CODE_SETTINGS_INVALID",
|
||||
"ERROR_CODE_SHUTDOWN_TIMEOUT",
|
||||
"Error",
|
||||
"ErrorCode",
|
||||
"ErrorContext",
|
||||
"MAX_RAW_ACCOUNT_DATA_BYTES",
|
||||
"MAX_RAW_CODE_BYTES",
|
||||
"MAX_RAW_PAGE_CURSOR_BYTES",
|
||||
"MAX_RAW_PAYLOAD_BYTES",
|
||||
"MAX_RAW_SOURCE_PAYLOAD_BYTES",
|
||||
"MAX_RAW_UNIX_MILLIS",
|
||||
"PostgresBootstrapSettings",
|
||||
"PostgresPoolSettings",
|
||||
"PostgresStoreSettings",
|
||||
"PostgresTlsMode",
|
||||
"Pubkey",
|
||||
"RawAccountObservation",
|
||||
"RawAccountObservationRead",
|
||||
"RawAccountObservationWrite",
|
||||
"RawAccountState",
|
||||
"RawAccountStateQuery",
|
||||
"RawAccountStateRead",
|
||||
"RawAccountStateReference",
|
||||
"RawAccountStateWrite",
|
||||
"RawAcquisitionOrigin",
|
||||
"RawAcquisitionProvenance",
|
||||
"RawAcquisitionWriteOutcome",
|
||||
"RawContentHash",
|
||||
"RawEntityWriteOutcome",
|
||||
"RawFormatId",
|
||||
"RawNetworkId",
|
||||
"RawObservationKey",
|
||||
"RawObservationWriteOutcome",
|
||||
"RawPage",
|
||||
"RawPageCursor",
|
||||
"RawPageLimit",
|
||||
"RawPageRequest",
|
||||
"RawPayload",
|
||||
"RawProvenanceCode",
|
||||
"RawRetentionState",
|
||||
"RawRetentionWriteOutcome",
|
||||
"RawSlotRange",
|
||||
"RawSortDirection",
|
||||
"RawTimestamp",
|
||||
"RawTransaction",
|
||||
"RawTransactionAcquisitionMode",
|
||||
"RawTransactionObservation",
|
||||
"RawTransactionObservationRead",
|
||||
"RawTransactionObservationWrite",
|
||||
"RawTransactionQuery",
|
||||
"RawTransactionRead",
|
||||
"RawTransactionReference",
|
||||
"RawTransactionRetentionRead",
|
||||
"RawTransactionRetentionTransition",
|
||||
"RawTransactionRetentionWrite",
|
||||
"RawTransactionSignature",
|
||||
"RawTransactionTombstone",
|
||||
"RawTransactionWrite",
|
||||
"Result",
|
||||
"Store",
|
||||
"StoreApiFuture",
|
||||
"StoreBackendKind",
|
||||
"StoreBackendSettings",
|
||||
"StoreHealthSnapshot",
|
||||
"StoreHealthState",
|
||||
"StoreRuntimeSnapshot",
|
||||
"StoreSettings",
|
||||
];
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual.as_slice(), expected.as_slice());
|
||||
assert_eq!(actual.len(), 84);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_facade_manifest_and_feature_contract_are_exact() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
assert!(manifest.contains("default = [\"postgres\"]"));
|
||||
assert!(manifest.contains("postgres = [\"dep:ksp-store-postgres-lib\"]"));
|
||||
let actual = manifest_dependency_names(manifest);
|
||||
let expected = ["ksp-logging-lib", "ksp-store-api", "ksp-store-postgres-lib"];
|
||||
assert_eq!(actual.as_slice(), expected.as_slice());
|
||||
for forbidden in ["tokio-postgres", "deadpool-postgres", "rustls", "sqlx", "ksp-config-lib", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib"] {
|
||||
assert!(!manifest.contains(forbidden), "forbidden Store facade dependency detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_secret_canary_never_crosses_settings_or_pre_io_error_debug() {
|
||||
let malformed = std::format!("not-a-postgresql-uri-{SECRET_CANARY}");
|
||||
let settings = hostile_settings(malformed.as_str());
|
||||
let rendered = std::format!("{settings:?}");
|
||||
assert!(!rendered.contains(SECRET_CANARY));
|
||||
assert!(rendered.contains("<redacted>"));
|
||||
let result = poll_ready(ksp_store_lib::Store::open(settings));
|
||||
let error = match result {
|
||||
std::result::Result::Err(value) => value,
|
||||
std::result::Result::Ok(_) => panic!("hostile Store settings unexpectedly opened"),
|
||||
};
|
||||
#[cfg(feature = "postgres")]
|
||||
assert_eq!(error.code(), ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID);
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
assert_eq!(error.code(), ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED);
|
||||
assert!(!std::format!("{error}").contains(SECRET_CANARY));
|
||||
assert!(!std::format!("{error:?}").contains(SECRET_CANARY));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_facade_production_sources_keep_config_env_physical_sql_and_backend_handles_out() {
|
||||
let production = std::format!(
|
||||
"{}
|
||||
{}
|
||||
{}
|
||||
{}",
|
||||
include_str!("../src/error.rs"),
|
||||
include_str!("../src/health.rs"),
|
||||
include_str!("../src/settings.rs"),
|
||||
include_str!("../src/store.rs")
|
||||
);
|
||||
for forbidden in [
|
||||
"ksp_config_lib",
|
||||
"std::env",
|
||||
"dotenv",
|
||||
"PGHOST",
|
||||
"PGPORT",
|
||||
"PGUSER",
|
||||
"PGPASSWORD",
|
||||
".pgpass",
|
||||
"tokio_postgres::Client",
|
||||
"tokio_postgres::Row",
|
||||
"tokio_postgres::Statement",
|
||||
"deadpool_postgres::Pool",
|
||||
"sqlx::",
|
||||
"CREATE TABLE",
|
||||
"INSERT INTO",
|
||||
"UPDATE ",
|
||||
"DELETE FROM",
|
||||
] {
|
||||
assert!(!production.contains(forbidden), "forbidden facade ownership/runtime material detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
68
crates/ksp-store-lib/tests/public_api.rs
Normal file
68
crates/ksp-store-lib/tests/public_api.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
// file: crates/ksp-store-lib/tests/public_api.rs
|
||||
// version: 5
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Public API canaries for Store settings, lifecycle and Store API reexports.
|
||||
|
||||
#[test]
|
||||
fn pre_003_settings_and_lifecycle_contract_are_available_from_crate_root() {
|
||||
let postgres = ksp_store_lib::PostgresStoreSettings::new(
|
||||
"postgresql://operator-supplied-sensitive-value",
|
||||
ksp_store_lib::PostgresPoolSettings::default(),
|
||||
ksp_store_lib::PostgresTlsMode::VerifyFull,
|
||||
ksp_store_lib::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
let network = match ksp_store_lib::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid public API network rejected: {error:?}"),
|
||||
};
|
||||
let settings = ksp_store_lib::StoreSettings::with_default_shutdown(network, ksp_store_lib::StoreBackendSettings::Postgres(postgres));
|
||||
assert_eq!(settings.backend_kind(), ksp_store_lib::StoreBackendKind::Postgres);
|
||||
assert_eq!(settings.network().as_str(), "devnet");
|
||||
assert!(settings.validate().is_ok());
|
||||
let _open = ksp_store_lib::Store::open;
|
||||
let _close = ksp_store_lib::Store::close;
|
||||
let _runtime_snapshot = ksp_store_lib::Store::runtime_snapshot;
|
||||
let _health = ksp_store_lib::Store::health;
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_common_and_postgres_error_codes_are_stable_and_store_owned() {
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.domain(), "store");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.code(), "settings_invalid");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED.code(), "backend_not_compiled");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED.code(), "backend_open_failed");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID.code(), "postgres_config_invalid");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONNECT_FAILED.code(), "postgres_connect_failed");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_HEALTH_FAILED.code(), "postgres_health_failed");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_POOL_TIMEOUT.code(), "postgres_pool_timeout");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_FAILED.code(), "postgres_migration_failed");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH.code(), "postgres_migration_mismatch");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_SCHEMA_NEWER.code(), "postgres_schema_newer");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_TLS_FAILED.code(), "postgres_tls_failed");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_CLOSED.code(), "backend_closed");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_facade_reexports_backend_agnostic_store_api_types() {
|
||||
let _raw_transaction = std::mem::size_of::<std::option::Option<ksp_store_lib::RawTransaction>>();
|
||||
let _raw_account_state = std::mem::size_of::<std::option::Option<ksp_store_lib::RawAccountState>>();
|
||||
let _query = std::mem::size_of::<std::option::Option<ksp_store_lib::RawTransactionQuery>>();
|
||||
let _capability = std::mem::size_of::<std::option::Option<&dyn ksp_store_lib::RawTransactionRead>>();
|
||||
let _result: ksp_store_lib::Result<()> = std::result::Result::Ok(());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_health_and_runtime_snapshot_types_are_portable_crate_root_contracts() {
|
||||
let _state = std::mem::size_of::<std::option::Option<ksp_store_lib::StoreHealthState>>();
|
||||
let _health = std::mem::size_of::<std::option::Option<ksp_store_lib::StoreHealthSnapshot>>();
|
||||
let _runtime = std::mem::size_of::<std::option::Option<ksp_store_lib::StoreRuntimeSnapshot>>();
|
||||
return;
|
||||
}
|
||||
45
crates/ksp-store-lib/unit_tests/health.rs
Normal file
45
crates/ksp-store-lib/unit_tests/health.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
// file: crates/ksp-store-lib/unit_tests/health.rs
|
||||
// version: 1
|
||||
|
||||
fn network() -> ksp_store_api::RawNetworkId {
|
||||
return match ksp_store_api::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid Store health test network rejected: {error:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_snapshot_is_backend_neutral_and_contains_only_safe_counts() {
|
||||
let runtime = crate::StoreRuntimeSnapshot::new(crate::StoreBackendKind::Postgres, network(), 8, 3, 2, 1);
|
||||
assert_eq!(runtime.backend_kind(), crate::StoreBackendKind::Postgres);
|
||||
assert_eq!(runtime.network().as_str(), "devnet");
|
||||
assert_eq!(runtime.pool_capacity(), 8);
|
||||
assert_eq!(runtime.pool_size(), 3);
|
||||
assert_eq!(runtime.pool_available(), 2);
|
||||
assert_eq!(runtime.pool_waiting(), 1);
|
||||
let rendered = format!("{runtime:?}");
|
||||
for forbidden in ["postgresql://", "password", "username", "database", "SELECT ", "ksp_store_schema_migrations"] {
|
||||
assert!(!rendered.contains(forbidden), "unsafe runtime snapshot material detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn health_snapshot_carries_only_safe_state_migration_and_error_code() {
|
||||
let runtime = crate::StoreRuntimeSnapshot::new(crate::StoreBackendKind::Postgres, network(), 8, 1, 1, 0);
|
||||
let ready = crate::StoreHealthSnapshot::new(crate::StoreHealthState::Ready, runtime.clone(), std::option::Option::Some(0), 0, std::option::Option::None);
|
||||
assert_eq!(ready.state(), crate::StoreHealthState::Ready);
|
||||
assert_eq!(ready.migration_version(), std::option::Option::Some(0));
|
||||
assert_eq!(ready.pending_migration_count(), 0);
|
||||
assert_eq!(ready.last_error_code(), std::option::Option::None);
|
||||
let not_ready = crate::StoreHealthSnapshot::new(
|
||||
crate::StoreHealthState::NotReady,
|
||||
runtime,
|
||||
std::option::Option::None,
|
||||
0,
|
||||
std::option::Option::Some(crate::ERROR_CODE_POSTGRES_HEALTH_FAILED),
|
||||
);
|
||||
assert_eq!(not_ready.state(), crate::StoreHealthState::NotReady);
|
||||
assert_eq!(not_ready.last_error_code(), std::option::Option::Some(crate::ERROR_CODE_POSTGRES_HEALTH_FAILED));
|
||||
return;
|
||||
}
|
||||
149
crates/ksp-store-lib/unit_tests/settings.rs
Normal file
149
crates/ksp-store-lib/unit_tests/settings.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
// file: crates/ksp-store-lib/unit_tests/settings.rs
|
||||
// version: 3
|
||||
|
||||
fn valid_network() -> crate::RawNetworkId {
|
||||
return match crate::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn valid_postgres_settings() -> crate::PostgresStoreSettings {
|
||||
return crate::PostgresStoreSettings::new(
|
||||
"postgresql://secret-user:secret-password@db.internal/ksp",
|
||||
crate::PostgresPoolSettings::default(),
|
||||
crate::PostgresTlsMode::VerifyFull,
|
||||
crate::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_match_the_pre_001_runtime_bounds() {
|
||||
let pool = crate::PostgresPoolSettings::default();
|
||||
assert_eq!(pool.max_connections(), 8);
|
||||
assert_eq!(pool.connect_timeout(), std::time::Duration::from_millis(10_000));
|
||||
assert_eq!(pool.wait_timeout(), std::time::Duration::from_millis(5_000));
|
||||
assert_eq!(pool.create_timeout(), std::time::Duration::from_millis(10_000));
|
||||
assert_eq!(pool.recycle_timeout(), std::time::Duration::from_millis(5_000));
|
||||
let bootstrap = crate::PostgresBootstrapSettings::default();
|
||||
assert!(bootstrap.auto_migrate());
|
||||
assert_eq!(bootstrap.migration_timeout(), std::time::Duration::from_millis(30_000));
|
||||
assert_eq!(bootstrap.migration_lock_timeout(), std::time::Duration::from_millis(10_000));
|
||||
let store = crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(valid_postgres_settings()));
|
||||
assert_eq!(store.shutdown_timeout(), std::time::Duration::from_millis(5_000));
|
||||
assert_eq!(store.backend_kind(), crate::StoreBackendKind::Postgres);
|
||||
assert_eq!(store.network().as_str(), "devnet");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_runtime_boundaries_validate_and_adjacent_values_are_rejected() {
|
||||
let minimum_pool = crate::PostgresPoolSettings::new(
|
||||
1,
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
);
|
||||
assert!(minimum_pool.validate().is_ok());
|
||||
let maximum_pool = crate::PostgresPoolSettings::new(
|
||||
64,
|
||||
std::time::Duration::from_millis(60_000),
|
||||
std::time::Duration::from_millis(60_000),
|
||||
std::time::Duration::from_millis(60_000),
|
||||
std::time::Duration::from_millis(60_000),
|
||||
);
|
||||
assert!(maximum_pool.validate().is_ok());
|
||||
assert!(
|
||||
crate::PostgresPoolSettings::new(
|
||||
0,
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
crate::PostgresPoolSettings::new(
|
||||
65,
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
crate::PostgresPoolSettings::new(
|
||||
1,
|
||||
std::time::Duration::from_millis(99),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
crate::PostgresPoolSettings::new(
|
||||
1,
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(60_001),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
let minimum_bootstrap = crate::PostgresBootstrapSettings::new(false, std::time::Duration::from_millis(1_000), std::time::Duration::from_millis(100));
|
||||
assert!(minimum_bootstrap.validate().is_ok());
|
||||
let maximum_bootstrap = crate::PostgresBootstrapSettings::new(true, std::time::Duration::from_millis(300_000), std::time::Duration::from_millis(120_000));
|
||||
assert!(maximum_bootstrap.validate().is_ok());
|
||||
assert!(
|
||||
crate::PostgresBootstrapSettings::new(true, std::time::Duration::from_millis(999), std::time::Duration::from_millis(100),)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
crate::PostgresBootstrapSettings::new(true, std::time::Duration::from_millis(1_000), std::time::Duration::from_millis(120_001),)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_shutdown_bound_is_independent_from_backend_and_rejects_outside_values() {
|
||||
let valid =
|
||||
crate::StoreSettings::new(valid_network(), crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(100));
|
||||
assert!(valid.validate().is_ok());
|
||||
let invalid =
|
||||
crate::StoreSettings::new(valid_network(), crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(30_001));
|
||||
let error = invalid.validate().err();
|
||||
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_uri_is_required_but_never_rendered_by_debug_or_validation_error() {
|
||||
let secret = "postgresql://secret-user:secret-password@db.internal/ksp";
|
||||
let settings = valid_postgres_settings();
|
||||
let debug = format!("{settings:?}");
|
||||
assert!(!debug.contains(secret));
|
||||
assert!(!debug.contains("secret-user"));
|
||||
assert!(!debug.contains("secret-password"));
|
||||
assert!(debug.contains("<redacted>"));
|
||||
let empty = crate::PostgresStoreSettings::new(
|
||||
std::string::String::new(),
|
||||
crate::PostgresPoolSettings::default(),
|
||||
crate::PostgresTlsMode::Disabled,
|
||||
crate::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
let error = empty.validate().err();
|
||||
assert_eq!(error.as_ref().map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
|
||||
assert!(!format!("{:?}", error).contains(secret));
|
||||
return;
|
||||
}
|
||||
57
crates/ksp-store-lib/unit_tests/store.rs
Normal file
57
crates/ksp-store-lib/unit_tests/store.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
// file: crates/ksp-store-lib/unit_tests/store.rs
|
||||
// version: 4
|
||||
|
||||
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
|
||||
let mut future = std::boxed::Box::pin(future);
|
||||
let waker = std::task::Waker::noop();
|
||||
let mut context = std::task::Context::from_waker(waker);
|
||||
return match std::future::Future::poll(future.as_mut(), &mut context) {
|
||||
std::task::Poll::Ready(value) => value,
|
||||
std::task::Poll::Pending => panic!("Store pre-I/O rejection unexpectedly became pending"),
|
||||
};
|
||||
}
|
||||
|
||||
fn valid_network() -> crate::RawNetworkId {
|
||||
return match crate::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn store_settings(connection_uri: &str) -> crate::StoreSettings {
|
||||
let postgres = crate::PostgresStoreSettings::new(
|
||||
connection_uri,
|
||||
crate::PostgresPoolSettings::default(),
|
||||
crate::PostgresTlsMode::Disabled,
|
||||
crate::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
return crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(postgres));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_settings_are_rejected_before_backend_dispatch() {
|
||||
let result = poll_ready(crate::Store::open(store_settings("")));
|
||||
let error = result.err();
|
||||
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
#[test]
|
||||
fn compiled_postgres_rejects_malformed_physical_configuration_before_io() {
|
||||
let secret_canary = "not-a-postgresql-uri-secret-canary";
|
||||
let result = poll_ready(crate::Store::open(store_settings(secret_canary)));
|
||||
let error = result.err();
|
||||
assert_eq!(error.as_ref().map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_POSTGRES_CONFIG_INVALID));
|
||||
assert!(!format!("{error:?}").contains(secret_canary));
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
#[test]
|
||||
fn known_postgres_without_feature_is_rejected_before_io() {
|
||||
let result = poll_ready(crate::Store::open(store_settings("postgresql://operator-supplied-sensitive-value@localhost/ksp")));
|
||||
let error = result.err();
|
||||
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED));
|
||||
return;
|
||||
}
|
||||
22
crates/ksp-store-postgres-lib/Cargo.toml
Normal file
22
crates/ksp-store-postgres-lib/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
# file: crates/ksp-store-postgres-lib/Cargo.toml
|
||||
# version: 3
|
||||
|
||||
[package]
|
||||
name = "ksp-store-postgres-lib"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
deadpool-postgres = { workspace = true, features = ["rt_tokio_1"] }
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
ksp-store-api = { path = "../ksp-store-api" }
|
||||
rustls = { workspace = true, features = ["aws_lc_rs", "std", "tls12"] }
|
||||
rustls-native-certs.workspace = true
|
||||
sha2.workspace = true
|
||||
tokio = { workspace = true, features = ["rt", "time"] }
|
||||
tokio-postgres = { workspace = true, features = ["runtime"] }
|
||||
tokio-postgres-rustls = { workspace = true, features = ["aws-lc-rs"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
124
crates/ksp-store-postgres-lib/README.md
Normal file
124
crates/ksp-store-postgres-lib/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
<!-- file: crates/ksp-store-postgres-lib/README.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# ksp-store-postgres-lib
|
||||
|
||||
`ksp-store-postgres-lib` est le backend PostgreSQL physique officiel du Store KSP.
|
||||
|
||||
La crate implémente la fondation connexion/pool/TLS/migrations/health derrière `ksp-store-lib`. Elle dépend directement de `ksp-store-api` mais ne dépend jamais de la façade `ksp-store-lib`.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
La crate possède seule pour PostgreSQL :
|
||||
|
||||
- le parsing et la normalisation de la configuration physique `tokio-postgres` ;
|
||||
- le pool borné `deadpool-postgres` ;
|
||||
- la policy TLS physique avec Rustls ;
|
||||
- les roots système et le provider cryptographique AWS-LC ;
|
||||
- le bootstrap/moteur de migrations privé KSP ;
|
||||
- la table metadata `ksp_store_schema_migrations` ;
|
||||
- le sentinel `V000__bootstrap.sql` et son checksum SHA-256 ;
|
||||
- l'advisory transaction lock borné des migrations ;
|
||||
- les snapshots runtime/health sûrs destinés au bridge de façade ;
|
||||
- la fermeture explicite du pool et son fallback `Drop` best-effort ;
|
||||
- la classification d'erreurs backend sans conserver le texte d'erreur PostgreSQL.
|
||||
|
||||
## Frontière d'utilisation
|
||||
|
||||
Les applications, jobs et workers KSP ne dépendent normalement pas de cette crate :
|
||||
|
||||
```text
|
||||
consumer -> ksp-store-lib -> [feature postgres] ksp-store-postgres-lib
|
||||
```
|
||||
|
||||
La surface publique de cette crate existe pour le bridge inter-crates et les tests d'intégration backend. Elle ne constitue pas une seconde façade Store.
|
||||
|
||||
`ksp-store-postgres-lib` ne réexporte pas `tokio-postgres`, Deadpool ou Rustls.
|
||||
|
||||
## Connexion et pool
|
||||
|
||||
`PostgresBackend::open` :
|
||||
|
||||
1. valide et normalise l'URI fournie explicitement ;
|
||||
2. impose la policy TLS typée ;
|
||||
3. construit un pool borné ;
|
||||
4. prouve une connexion physique ;
|
||||
5. vérifie/applique le bootstrap selon les settings ;
|
||||
6. ne retourne qu'après succès de cette fondation.
|
||||
|
||||
Le backend ne lit aucun environnement, `.env`, `PG*`, `.pgpass` ou fichier TLS implicite libpq.
|
||||
|
||||
## TLS
|
||||
|
||||
Les modes sont exactement :
|
||||
|
||||
```text
|
||||
Disabled
|
||||
VerifyFull
|
||||
```
|
||||
|
||||
`VerifyFull` exige :
|
||||
|
||||
- TLS ;
|
||||
- roots système ;
|
||||
- certificat valide ;
|
||||
- vérification de l'identité serveur ;
|
||||
- aucune dégradation automatique en plaintext.
|
||||
|
||||
Les configurations ne permettant pas de vérifier une identité serveur, comme `hostaddr` seul, sont rejetées.
|
||||
|
||||
## Migrations
|
||||
|
||||
La fondation embarque uniquement :
|
||||
|
||||
```text
|
||||
migrations/V000__bootstrap.sql
|
||||
```
|
||||
|
||||
Elle crée la metadata privée :
|
||||
|
||||
```text
|
||||
ksp_store_schema_migrations
|
||||
```
|
||||
|
||||
Le moteur vérifie version, nom et checksum SHA-256, sérialise les runners par advisory transaction lock et refuse une history divergente ou plus récente que le runtime.
|
||||
|
||||
Aucune migration métier RAW n'appartient à cette fondation.
|
||||
|
||||
## Health et erreurs
|
||||
|
||||
`PostgresBackendRuntimeSnapshot` et `PostgresBackendHealthSnapshot` ne contiennent que des compteurs et états sûrs destinés à la façade.
|
||||
|
||||
`PostgresBackendError` ne conserve que :
|
||||
|
||||
```text
|
||||
PostgresBackendErrorKind
|
||||
phase statique
|
||||
```
|
||||
|
||||
Le texte d'erreur PostgreSQL, l'URI, SQL et les valeurs bind ne traversent pas cette frontière.
|
||||
|
||||
## Support PostgreSQL
|
||||
|
||||
La politique de support de `0.3.2` fixe PostgreSQL 15 comme major minimal. Le test live de fondation refuse explicitement un serveur plus ancien ; le backend ne fixe aucun plafond arbitraire de major PostgreSQL. La compatibilité de migration reste basée sur le schéma KSP.
|
||||
|
||||
La preuve opérateur réelle et le major effectivement exercé sont conservés dans la matrice de validation, pas dans cette documentation durable.
|
||||
|
||||
## Hors périmètre actuel
|
||||
|
||||
La crate ne contient encore :
|
||||
|
||||
- aucune implémentation PostgreSQL des capabilities `RawTransaction*` ;
|
||||
- aucune implémentation PostgreSQL des capabilities `RawAccount*` ;
|
||||
- aucun repository métier RAW ;
|
||||
- aucune table/index métier ;
|
||||
- aucune orchestration worker/job ;
|
||||
- aucun transport d'acquisition ou decoder Program.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`USAGE.md`](USAGE.md) — bridge physique et lifecycle ;
|
||||
- [`../ksp-store-lib/README.md`](../ksp-store-lib/README.md) — façade runtime destinée aux consumers ;
|
||||
- [`../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md`](../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md) — architecture Store ;
|
||||
- [`../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md`](../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md) — décisions pool/TLS/migrations ;
|
||||
- [`../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md`](../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md) — preuves déterministes et PostgreSQL réel.
|
||||
155
crates/ksp-store-postgres-lib/USAGE.md
Normal file
155
crates/ksp-store-postgres-lib/USAGE.md
Normal file
@@ -0,0 +1,155 @@
|
||||
<!-- file: crates/ksp-store-postgres-lib/USAGE.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Utilisation de ksp-store-postgres-lib
|
||||
|
||||
## 1. Quand utiliser cette crate directement
|
||||
|
||||
Le consumer applicatif normal utilise `ksp-store-lib`.
|
||||
|
||||
Une dépendance directe à `ksp-store-postgres-lib` est réservée aux composants qui implémentent ou testent le bridge physique PostgreSQL. La crate backend ne doit pas devenir une façade parallèle.
|
||||
|
||||
Un tel composant doit déclarer explicitement le backend et `ksp-store-api`, car `PostgresBackendSettings::new` reçoit le `RawNetworkId` backend-neutral sans le réexporter :
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ksp-store-api = { path = "../ksp-store-api" }
|
||||
ksp-store-postgres-lib = { path = "../ksp-store-postgres-lib" }
|
||||
```
|
||||
|
||||
## 2. Construire le bridge physique
|
||||
|
||||
`PostgresBackendSettings` reçoit des valeurs déjà possédées et validées par la couche appelante. L'URI est sensible et son `Debug` est redacted.
|
||||
|
||||
```rust
|
||||
fn backend_settings(
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
connection_uri: std::string::String,
|
||||
) -> ksp_store_postgres_lib::PostgresBackendSettings {
|
||||
return ksp_store_postgres_lib::PostgresBackendSettings::new(
|
||||
network,
|
||||
connection_uri,
|
||||
8,
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
|
||||
true,
|
||||
std::time::Duration::from_secs(30),
|
||||
std::time::Duration::from_secs(10),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Le backend reçoit un seul `RawNetworkId`. Une instance physique n'est pas un routeur multi-réseau.
|
||||
|
||||
## 3. Ouvrir, sonder et fermer
|
||||
|
||||
```rust
|
||||
async fn use_backend(
|
||||
settings: ksp_store_postgres_lib::PostgresBackendSettings,
|
||||
) -> std::result::Result<(), ksp_store_postgres_lib::PostgresBackendError> {
|
||||
let backend = ksp_store_postgres_lib::PostgresBackend::open(settings).await;
|
||||
let backend = match backend {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
let runtime = backend.runtime_snapshot();
|
||||
let _capacity = runtime.pool_capacity();
|
||||
let _size = runtime.pool_size();
|
||||
let _available = runtime.pool_available();
|
||||
let _waiting = runtime.pool_waiting();
|
||||
|
||||
let health = backend.health().await;
|
||||
let _ready = health.ready();
|
||||
let _migration_version = health.migration_version();
|
||||
let _pending = health.pending_migration_count();
|
||||
let _safe_error_kind = health.last_error_kind();
|
||||
|
||||
return backend.close(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
```
|
||||
|
||||
`open` prouve la connexion et le bootstrap avant de retourner. `close` ferme le pool puis attend son drain dans la deadline fournie.
|
||||
|
||||
## 4. Choisir le mode TLS
|
||||
|
||||
### `VerifyFull`
|
||||
|
||||
À utiliser pour les connexions PostgreSQL protégées :
|
||||
|
||||
```rust
|
||||
ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull
|
||||
```
|
||||
|
||||
Le backend charge les roots système et vérifie certificat + identité serveur. Il rejette une configuration ne fournissant pas d'identité vérifiable.
|
||||
|
||||
### `Disabled`
|
||||
|
||||
```rust
|
||||
ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled
|
||||
```
|
||||
|
||||
Ce mode désactive explicitement TLS. Il ne doit être utilisé que lorsque la topologie de déploiement justifie clairement une connexion non chiffrée.
|
||||
|
||||
La valeur typée choisie par KSP prime sur les paramètres SSL de l'URI.
|
||||
|
||||
## 5. Bootstrap et migrations
|
||||
|
||||
Le backend embarque son propre moteur de migrations. Le seul artefact initial est :
|
||||
|
||||
```text
|
||||
migrations/V000__bootstrap.sql
|
||||
```
|
||||
|
||||
Le bootstrap maintient :
|
||||
|
||||
```text
|
||||
ksp_store_schema_migrations
|
||||
version
|
||||
name
|
||||
checksum SHA-256
|
||||
```
|
||||
|
||||
Le runner est transactionnel et sérialisé par advisory transaction lock. Une divergence de checksum/nom/version ou une history plus récente est terminale ; aucun down migration automatique n'est exécuté.
|
||||
|
||||
`auto_migrate = false` permet de vérifier l'état sans appliquer de migration pending.
|
||||
|
||||
## 6. Classifier les erreurs sans fuite
|
||||
|
||||
```rust
|
||||
match error.kind() {
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => {}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _safe_phase = error.phase();
|
||||
```
|
||||
|
||||
Ne pas reconstruire un diagnostic utilisateur à partir de l'erreur brute PostgreSQL : cette erreur n'est volontairement pas conservée par le bridge.
|
||||
|
||||
## 7. Ce que cette crate ne permet pas encore
|
||||
|
||||
La fondation physique n'implémente pas les traits `RawTransaction*` ou `RawAccount*` de `ksp-store-api`.
|
||||
|
||||
Un backend ouvert et healthy prouve uniquement :
|
||||
|
||||
```text
|
||||
connexion/pool
|
||||
TLS selon policy
|
||||
bootstrap/history
|
||||
health/readiness
|
||||
close borné
|
||||
```
|
||||
|
||||
Il ne prouve aucune persistence métier RAW.
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE ksp_store_schema_migrations (
|
||||
version BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
checksum TEXT NOT NULL,
|
||||
applied_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
7
crates/ksp-store-postgres-lib/src/constants.rs
Normal file
7
crates/ksp-store-postgres-lib/src/constants.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/constants.rs
|
||||
// version: 2
|
||||
|
||||
//! PostgreSQL Store backend-owned constants.
|
||||
|
||||
/// Owning tracing target reserved for events emitted by the PostgreSQL Store backend.
|
||||
pub(crate) const TRACING_TARGET: &str = "ksp-store-postgres-lib";
|
||||
53
crates/ksp-store-postgres-lib/src/error.rs
Normal file
53
crates/ksp-store-postgres-lib/src/error.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/error.rs
|
||||
// version: 3
|
||||
|
||||
/// Safe backend-local classification used by the Store facade for stable error mapping.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum PostgresBackendErrorKind {
|
||||
/// The supplied physical PostgreSQL configuration is unsupported or malformed.
|
||||
ConfigInvalid,
|
||||
/// A physical PostgreSQL connection could not be established.
|
||||
ConnectFailed,
|
||||
/// A bounded pool wait, create or recycle operation reached its deadline.
|
||||
PoolTimeout,
|
||||
/// A lightweight PostgreSQL health/readiness probe failed without exposing server text or SQL.
|
||||
HealthFailed,
|
||||
/// PostgreSQL migration/bootstrap execution failed without exposing server text or SQL.
|
||||
MigrationFailed,
|
||||
/// Applied PostgreSQL migration history diverges from the embedded immutable KSP history.
|
||||
MigrationMismatch,
|
||||
/// The database schema history contains a migration newer than this runtime understands.
|
||||
SchemaNewer,
|
||||
/// Explicit backend shutdown did not drain inside the supplied deadline.
|
||||
ShutdownTimeout,
|
||||
/// Verified TLS configuration or negotiation could not be established.
|
||||
TlsFailed,
|
||||
}
|
||||
|
||||
/// Redacted PostgreSQL backend error carrying only a safe classification and static lifecycle phase.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PostgresBackendError {
|
||||
kind: PostgresBackendErrorKind,
|
||||
phase: &'static str,
|
||||
}
|
||||
|
||||
impl PostgresBackendError {
|
||||
/// Creates one backend error without retaining external error text or sensitive connection material.
|
||||
#[must_use]
|
||||
pub(crate) const fn new(kind: PostgresBackendErrorKind, phase: &'static str) -> Self {
|
||||
return Self { kind, phase };
|
||||
}
|
||||
|
||||
/// Returns the safe backend-local error classification.
|
||||
#[must_use]
|
||||
pub const fn kind(&self) -> PostgresBackendErrorKind {
|
||||
return self.kind;
|
||||
}
|
||||
|
||||
/// Returns the static safe lifecycle phase associated with the failure.
|
||||
#[must_use]
|
||||
pub const fn phase(&self) -> &'static str {
|
||||
return self.phase;
|
||||
}
|
||||
}
|
||||
101
crates/ksp-store-postgres-lib/src/health.rs
Normal file
101
crates/ksp-store-postgres-lib/src/health.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/health.rs
|
||||
// version: 1
|
||||
|
||||
const DEFAULT_HEALTH_TIMEOUT_MS: u64 = 5_000;
|
||||
const MIGRATION_VERSION_SQL: &str = "SELECT COALESCE(MAX(version), -1)::BIGINT FROM ksp_store_schema_migrations";
|
||||
const READINESS_SQL: &str = "SELECT 1::BIGINT";
|
||||
|
||||
/// Runs one bounded lightweight readiness probe and returns only safe classified diagnostics.
|
||||
pub(crate) async fn probe_health(pool: &deadpool_postgres::Pool) -> crate::PostgresBackendHealthSnapshot {
|
||||
let runtime = crate::runtime_snapshot_from_status(pool.status());
|
||||
let timeouts = pool.timeouts();
|
||||
let timeout = match timeouts.wait {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => std::time::Duration::from_millis(DEFAULT_HEALTH_TIMEOUT_MS),
|
||||
};
|
||||
let bounded = tokio::time::timeout(timeout, probe_health_inner(pool, runtime.clone())).await;
|
||||
return match bounded {
|
||||
std::result::Result::Ok(snapshot) => snapshot,
|
||||
std::result::Result::Err(_) => {
|
||||
crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async fn probe_health_inner(pool: &deadpool_postgres::Pool, runtime: crate::PostgresBackendRuntimeSnapshot) -> crate::PostgresBackendHealthSnapshot {
|
||||
let client_result = pool.get().await;
|
||||
let client = match client_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let classified = crate::map_pool_error(error);
|
||||
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, classified.kind());
|
||||
},
|
||||
};
|
||||
let readiness_result = client.query_one(READINESS_SQL, &[]).await;
|
||||
let readiness_row = match readiness_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed);
|
||||
},
|
||||
};
|
||||
let readiness_value = match readiness_row.try_get::<usize, i64>(0) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed);
|
||||
},
|
||||
};
|
||||
if readiness_value != 1 {
|
||||
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed);
|
||||
}
|
||||
let migration_result = client.query_one(MIGRATION_VERSION_SQL, &[]).await;
|
||||
let migration_row = match migration_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed);
|
||||
},
|
||||
};
|
||||
let migration_value = migration_row.try_get::<usize, i64>(0);
|
||||
let migration_version = match migration_value {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed);
|
||||
},
|
||||
};
|
||||
let expected = crate::current_migration_version();
|
||||
if migration_version < 0 || migration_version < expected {
|
||||
let observed = nonnegative_version(migration_version);
|
||||
let pending = pending_migration_count(migration_version, expected);
|
||||
return crate::PostgresBackendHealthSnapshot::not_ready(runtime, observed, pending, crate::PostgresBackendErrorKind::MigrationMismatch);
|
||||
}
|
||||
if migration_version > expected {
|
||||
return crate::PostgresBackendHealthSnapshot::not_ready(
|
||||
runtime,
|
||||
nonnegative_version(migration_version),
|
||||
0,
|
||||
crate::PostgresBackendErrorKind::SchemaNewer,
|
||||
);
|
||||
}
|
||||
return crate::PostgresBackendHealthSnapshot::ready(runtime, migration_version as u64, 0);
|
||||
}
|
||||
|
||||
fn nonnegative_version(value: i64) -> std::option::Option<u64> {
|
||||
if value < 0 {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
return std::option::Option::Some(value as u64);
|
||||
}
|
||||
|
||||
fn pending_migration_count(observed: i64, expected: i64) -> u32 {
|
||||
if observed >= expected {
|
||||
return 0;
|
||||
}
|
||||
let delta = expected.saturating_sub(observed);
|
||||
if delta > i64::from(u32::MAX) {
|
||||
return u32::MAX;
|
||||
}
|
||||
return delta as u32;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/health.rs"]
|
||||
mod tests;
|
||||
53
crates/ksp-store-postgres-lib/src/lib.rs
Normal file
53
crates/ksp-store-postgres-lib/src/lib.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/lib.rs
|
||||
// version: 5
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Official PostgreSQL backend implementation for KSP Store.
|
||||
//!
|
||||
//! `0.3.2-pre.007` owns the physical `tokio-postgres` connection, bounded
|
||||
//! Deadpool pool, explicit Rustls TLS policy, private KSP migration/bootstrap
|
||||
//! engine and safe lightweight health/readiness probe. Business persistence
|
||||
//! remains absent from this foundation release.
|
||||
//!
|
||||
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
|
||||
//! common facade consumes only this crate's narrow backend bridge and never
|
||||
//! exposes PostgreSQL pool, client, row or statement types.
|
||||
|
||||
mod constants;
|
||||
mod error;
|
||||
mod health;
|
||||
mod migration;
|
||||
mod runtime;
|
||||
|
||||
/// Safe backend-local error returned to the common Store facade.
|
||||
pub use self::error::PostgresBackendError;
|
||||
/// Safe backend-local error classification used by the common Store facade.
|
||||
pub use self::error::PostgresBackendErrorKind;
|
||||
/// Opaque physical PostgreSQL backend owning its connection pool.
|
||||
pub use self::runtime::PostgresBackend;
|
||||
/// Safe PostgreSQL readiness projection returned through the backend bridge.
|
||||
pub use self::runtime::PostgresBackendHealthSnapshot;
|
||||
/// Safe PostgreSQL pool counter projection returned through the backend bridge.
|
||||
pub use self::runtime::PostgresBackendRuntimeSnapshot;
|
||||
/// Physical PostgreSQL settings bridge consumed only by the backend crate.
|
||||
pub use self::runtime::PostgresBackendSettings;
|
||||
/// TLS mode accepted by the physical PostgreSQL settings bridge.
|
||||
pub use self::runtime::PostgresBackendTlsMode;
|
||||
|
||||
/// Crate-owned tracing target for PostgreSQL backend behavior.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
/// Private bounded health probe consumed by the physical backend runtime.
|
||||
pub(crate) use self::health::probe_health;
|
||||
/// Private migration/bootstrap runner consumed by the physical backend runtime.
|
||||
pub(crate) use self::migration::bootstrap;
|
||||
/// Current embedded migration version consumed by the private health probe.
|
||||
pub(crate) use self::migration::current_migration_version;
|
||||
/// Private Deadpool error mapper shared with the health probe.
|
||||
pub(crate) use self::runtime::map_pool_error;
|
||||
/// Private Deadpool status projector shared with the health probe.
|
||||
pub(crate) use self::runtime::runtime_snapshot_from_status;
|
||||
|
||||
const _: &str = crate::TRACING_TARGET;
|
||||
318
crates/ksp-store-postgres-lib/src/migration.rs
Normal file
318
crates/ksp-store-postgres-lib/src/migration.rs
Normal file
@@ -0,0 +1,318 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/migration.rs
|
||||
// version: 2
|
||||
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
const ADVISORY_LOCK_KEY: i64 = 0x4b53_5053_544f_5245;
|
||||
const BOOTSTRAP_MIGRATION_NAME: &str = "bootstrap";
|
||||
const BOOTSTRAP_MIGRATION_SQL: &str = include_str!("../migrations/V000__bootstrap.sql");
|
||||
const BOOTSTRAP_MIGRATION_VERSION: i64 = 0;
|
||||
const HEX_LOWER: &[u8; 16] = b"0123456789abcdef";
|
||||
const HISTORY_INSERT_SQL: &str = "INSERT INTO ksp_store_schema_migrations (version, name, checksum, applied_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP)";
|
||||
const HISTORY_LOAD_SQL: &str = "SELECT version, name, checksum FROM ksp_store_schema_migrations ORDER BY version";
|
||||
const LOCK_POLL_INTERVAL_MS: u64 = 25;
|
||||
const METADATA_EXISTS_SQL: &str = r#"SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'ksp_store_schema_migrations'
|
||||
AND table_type = 'BASE TABLE'
|
||||
)"#;
|
||||
const METADATA_PRIMARY_KEY_SQL: &str = r#"SELECT COUNT(*)::BIGINT,
|
||||
COUNT(*) FILTER (WHERE kcu.column_name = 'version')::BIGINT
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_catalog = kcu.constraint_catalog
|
||||
AND tc.constraint_schema = kcu.constraint_schema
|
||||
AND tc.constraint_name = kcu.constraint_name
|
||||
WHERE tc.table_schema = current_schema()
|
||||
AND tc.table_name = 'ksp_store_schema_migrations'
|
||||
AND tc.constraint_type = 'PRIMARY KEY'"#;
|
||||
const METADATA_SHAPE_SQL: &str = r#"SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'ksp_store_schema_migrations'
|
||||
ORDER BY ordinal_position"#;
|
||||
const SET_STATEMENT_TIMEOUT_SQL: &str = "SELECT set_config('statement_timeout', $1, true)";
|
||||
|
||||
struct AppliedMigration {
|
||||
checksum: std::string::String,
|
||||
name: std::string::String,
|
||||
version: i64,
|
||||
}
|
||||
|
||||
/// Returns the latest migration version embedded by this backend runtime.
|
||||
#[must_use]
|
||||
pub(crate) const fn current_migration_version() -> i64 {
|
||||
return BOOTSTRAP_MIGRATION_VERSION;
|
||||
}
|
||||
|
||||
/// Runs the private bounded PostgreSQL schema bootstrap on one dedicated pooled client.
|
||||
pub(crate) async fn bootstrap(
|
||||
client: &mut deadpool_postgres::Client,
|
||||
auto_migrate: bool,
|
||||
migration_timeout: std::time::Duration,
|
||||
migration_lock_timeout: std::time::Duration,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
let bounded = tokio::time::timeout(migration_timeout, bootstrap_inner(client, auto_migrate, migration_timeout, migration_lock_timeout)).await;
|
||||
return match bounded {
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(_) => {
|
||||
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_timeout"))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async fn bootstrap_inner(
|
||||
client: &mut deadpool_postgres::Client,
|
||||
auto_migrate: bool,
|
||||
migration_timeout: std::time::Duration,
|
||||
migration_lock_timeout: std::time::Duration,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
let transaction_result = client.transaction().await;
|
||||
let transaction = match transaction_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_begin"));
|
||||
},
|
||||
};
|
||||
let lock_result = acquire_advisory_lock(&transaction, migration_lock_timeout).await;
|
||||
if let std::result::Result::Err(error) = lock_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let timeout_result = set_statement_timeout(&transaction, migration_timeout).await;
|
||||
if let std::result::Result::Err(error) = timeout_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let exists_result = metadata_exists(&transaction).await;
|
||||
let metadata_exists = match exists_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let checksum = bootstrap_checksum();
|
||||
if !metadata_exists {
|
||||
if !auto_migrate {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_pending"));
|
||||
}
|
||||
let create_result = transaction.batch_execute(BOOTSTRAP_MIGRATION_SQL).await;
|
||||
if create_result.is_err() {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_create"));
|
||||
}
|
||||
let shape_result = verify_metadata_shape(&transaction).await;
|
||||
if let std::result::Result::Err(error) = shape_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let insert_result = transaction.execute(HISTORY_INSERT_SQL, &[&BOOTSTRAP_MIGRATION_VERSION, &BOOTSTRAP_MIGRATION_NAME, &checksum]).await;
|
||||
match insert_result {
|
||||
std::result::Result::Ok(1) => {},
|
||||
std::result::Result::Ok(_) | std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_insert"));
|
||||
},
|
||||
}
|
||||
} else {
|
||||
let shape_result = verify_metadata_shape(&transaction).await;
|
||||
if let std::result::Result::Err(error) = shape_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let history_result = load_history(&transaction).await;
|
||||
let history = match history_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let validation_result = validate_history(history.as_slice(), checksum.as_str());
|
||||
if let std::result::Result::Err(error) = validation_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
let commit_result = transaction.commit().await;
|
||||
return match commit_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(_) => {
|
||||
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_commit"))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async fn acquire_advisory_lock(
|
||||
transaction: &deadpool_postgres::Transaction<'_>,
|
||||
timeout: std::time::Duration,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
let started = tokio::time::Instant::now();
|
||||
let deadline = match started.checked_add(timeout) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_lock_timeout"));
|
||||
},
|
||||
};
|
||||
loop {
|
||||
let row_result = transaction.query_one("SELECT pg_try_advisory_xact_lock($1)", &[&ADVISORY_LOCK_KEY]).await;
|
||||
let row = match row_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_lock"));
|
||||
},
|
||||
};
|
||||
let acquired_result = row.try_get::<usize, bool>(0);
|
||||
let acquired = match acquired_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_lock_decode"));
|
||||
},
|
||||
};
|
||||
if acquired {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let now = tokio::time::Instant::now();
|
||||
if now >= deadline {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "migration_lock_timeout"));
|
||||
}
|
||||
let candidate = now + std::time::Duration::from_millis(LOCK_POLL_INTERVAL_MS);
|
||||
let wake = if candidate < deadline { candidate } else { deadline };
|
||||
tokio::time::sleep_until(wake).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_statement_timeout(
|
||||
transaction: &deadpool_postgres::Transaction<'_>,
|
||||
timeout: std::time::Duration,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
let timeout_value = format!("{}ms", timeout.as_millis());
|
||||
let result = transaction.query_one(SET_STATEMENT_TIMEOUT_SQL, &[&timeout_value]).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(_) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(_) => {
|
||||
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "statement_timeout"))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async fn metadata_exists(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> {
|
||||
let result = transaction.query_one(METADATA_EXISTS_SQL, &[]).await;
|
||||
let row = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_probe"));
|
||||
},
|
||||
};
|
||||
return match row.try_get::<usize, bool>(0) {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(_) => {
|
||||
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_probe_decode"))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async fn verify_metadata_shape(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
let result = transaction.query(METADATA_SHAPE_SQL, &[]).await;
|
||||
let rows = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_shape"));
|
||||
},
|
||||
};
|
||||
let expected = [("version", "bigint", "NO"), ("name", "text", "NO"), ("checksum", "text", "NO"), ("applied_at", "timestamp with time zone", "NO")];
|
||||
let mut found = [false; 4];
|
||||
for row in rows {
|
||||
let column_result = row.try_get::<usize, std::string::String>(0);
|
||||
let data_type_result = row.try_get::<usize, std::string::String>(1);
|
||||
let nullable_result = row.try_get::<usize, std::string::String>(2);
|
||||
let (column, data_type, nullable) = match (column_result, data_type_result, nullable_result) {
|
||||
(std::result::Result::Ok(column), std::result::Result::Ok(data_type), std::result::Result::Ok(nullable)) => (column, data_type, nullable),
|
||||
_ => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_shape_decode"));
|
||||
},
|
||||
};
|
||||
for (index, expected_row) in expected.iter().enumerate() {
|
||||
if column == expected_row.0 {
|
||||
if found[index] || data_type != expected_row.1 || nullable != expected_row.2 {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_shape"));
|
||||
}
|
||||
found[index] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for required in found {
|
||||
if !required {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_shape"));
|
||||
}
|
||||
}
|
||||
let key_result = transaction.query_one(METADATA_PRIMARY_KEY_SQL, &[]).await;
|
||||
let key_row = match key_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_primary_key"));
|
||||
},
|
||||
};
|
||||
let key_count = key_row.try_get::<usize, i64>(0);
|
||||
let version_count = key_row.try_get::<usize, i64>(1);
|
||||
return match (key_count, version_count) {
|
||||
(std::result::Result::Ok(1), std::result::Result::Ok(1)) => std::result::Result::Ok(()),
|
||||
(std::result::Result::Ok(_), std::result::Result::Ok(_)) => {
|
||||
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "metadata_primary_key"))
|
||||
},
|
||||
_ => std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "metadata_primary_key_decode")),
|
||||
};
|
||||
}
|
||||
|
||||
async fn load_history(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<std::vec::Vec<AppliedMigration>, crate::PostgresBackendError> {
|
||||
let result = transaction.query(HISTORY_LOAD_SQL, &[]).await;
|
||||
let rows = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_load"));
|
||||
},
|
||||
};
|
||||
let mut history = std::vec::Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let version_result = row.try_get::<usize, i64>(0);
|
||||
let name_result = row.try_get::<usize, std::string::String>(1);
|
||||
let checksum_result = row.try_get::<usize, std::string::String>(2);
|
||||
match (version_result, name_result, checksum_result) {
|
||||
(std::result::Result::Ok(version), std::result::Result::Ok(name), std::result::Result::Ok(checksum)) => {
|
||||
history.push(AppliedMigration { checksum, name, version });
|
||||
},
|
||||
_ => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_decode"));
|
||||
},
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(history);
|
||||
}
|
||||
|
||||
fn validate_history(history: &[AppliedMigration], expected_checksum: &str) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
let mut sentinel_found = false;
|
||||
for applied in history {
|
||||
if applied.version > BOOTSTRAP_MIGRATION_VERSION {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::SchemaNewer, "history_newer"));
|
||||
}
|
||||
if applied.version < BOOTSTRAP_MIGRATION_VERSION {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_unknown"));
|
||||
}
|
||||
if applied.name != BOOTSTRAP_MIGRATION_NAME || applied.checksum != expected_checksum {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_diverged"));
|
||||
}
|
||||
sentinel_found = true;
|
||||
}
|
||||
if !sentinel_found {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_missing"));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn bootstrap_checksum() -> std::string::String {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(BOOTSTRAP_MIGRATION_SQL.as_bytes());
|
||||
let digest = hasher.finalize();
|
||||
let bytes = digest.as_slice();
|
||||
let mut encoded = std::string::String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
let value = *byte;
|
||||
encoded.push(char::from(HEX_LOWER[(value >> 4) as usize]));
|
||||
encoded.push(char::from(HEX_LOWER[(value & 0x0f) as usize]));
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/migration.rs"]
|
||||
mod tests;
|
||||
467
crates/ksp-store-postgres-lib/src/runtime.rs
Normal file
467
crates/ksp-store-postgres-lib/src/runtime.rs
Normal file
@@ -0,0 +1,467 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/runtime.rs
|
||||
// version: 3
|
||||
|
||||
const APPLICATION_NAME: &str = "ksp-store";
|
||||
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
|
||||
const SHUTDOWN_POLL_INTERVAL_MS: u64 = 10;
|
||||
|
||||
/// Safe PostgreSQL runtime counters exported only through the narrow backend bridge.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PostgresBackendRuntimeSnapshot {
|
||||
pool_available: u32,
|
||||
pool_capacity: u32,
|
||||
pool_size: u32,
|
||||
pool_waiting: u32,
|
||||
}
|
||||
|
||||
impl PostgresBackendRuntimeSnapshot {
|
||||
/// Creates a safe pool-counter projection from already bounded values.
|
||||
#[must_use]
|
||||
pub(crate) const fn new(pool_capacity: u32, pool_size: u32, pool_available: u32, pool_waiting: u32) -> Self {
|
||||
return Self { pool_available, pool_capacity, pool_size, pool_waiting };
|
||||
}
|
||||
|
||||
/// Returns the number of currently available pooled PostgreSQL clients.
|
||||
#[must_use]
|
||||
pub const fn pool_available(&self) -> u32 {
|
||||
return self.pool_available;
|
||||
}
|
||||
|
||||
/// Returns the configured maximum pooled PostgreSQL client count.
|
||||
#[must_use]
|
||||
pub const fn pool_capacity(&self) -> u32 {
|
||||
return self.pool_capacity;
|
||||
}
|
||||
|
||||
/// Returns the current pooled PostgreSQL client count.
|
||||
#[must_use]
|
||||
pub const fn pool_size(&self) -> u32 {
|
||||
return self.pool_size;
|
||||
}
|
||||
|
||||
/// Returns the number of tasks currently waiting for a pooled PostgreSQL client.
|
||||
#[must_use]
|
||||
pub const fn pool_waiting(&self) -> u32 {
|
||||
return self.pool_waiting;
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe PostgreSQL readiness projection returned to the common Store facade.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PostgresBackendHealthSnapshot {
|
||||
error_kind: std::option::Option<crate::PostgresBackendErrorKind>,
|
||||
migration_version: std::option::Option<u64>,
|
||||
pending_migration_count: u32,
|
||||
ready: bool,
|
||||
runtime: PostgresBackendRuntimeSnapshot,
|
||||
}
|
||||
|
||||
impl PostgresBackendHealthSnapshot {
|
||||
/// Creates one successful safe readiness projection.
|
||||
#[must_use]
|
||||
pub(crate) const fn ready(runtime: PostgresBackendRuntimeSnapshot, migration_version: u64, pending_migration_count: u32) -> Self {
|
||||
return Self {
|
||||
error_kind: std::option::Option::None,
|
||||
migration_version: std::option::Option::Some(migration_version),
|
||||
pending_migration_count,
|
||||
ready: true,
|
||||
runtime,
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates one failed safe readiness projection from a classified backend error.
|
||||
#[must_use]
|
||||
pub(crate) const fn not_ready(
|
||||
runtime: PostgresBackendRuntimeSnapshot,
|
||||
migration_version: std::option::Option<u64>,
|
||||
pending_migration_count: u32,
|
||||
error_kind: crate::PostgresBackendErrorKind,
|
||||
) -> Self {
|
||||
return Self {
|
||||
error_kind: std::option::Option::Some(error_kind),
|
||||
migration_version,
|
||||
pending_migration_count,
|
||||
ready: false,
|
||||
runtime,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the safe backend error classification when readiness could not be proven.
|
||||
#[must_use]
|
||||
pub const fn error_kind(&self) -> std::option::Option<crate::PostgresBackendErrorKind> {
|
||||
return self.error_kind;
|
||||
}
|
||||
|
||||
/// Returns whether the latest bounded PostgreSQL probe proved readiness.
|
||||
#[must_use]
|
||||
pub const fn is_ready(&self) -> bool {
|
||||
return self.ready;
|
||||
}
|
||||
|
||||
/// Returns the migration version observed by the readiness probe when available.
|
||||
#[must_use]
|
||||
pub const fn migration_version(&self) -> std::option::Option<u64> {
|
||||
return self.migration_version;
|
||||
}
|
||||
|
||||
/// Returns the number of embedded migrations newer than the observed applied version.
|
||||
#[must_use]
|
||||
pub const fn pending_migration_count(&self) -> u32 {
|
||||
return self.pending_migration_count;
|
||||
}
|
||||
|
||||
/// Returns the safe PostgreSQL pool counters captured for this probe.
|
||||
#[must_use]
|
||||
pub const fn runtime(&self) -> &PostgresBackendRuntimeSnapshot {
|
||||
return &self.runtime;
|
||||
}
|
||||
}
|
||||
|
||||
/// TLS mode accepted by the physical PostgreSQL backend bridge.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum PostgresBackendTlsMode {
|
||||
/// Disable TLS for the selected PostgreSQL target.
|
||||
Disabled,
|
||||
/// Require TLS with system-root trust and server-identity verification.
|
||||
VerifyFull,
|
||||
}
|
||||
|
||||
impl PostgresBackendTlsMode {
|
||||
/// Returns the stable safe TLS mode code used only in diagnostics.
|
||||
#[must_use]
|
||||
pub const fn code(&self) -> &'static str {
|
||||
return match self {
|
||||
Self::Disabled => "disabled",
|
||||
Self::VerifyFull => "verify_full",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Physical settings consumed only by the PostgreSQL backend crate.
|
||||
pub struct PostgresBackendSettings {
|
||||
connect_timeout: std::time::Duration,
|
||||
connection_uri: std::string::String,
|
||||
create_timeout: std::time::Duration,
|
||||
auto_migrate: bool,
|
||||
max_connections: u32,
|
||||
migration_lock_timeout: std::time::Duration,
|
||||
migration_timeout: std::time::Duration,
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
recycle_timeout: std::time::Duration,
|
||||
tls_mode: PostgresBackendTlsMode,
|
||||
wait_timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
impl PostgresBackendSettings {
|
||||
/// Creates the physical PostgreSQL settings bridge from already validated facade-owned values.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
connection_uri: impl std::convert::Into<std::string::String>,
|
||||
max_connections: u32,
|
||||
connect_timeout: std::time::Duration,
|
||||
wait_timeout: std::time::Duration,
|
||||
create_timeout: std::time::Duration,
|
||||
recycle_timeout: std::time::Duration,
|
||||
tls_mode: PostgresBackendTlsMode,
|
||||
auto_migrate: bool,
|
||||
migration_timeout: std::time::Duration,
|
||||
migration_lock_timeout: std::time::Duration,
|
||||
) -> Self {
|
||||
return Self {
|
||||
auto_migrate,
|
||||
connect_timeout,
|
||||
connection_uri: connection_uri.into(),
|
||||
create_timeout,
|
||||
max_connections,
|
||||
migration_lock_timeout,
|
||||
migration_timeout,
|
||||
network,
|
||||
recycle_timeout,
|
||||
tls_mode,
|
||||
wait_timeout,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the logical network bound to this one backend instance.
|
||||
#[must_use]
|
||||
pub const fn network(&self) -> &ksp_store_api::RawNetworkId {
|
||||
return &self.network;
|
||||
}
|
||||
|
||||
/// Returns the selected safe TLS mode.
|
||||
#[must_use]
|
||||
pub const fn tls_mode(&self) -> PostgresBackendTlsMode {
|
||||
return self.tls_mode;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PostgresBackendSettings {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("PostgresBackendSettings")
|
||||
.field("network", &self.network)
|
||||
.field("connection_uri", &"<redacted>")
|
||||
.field("auto_migrate", &self.auto_migrate)
|
||||
.field("max_connections", &self.max_connections)
|
||||
.field("migration_timeout", &self.migration_timeout)
|
||||
.field("migration_lock_timeout", &self.migration_lock_timeout)
|
||||
.field("connect_timeout", &self.connect_timeout)
|
||||
.field("wait_timeout", &self.wait_timeout)
|
||||
.field("create_timeout", &self.create_timeout)
|
||||
.field("recycle_timeout", &self.recycle_timeout)
|
||||
.field("tls_mode", &self.tls_mode)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque physical PostgreSQL backend owning the bounded Deadpool connection pool.
|
||||
pub struct PostgresBackend {
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
pool: deadpool_postgres::Pool,
|
||||
}
|
||||
|
||||
impl PostgresBackend {
|
||||
/// Parses and normalizes one supplied URI, builds a bounded pool and proves one physical connection before returning readiness.
|
||||
pub async fn open(settings: PostgresBackendSettings) -> std::result::Result<Self, crate::PostgresBackendError> {
|
||||
let normalized = normalized_config(&settings);
|
||||
let pg_config = match normalized {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
network = settings.network().as_str(),
|
||||
tls_mode = settings.tls_mode().code(),
|
||||
max_connections = settings.max_connections,
|
||||
"opening PostgreSQL Store backend pool"
|
||||
);
|
||||
let pool_result = match settings.tls_mode {
|
||||
PostgresBackendTlsMode::Disabled => build_pool(pg_config, tokio_postgres::NoTls, &settings),
|
||||
PostgresBackendTlsMode::VerifyFull => {
|
||||
let tls_result = build_verified_tls();
|
||||
let tls = match tls_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
build_pool(pg_config, tls, &settings)
|
||||
},
|
||||
};
|
||||
let pool = match pool_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let probe = pool.get().await;
|
||||
let mut client = match probe {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(map_pool_error(error)),
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
network = settings.network().as_str(),
|
||||
tls_mode = settings.tls_mode().code(),
|
||||
"PostgreSQL Store backend established initial physical connection"
|
||||
);
|
||||
let bootstrap_result = crate::bootstrap(&mut client, settings.auto_migrate, settings.migration_timeout, settings.migration_lock_timeout).await;
|
||||
if let std::result::Result::Err(error) = bootstrap_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
drop(client);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
network = settings.network().as_str(),
|
||||
auto_migrate = settings.auto_migrate,
|
||||
"PostgreSQL Store migration/bootstrap foundation verified"
|
||||
);
|
||||
return std::result::Result::Ok(Self { network: settings.network, pool });
|
||||
}
|
||||
|
||||
/// Returns a safe synchronous snapshot of bounded pool counters without performing PostgreSQL I/O.
|
||||
#[must_use]
|
||||
pub fn runtime_snapshot(&self) -> PostgresBackendRuntimeSnapshot {
|
||||
return runtime_snapshot_from_status(self.pool.status());
|
||||
}
|
||||
|
||||
/// Runs a bounded lightweight PostgreSQL readiness probe and returns only safe classified diagnostics.
|
||||
pub async fn health(&self) -> PostgresBackendHealthSnapshot {
|
||||
return crate::probe_health(&self.pool).await;
|
||||
}
|
||||
|
||||
/// Explicitly closes the pool and waits for all owned pooled objects to drain inside the supplied bound.
|
||||
pub async fn close(self, timeout: std::time::Duration) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
self.pool.close();
|
||||
let drain = async {
|
||||
loop {
|
||||
if self.pool.status().size == 0 {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(SHUTDOWN_POLL_INTERVAL_MS)).await;
|
||||
}
|
||||
};
|
||||
let result = tokio::time::timeout(timeout, drain).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(()) => {
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, network = self.network.as_str(), "PostgreSQL Store backend pool closed");
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(_) => {
|
||||
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ShutdownTimeout, "pool_drain"))
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PostgresBackend {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("PostgresBackend").field("network", &self.network).field("state", &"open").finish();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Drop for PostgresBackend {
|
||||
fn drop(&mut self) {
|
||||
self.pool.close();
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_config(settings: &PostgresBackendSettings) -> std::result::Result<tokio_postgres::Config, crate::PostgresBackendError> {
|
||||
if settings.connection_uri.is_empty() || settings.connection_uri.len() > MAX_CONNECTION_URI_BYTES {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "connection_uri"));
|
||||
}
|
||||
let parsed = settings.connection_uri.parse::<tokio_postgres::Config>();
|
||||
let mut config = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "connection_uri"));
|
||||
},
|
||||
};
|
||||
if config.get_options().is_some() {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "server_options"));
|
||||
}
|
||||
if config.get_hosts().is_empty() && config.get_hostaddrs().is_empty() {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "host"));
|
||||
}
|
||||
if settings.tls_mode == PostgresBackendTlsMode::VerifyFull {
|
||||
if config.get_hosts().is_empty() {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "tls_server_identity"));
|
||||
}
|
||||
for host in config.get_hosts() {
|
||||
if !is_tcp_host(host) {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "tls_server_identity"));
|
||||
}
|
||||
}
|
||||
}
|
||||
config.application_name(APPLICATION_NAME);
|
||||
config.connect_timeout(settings.connect_timeout);
|
||||
config.ssl_negotiation(tokio_postgres::config::SslNegotiation::Postgres);
|
||||
match settings.tls_mode {
|
||||
PostgresBackendTlsMode::Disabled => {
|
||||
config.ssl_mode(tokio_postgres::config::SslMode::Disable);
|
||||
},
|
||||
PostgresBackendTlsMode::VerifyFull => {
|
||||
config.ssl_mode(tokio_postgres::config::SslMode::Require);
|
||||
},
|
||||
}
|
||||
return std::result::Result::Ok(config);
|
||||
}
|
||||
|
||||
fn is_tcp_host(host: &tokio_postgres::config::Host) -> bool {
|
||||
return match host {
|
||||
tokio_postgres::config::Host::Tcp(_) => true,
|
||||
#[cfg(unix)]
|
||||
tokio_postgres::config::Host::Unix(_) => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn build_pool<T>(
|
||||
pg_config: tokio_postgres::Config,
|
||||
tls: T,
|
||||
settings: &PostgresBackendSettings,
|
||||
) -> std::result::Result<deadpool_postgres::Pool, crate::PostgresBackendError>
|
||||
where
|
||||
T: tokio_postgres::tls::MakeTlsConnect<tokio_postgres::Socket> + std::clone::Clone + std::marker::Send + std::marker::Sync + 'static,
|
||||
T::Stream: std::marker::Send + std::marker::Sync,
|
||||
T::TlsConnect: std::marker::Send + std::marker::Sync,
|
||||
<T::TlsConnect as tokio_postgres::tls::TlsConnect<tokio_postgres::Socket>>::Future: std::marker::Send,
|
||||
{
|
||||
let manager_config = deadpool_postgres::ManagerConfig { recycling_method: deadpool_postgres::RecyclingMethod::Verified };
|
||||
let manager = deadpool_postgres::Manager::from_config(pg_config, tls, manager_config);
|
||||
let built = deadpool_postgres::Pool::builder(manager)
|
||||
.max_size(settings.max_connections as usize)
|
||||
.wait_timeout(std::option::Option::Some(settings.wait_timeout))
|
||||
.create_timeout(std::option::Option::Some(settings.create_timeout))
|
||||
.recycle_timeout(std::option::Option::Some(settings.recycle_timeout))
|
||||
.runtime(deadpool_postgres::Runtime::Tokio1)
|
||||
.build();
|
||||
return match built {
|
||||
std::result::Result::Ok(pool) => std::result::Result::Ok(pool),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "pool_build")),
|
||||
};
|
||||
}
|
||||
|
||||
fn build_verified_tls() -> std::result::Result<tokio_postgres_rustls::MakeRustlsConnect, crate::PostgresBackendError> {
|
||||
let native = rustls_native_certs::load_native_certs();
|
||||
let native_error_count = native.errors.len();
|
||||
if native.certs.is_empty() {
|
||||
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, native_error_count, "no system TLS roots available for PostgreSQL verify_full");
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "native_roots"));
|
||||
}
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
let (added, ignored) = roots.add_parsable_certificates(native.certs);
|
||||
if added == 0 {
|
||||
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, native_error_count, ignored, "system TLS roots could not be admitted for PostgreSQL verify_full");
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "native_roots"));
|
||||
}
|
||||
if native_error_count > 0 || ignored > 0 {
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, added, ignored, native_error_count, "loaded PostgreSQL system TLS roots with partial diagnostics");
|
||||
}
|
||||
let provider = std::sync::Arc::new(rustls::crypto::aws_lc_rs::default_provider());
|
||||
let builder_result = rustls::ClientConfig::builder_with_provider(provider).with_safe_default_protocol_versions();
|
||||
let builder = match builder_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "protocol_versions"));
|
||||
},
|
||||
};
|
||||
let client_config = builder.with_root_certificates(roots).with_no_client_auth();
|
||||
return std::result::Result::Ok(tokio_postgres_rustls::MakeRustlsConnect::new(client_config));
|
||||
}
|
||||
|
||||
/// Maps one Deadpool acquisition error into a redacted backend classification.
|
||||
pub(crate) fn map_pool_error(error: deadpool_postgres::PoolError) -> crate::PostgresBackendError {
|
||||
return match error {
|
||||
deadpool_postgres::PoolError::Timeout(timeout_type) => crate::PostgresBackendError::new(
|
||||
crate::PostgresBackendErrorKind::PoolTimeout,
|
||||
match timeout_type {
|
||||
deadpool_postgres::TimeoutType::Wait => "pool_wait",
|
||||
deadpool_postgres::TimeoutType::Create => "pool_create",
|
||||
deadpool_postgres::TimeoutType::Recycle => "pool_recycle",
|
||||
},
|
||||
),
|
||||
deadpool_postgres::PoolError::Backend(_) => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "physical_connect"),
|
||||
deadpool_postgres::PoolError::Closed => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "pool_closed"),
|
||||
deadpool_postgres::PoolError::NoRuntimeSpecified => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "pool_runtime"),
|
||||
deadpool_postgres::PoolError::PostCreateHook(_) => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "pool_post_create"),
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts Deadpool status into a bounded safe backend runtime projection.
|
||||
pub(crate) fn runtime_snapshot_from_status(status: deadpool_postgres::Status) -> PostgresBackendRuntimeSnapshot {
|
||||
return PostgresBackendRuntimeSnapshot::new(
|
||||
bounded_count(status.max_size),
|
||||
bounded_count(status.size),
|
||||
bounded_count(status.available),
|
||||
bounded_count(status.waiting),
|
||||
);
|
||||
}
|
||||
|
||||
fn bounded_count(value: usize) -> u32 {
|
||||
if value > u32::MAX as usize {
|
||||
return u32::MAX;
|
||||
}
|
||||
return value as u32;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/runtime.rs"]
|
||||
mod tests;
|
||||
84
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
Normal file
84
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||
// version: 5
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Dependency and ownership canaries for the physical PostgreSQL Store backend.
|
||||
|
||||
#[test]
|
||||
fn pre_005_backend_owns_exact_physical_runtime_dependencies_without_reverse_facade_edge() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
for required in
|
||||
["deadpool-postgres", "ksp-logging-lib", "ksp-store-api", "rustls", "rustls-native-certs", "sha2", "tokio-postgres", "tokio-postgres-rustls"]
|
||||
{
|
||||
assert!(manifest.contains(required), "missing PostgreSQL backend dependency: {required}");
|
||||
}
|
||||
for forbidden in ["ksp-store-lib", "ksp-config-lib", "ksp-materializer", "ksp-program", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib", "sqlx"] {
|
||||
assert!(!manifest.contains(forbidden), "forbidden PostgreSQL backend dependency detected: {forbidden}");
|
||||
}
|
||||
let migration = include_str!("../src/migration.rs");
|
||||
let bootstrap_sql = include_str!("../migrations/V000__bootstrap.sql");
|
||||
assert!(migration.contains("include_str!(\"../migrations/V000__bootstrap.sql\")"));
|
||||
assert!(bootstrap_sql.contains("ksp_store_schema_migrations"));
|
||||
for forbidden in ["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED"] {
|
||||
assert!(!migration.contains(forbidden), "business migration implementation leaked into foundation: {forbidden}");
|
||||
assert!(!bootstrap_sql.contains(forbidden), "business schema leaked into foundation SQL: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
assert!(crate_root.contains("mod error;"));
|
||||
assert!(crate_root.contains("mod health;"));
|
||||
assert!(crate_root.contains("mod migration;"));
|
||||
assert!(crate_root.contains("mod runtime;"));
|
||||
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
|
||||
for forbidden in [
|
||||
"pub mod ",
|
||||
"ksp_store_lib",
|
||||
"ksp_config_lib",
|
||||
"tokio_postgres::Client",
|
||||
"tokio_postgres::Row",
|
||||
"tokio_postgres::Statement",
|
||||
"deadpool_postgres::Pool;",
|
||||
] {
|
||||
assert!(!crate_root.contains(forbidden), "forbidden PostgreSQL crate-root surface detected: {forbidden}");
|
||||
}
|
||||
let runtime = include_str!("../src/runtime.rs");
|
||||
for forbidden in [
|
||||
"std::env",
|
||||
"dotenv",
|
||||
"KSP_",
|
||||
"KSPB_",
|
||||
"PGHOST",
|
||||
"PGPORT",
|
||||
"PGUSER",
|
||||
"PGPASSWORD",
|
||||
".pgpass",
|
||||
"CREATE TABLE",
|
||||
"INSERT INTO",
|
||||
"UPDATE ",
|
||||
"DELETE FROM",
|
||||
"SELECT ",
|
||||
] {
|
||||
assert!(!runtime.contains(forbidden), "forbidden PostgreSQL backend ownership/scope content detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_health_probe_remains_foundation_only_and_private_sql() {
|
||||
let health = include_str!("../src/health.rs");
|
||||
assert!(health.contains("SELECT 1::BIGINT"));
|
||||
assert!(health.contains("ksp_store_schema_migrations"));
|
||||
for forbidden in
|
||||
["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED", "std::env", "dotenv", "KSP_SECRET_"]
|
||||
{
|
||||
assert!(!health.contains(forbidden), "forbidden health ownership/scope content detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
220
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
Normal file
220
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
|
||||
// version: 1
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Hardening, exact-surface and scope canaries for the physical PostgreSQL Store backend.
|
||||
|
||||
const SECRET_CANARY: &str = "KSP-POSTGRES-SECRET-CANARY-PRE009";
|
||||
|
||||
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
|
||||
let mut future = std::boxed::Box::pin(future);
|
||||
let waker = std::task::Waker::noop();
|
||||
let mut context = std::task::Context::from_waker(waker);
|
||||
return match std::future::Future::poll(future.as_mut(), &mut context) {
|
||||
std::task::Poll::Ready(value) => value,
|
||||
std::task::Poll::Pending => panic!("PostgreSQL hardening pre-I/O rejection unexpectedly became pending"),
|
||||
};
|
||||
}
|
||||
|
||||
fn network() -> ksp_store_api::RawNetworkId {
|
||||
return match ksp_store_api::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid backend hardening network rejected: {error:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn settings(connection_uri: &str, tls_mode: ksp_store_postgres_lib::PostgresBackendTlsMode) -> ksp_store_postgres_lib::PostgresBackendSettings {
|
||||
return ksp_store_postgres_lib::PostgresBackendSettings::new(
|
||||
network(),
|
||||
connection_uri,
|
||||
8,
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
tls_mode,
|
||||
true,
|
||||
std::time::Duration::from_secs(30),
|
||||
std::time::Duration::from_secs(10),
|
||||
);
|
||||
}
|
||||
|
||||
fn public_reexport_names(source: &str) -> std::vec::Vec<&str> {
|
||||
let mut names = std::vec::Vec::new();
|
||||
for line in source.lines() {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.starts_with("pub use ") || !trimmed.ends_with(';') {
|
||||
continue;
|
||||
}
|
||||
let without_semicolon = trimmed.trim_end_matches(';');
|
||||
let name = match without_semicolon.rsplit("::").next() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
names.push(name);
|
||||
}
|
||||
names.sort_unstable();
|
||||
return names;
|
||||
}
|
||||
|
||||
fn manifest_dependency_names(source: &str) -> std::vec::Vec<&str> {
|
||||
let dependencies_tail = match source.split("[dependencies]").nth(1) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::vec::Vec::new(),
|
||||
};
|
||||
let dependencies = match dependencies_tail.split("[lints]").next() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::vec::Vec::new(),
|
||||
};
|
||||
let mut names = std::vec::Vec::new();
|
||||
for line in dependencies.lines() {
|
||||
let content = match line.split('#').next() {
|
||||
std::option::Option::Some(value) => value.trim(),
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
if content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let key = match content.split('=').next() {
|
||||
std::option::Option::Some(value) => value.trim().trim_end_matches(".workspace"),
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
if !key.is_empty() {
|
||||
names.push(key);
|
||||
}
|
||||
}
|
||||
names.sort_unstable();
|
||||
return names;
|
||||
}
|
||||
|
||||
fn assert_pre_io_rejection(connection_uri: &str, tls_mode: ksp_store_postgres_lib::PostgresBackendTlsMode, expected_phase: &str) {
|
||||
let settings = settings(connection_uri, tls_mode);
|
||||
let rendered_settings = std::format!("{settings:?}");
|
||||
assert!(!rendered_settings.contains(SECRET_CANARY));
|
||||
assert!(rendered_settings.contains("<redacted>"));
|
||||
let result = poll_ready(ksp_store_postgres_lib::PostgresBackend::open(settings));
|
||||
let error = match result {
|
||||
std::result::Result::Err(value) => value,
|
||||
std::result::Result::Ok(_) => panic!("hostile PostgreSQL settings unexpectedly opened"),
|
||||
};
|
||||
assert_eq!(error.kind(), ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid);
|
||||
assert_eq!(error.phase(), expected_phase);
|
||||
assert!(!std::format!("{error:?}").contains(SECRET_CANARY));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_backend_modules_exports_and_manifest_dependencies_are_exact() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
for required in ["mod constants;", "mod error;", "mod health;", "mod migration;", "mod runtime;"] {
|
||||
assert!(crate_root.contains(required), "missing PostgreSQL backend module: {required}");
|
||||
}
|
||||
assert!(!crate_root.contains("pub mod "));
|
||||
let actual_exports = public_reexport_names(crate_root);
|
||||
let mut expected_exports = [
|
||||
"PostgresBackend",
|
||||
"PostgresBackendError",
|
||||
"PostgresBackendErrorKind",
|
||||
"PostgresBackendHealthSnapshot",
|
||||
"PostgresBackendRuntimeSnapshot",
|
||||
"PostgresBackendSettings",
|
||||
"PostgresBackendTlsMode",
|
||||
];
|
||||
expected_exports.sort_unstable();
|
||||
assert_eq!(actual_exports.as_slice(), expected_exports.as_slice());
|
||||
assert_eq!(actual_exports.len(), 7);
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
let actual_dependencies = manifest_dependency_names(manifest);
|
||||
let expected_dependencies = [
|
||||
"deadpool-postgres",
|
||||
"ksp-logging-lib",
|
||||
"ksp-store-api",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
"tokio-postgres-rustls",
|
||||
];
|
||||
assert_eq!(actual_dependencies.as_slice(), expected_dependencies.as_slice());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_hostile_uri_matrix_is_rejected_before_io_without_secret_echo() {
|
||||
let malformed = std::format!("not-a-postgresql-uri-{SECRET_CANARY}");
|
||||
assert_pre_io_rejection(malformed.as_str(), ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled, "connection_uri");
|
||||
let oversized = std::format!("{}{SECRET_CANARY}", "x".repeat(4_097));
|
||||
assert_pre_io_rejection(oversized.as_str(), ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled, "connection_uri");
|
||||
let hostaddr_only = std::format!("hostaddr=127.0.0.1 user=operator password={SECRET_CANARY} dbname=ksp");
|
||||
assert_pre_io_rejection(hostaddr_only.as_str(), ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull, "tls_server_identity");
|
||||
let server_options = std::format!("host=localhost user=operator password={SECRET_CANARY} dbname=ksp options='-c application_name={SECRET_CANARY}'");
|
||||
assert_pre_io_rejection(server_options.as_str(), ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled, "server_options");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_backend_error_bridge_cannot_retain_external_error_or_secret_text() {
|
||||
let error_source = include_str!("../src/error.rs");
|
||||
assert!(error_source.contains("kind: PostgresBackendErrorKind"));
|
||||
assert!(error_source.contains("phase: &'static str"));
|
||||
for forbidden in ["String", "source:", "message:", "tokio_postgres::Error", "deadpool_postgres::PoolError"] {
|
||||
assert!(!error_source.contains(forbidden), "backend error type can retain forbidden external material: {forbidden}");
|
||||
}
|
||||
let runtime = include_str!("../src/runtime.rs");
|
||||
assert!(runtime.contains("deadpool_postgres::PoolError::Backend(_)"));
|
||||
assert!(!runtime.contains("deadpool_postgres::PoolError::Backend(error)"));
|
||||
for source in [runtime, include_str!("../src/migration.rs"), include_str!("../src/health.rs")] {
|
||||
for forbidden in ["format!(\"{error", "format!(\"{error:?", "error = ?", "error = %"] {
|
||||
assert!(!source.contains(forbidden), "backend source renders external error material: {forbidden}");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_backend_has_no_env_bypass_or_business_persistence_capability() {
|
||||
let production = std::format!(
|
||||
"{}
|
||||
{}
|
||||
{}
|
||||
{}
|
||||
{}",
|
||||
include_str!("../src/error.rs"),
|
||||
include_str!("../src/health.rs"),
|
||||
include_str!("../src/lib.rs"),
|
||||
include_str!("../src/migration.rs"),
|
||||
include_str!("../src/runtime.rs")
|
||||
);
|
||||
for forbidden in [
|
||||
"std::env",
|
||||
"dotenv",
|
||||
"KSP_SECRET_",
|
||||
"KSPB_",
|
||||
"PGHOST",
|
||||
"PGPORT",
|
||||
"PGUSER",
|
||||
"PGPASSWORD",
|
||||
".pgpass",
|
||||
".postgresql/",
|
||||
"sslrootcert",
|
||||
"sslcert",
|
||||
"sslkey",
|
||||
"ksp_store_lib",
|
||||
"ksp_config_lib",
|
||||
"sqlx::",
|
||||
"impl ksp_store_api::RawTransaction",
|
||||
"impl ksp_store_api::RawAccount",
|
||||
] {
|
||||
assert!(!production.contains(forbidden), "forbidden backend ownership/capability material detected: {forbidden}");
|
||||
}
|
||||
let bootstrap_sql = include_str!("../migrations/V000__bootstrap.sql");
|
||||
assert!(bootstrap_sql.contains("ksp_store_schema_migrations"));
|
||||
for forbidden in ["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED"] {
|
||||
assert!(!bootstrap_sql.contains(forbidden), "business schema leaked into foundation migration: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
416
crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs
Normal file
416
crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs
Normal file
@@ -0,0 +1,416 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs
|
||||
// version: 1
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Opt-in real PostgreSQL proof for the Store foundation runtime.
|
||||
//!
|
||||
//! The test reads one dedicated PostgreSQL URI from stdin, refuses to start
|
||||
//! when the KSP migration metadata table already exists, never prints the URI,
|
||||
//! creates no business table and cleans up only metadata it proved it created.
|
||||
|
||||
const LIVE_BOOTSTRAP_SQL: &str = include_str!("../migrations/V000__bootstrap.sql");
|
||||
const LIVE_BROKEN_CHECKSUM_A: &str = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
const LIVE_BROKEN_CHECKSUM_B: &str = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
|
||||
const LIVE_MAX_URI_BYTES: usize = 4_096;
|
||||
const LIVE_METADATA_EXISTS_SQL: &str = r#"SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'ksp_store_schema_migrations'
|
||||
AND table_type = 'BASE TABLE'
|
||||
)"#;
|
||||
const LIVE_METADATA_DROP_SQL: &str = "DROP TABLE IF EXISTS ksp_store_schema_migrations";
|
||||
const LIVE_SENTINEL_CHECKSUM_SQL: &str = "SELECT checksum FROM ksp_store_schema_migrations WHERE version = 0";
|
||||
const LIVE_SENTINEL_INSERT_SQL: &str =
|
||||
"INSERT INTO ksp_store_schema_migrations (version, name, checksum, applied_at) VALUES (0, 'bootstrap', 'pre008_rollback_injected', CURRENT_TIMESTAMP)";
|
||||
const LIVE_SENTINEL_UPDATE_SQL: &str = "UPDATE ksp_store_schema_migrations SET checksum = $1 WHERE version = 0";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct LiveFailure {
|
||||
phase: &'static str,
|
||||
}
|
||||
|
||||
impl LiveFailure {
|
||||
const fn new(phase: &'static str) -> Self {
|
||||
return Self { phase };
|
||||
}
|
||||
|
||||
const fn phase(&self) -> &'static str {
|
||||
return self.phase;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "opt-in real PostgreSQL foundation proof; reads one dedicated URI from stdin"]
|
||||
fn pre_008_real_postgres_foundation_is_safe_idempotent_concurrent_and_recoverable() {
|
||||
eprintln!("KSP Store PostgreSQL live proof: reading one dedicated URI from stdin without echoing it from the test.");
|
||||
let uri_result = read_uri_from_stdin();
|
||||
let uri = match uri_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("PostgreSQL live input rejected at phase {}", error.phase()),
|
||||
};
|
||||
let runtime_result = tokio::runtime::Builder::new_current_thread().enable_all().build();
|
||||
let runtime = match runtime_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => panic!("PostgreSQL live runtime could not be constructed"),
|
||||
};
|
||||
let outcome = runtime.block_on(run_live_test(uri.as_str()));
|
||||
if let std::result::Result::Err(error) = outcome {
|
||||
panic!("PostgreSQL live foundation proof failed at safe phase {}", error.phase());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fn read_uri_from_stdin() -> std::result::Result<std::string::String, LiveFailure> {
|
||||
let mut input = std::string::String::new();
|
||||
let read_result = std::io::stdin().read_line(&mut input);
|
||||
match read_result {
|
||||
std::result::Result::Ok(0) | std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("stdin_read")),
|
||||
std::result::Result::Ok(_) => {},
|
||||
}
|
||||
let uri = input.trim().to_owned();
|
||||
if uri.is_empty() || uri.len() > LIVE_MAX_URI_BYTES {
|
||||
return std::result::Result::Err(LiveFailure::new("stdin_uri"));
|
||||
}
|
||||
return std::result::Result::Ok(uri);
|
||||
}
|
||||
|
||||
async fn run_live_test(uri: &str) -> std::result::Result<(), LiveFailure> {
|
||||
let admin_result = connect_admin(uri).await;
|
||||
let mut admin = match admin_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let preexisting_result = metadata_exists(&admin).await;
|
||||
let preexisting = match preexisting_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if preexisting {
|
||||
return std::result::Result::Err(LiveFailure::new("metadata_preexisting_refusal"));
|
||||
}
|
||||
let major_result = postgres_major(&admin).await;
|
||||
let major = match major_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if major < 15 {
|
||||
return std::result::Result::Err(LiveFailure::new("postgres_major_unsupported"));
|
||||
}
|
||||
eprintln!("KSP Store PostgreSQL live proof: server major {major}");
|
||||
let mut owns_metadata = false;
|
||||
let scenario = run_foundation_scenario(&mut admin, uri, &mut owns_metadata).await;
|
||||
let cleanup = if owns_metadata { drop_metadata(&admin).await } else { std::result::Result::Ok(()) };
|
||||
if let std::result::Result::Err(error) = cleanup {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) = scenario {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let remains_result = metadata_exists(&admin).await;
|
||||
let remains = match remains_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if remains {
|
||||
return std::result::Result::Err(LiveFailure::new("cleanup_verification"));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
async fn run_foundation_scenario(admin: &mut tokio_postgres::Client, uri: &str, owns_metadata: &mut bool) -> std::result::Result<(), LiveFailure> {
|
||||
let initial_result = open_backend(uri).await;
|
||||
let initial = match initial_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let created_result = metadata_exists(admin).await;
|
||||
let created = match created_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !created {
|
||||
return std::result::Result::Err(LiveFailure::new("initial_bootstrap_metadata"));
|
||||
}
|
||||
*owns_metadata = true;
|
||||
let initial_health = initial.health().await;
|
||||
if !initial_health.is_ready() || initial_health.migration_version() != std::option::Option::Some(0) || initial_health.pending_migration_count() != 0 {
|
||||
return std::result::Result::Err(LiveFailure::new("initial_health"));
|
||||
}
|
||||
let initial_close = close_backend(initial).await;
|
||||
if let std::result::Result::Err(error) = initial_close {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let idempotent_result = open_backend(uri).await;
|
||||
let idempotent = match idempotent_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let idempotent_health = idempotent.health().await;
|
||||
if !idempotent_health.is_ready()
|
||||
|| idempotent_health.migration_version() != std::option::Option::Some(0)
|
||||
|| idempotent_health.pending_migration_count() != 0
|
||||
{
|
||||
return std::result::Result::Err(LiveFailure::new("idempotent_health"));
|
||||
}
|
||||
let idempotent_close = close_backend(idempotent).await;
|
||||
if let std::result::Result::Err(error) = idempotent_close {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let reset_result = drop_metadata(admin).await;
|
||||
if let std::result::Result::Err(error) = reset_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let concurrent_result = concurrent_bootstrap(uri).await;
|
||||
if let std::result::Result::Err(error) = concurrent_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let after_concurrent_result = metadata_exists(admin).await;
|
||||
match after_concurrent_result {
|
||||
std::result::Result::Ok(true) => {},
|
||||
std::result::Result::Ok(false) => return std::result::Result::Err(LiveFailure::new("concurrent_bootstrap_metadata")),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let checksum_result = sentinel_checksum(admin).await;
|
||||
let checksum = match checksum_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let broken_checksum = if checksum == LIVE_BROKEN_CHECKSUM_A { LIVE_BROKEN_CHECKSUM_B } else { LIVE_BROKEN_CHECKSUM_A };
|
||||
let corrupt_result = set_sentinel_checksum(admin, broken_checksum).await;
|
||||
if let std::result::Result::Err(error) = corrupt_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let mismatch_settings = match settings(uri) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mismatch = ksp_store_postgres_lib::PostgresBackend::open(mismatch_settings).await;
|
||||
match mismatch {
|
||||
std::result::Result::Err(error) if error.kind() == ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => {},
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("checksum_mismatch_classification")),
|
||||
std::result::Result::Ok(backend) => {
|
||||
let _ = close_backend(backend).await;
|
||||
return std::result::Result::Err(LiveFailure::new("checksum_mismatch_accepted"));
|
||||
},
|
||||
}
|
||||
let restore_result = set_sentinel_checksum(admin, checksum.as_str()).await;
|
||||
if let std::result::Result::Err(error) = restore_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let recovered_result = open_backend(uri).await;
|
||||
let recovered = match recovered_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let recovered_health = recovered.health().await;
|
||||
if !recovered_health.is_ready() {
|
||||
return std::result::Result::Err(LiveFailure::new("checksum_recovery_health"));
|
||||
}
|
||||
let recovered_close = close_backend(recovered).await;
|
||||
if let std::result::Result::Err(error) = recovered_close {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let rollback_reset = drop_metadata(admin).await;
|
||||
if let std::result::Result::Err(error) = rollback_reset {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let rollback_result = prove_transaction_rollback(admin).await;
|
||||
if let std::result::Result::Err(error) = rollback_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let absent_after_rollback = metadata_exists(admin).await;
|
||||
match absent_after_rollback {
|
||||
std::result::Result::Ok(false) => {},
|
||||
std::result::Result::Ok(true) => return std::result::Result::Err(LiveFailure::new("rollback_left_metadata")),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let final_result = open_backend(uri).await;
|
||||
let final_backend = match final_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let final_health = final_backend.health().await;
|
||||
if !final_health.is_ready() || final_health.migration_version() != std::option::Option::Some(0) || final_health.pending_migration_count() != 0 {
|
||||
return std::result::Result::Err(LiveFailure::new("final_health"));
|
||||
}
|
||||
return close_backend(final_backend).await;
|
||||
}
|
||||
|
||||
fn settings(uri: &str) -> std::result::Result<ksp_store_postgres_lib::PostgresBackendSettings, LiveFailure> {
|
||||
let network_result = ksp_store_api::RawNetworkId::new("devnet");
|
||||
let network = match network_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("network")),
|
||||
};
|
||||
return std::result::Result::Ok(ksp_store_postgres_lib::PostgresBackendSettings::new(
|
||||
network,
|
||||
uri,
|
||||
4,
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled,
|
||||
true,
|
||||
std::time::Duration::from_secs(30),
|
||||
std::time::Duration::from_secs(10),
|
||||
));
|
||||
}
|
||||
|
||||
async fn open_backend(uri: &str) -> std::result::Result<ksp_store_postgres_lib::PostgresBackend, LiveFailure> {
|
||||
let settings_result = settings(uri);
|
||||
let value = match settings_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match ksp_store_postgres_lib::PostgresBackend::open(value).await {
|
||||
std::result::Result::Ok(backend) => std::result::Result::Ok(backend),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("backend_open")),
|
||||
};
|
||||
}
|
||||
|
||||
async fn close_backend(backend: ksp_store_postgres_lib::PostgresBackend) -> std::result::Result<(), LiveFailure> {
|
||||
return match backend.close(std::time::Duration::from_secs(5)).await {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("backend_close")),
|
||||
};
|
||||
}
|
||||
|
||||
async fn concurrent_bootstrap(uri: &str) -> std::result::Result<(), LiveFailure> {
|
||||
let first_settings = match settings(uri) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let second_settings = match settings(uri) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let first = tokio::spawn(async move {
|
||||
return ksp_store_postgres_lib::PostgresBackend::open(first_settings).await;
|
||||
});
|
||||
let second = tokio::spawn(async move {
|
||||
return ksp_store_postgres_lib::PostgresBackend::open(second_settings).await;
|
||||
});
|
||||
let first_joined = first.await;
|
||||
let second_joined = second.await;
|
||||
let first_backend = match first_joined {
|
||||
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
||||
_ => return std::result::Result::Err(LiveFailure::new("concurrent_first")),
|
||||
};
|
||||
let second_backend = match second_joined {
|
||||
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
||||
_ => {
|
||||
let _ = close_backend(first_backend).await;
|
||||
return std::result::Result::Err(LiveFailure::new("concurrent_second"));
|
||||
},
|
||||
};
|
||||
let first_close = close_backend(first_backend).await;
|
||||
let second_close = close_backend(second_backend).await;
|
||||
if first_close.is_err() || second_close.is_err() {
|
||||
return std::result::Result::Err(LiveFailure::new("concurrent_close"));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
async fn connect_admin(uri: &str) -> std::result::Result<tokio_postgres::Client, LiveFailure> {
|
||||
let parsed = uri.parse::<tokio_postgres::Config>();
|
||||
let mut config = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("admin_config")),
|
||||
};
|
||||
config.ssl_mode(tokio_postgres::config::SslMode::Disable);
|
||||
config.ssl_negotiation(tokio_postgres::config::SslNegotiation::Postgres);
|
||||
let connected = config.connect(tokio_postgres::NoTls).await;
|
||||
let (client, connection) = match connected {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("admin_connect")),
|
||||
};
|
||||
let _connection_task = tokio::spawn(async move {
|
||||
let _result = connection.await;
|
||||
return;
|
||||
});
|
||||
return std::result::Result::Ok(client);
|
||||
}
|
||||
|
||||
async fn postgres_major(client: &tokio_postgres::Client) -> std::result::Result<u32, LiveFailure> {
|
||||
let row_result = client.query_one("SHOW server_version_num", &[]).await;
|
||||
let row = match row_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("server_version")),
|
||||
};
|
||||
let value_result = row.try_get::<usize, std::string::String>(0);
|
||||
let value = match value_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("server_version_decode")),
|
||||
};
|
||||
let parsed = value.parse::<u32>();
|
||||
let version_num = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("server_version_parse")),
|
||||
};
|
||||
return std::result::Result::Ok(version_num / 10_000);
|
||||
}
|
||||
|
||||
async fn metadata_exists(client: &tokio_postgres::Client) -> std::result::Result<bool, LiveFailure> {
|
||||
let row_result = client.query_one(LIVE_METADATA_EXISTS_SQL, &[]).await;
|
||||
let row = match row_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("metadata_probe")),
|
||||
};
|
||||
return match row.try_get::<usize, bool>(0) {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("metadata_probe_decode")),
|
||||
};
|
||||
}
|
||||
|
||||
async fn drop_metadata(client: &tokio_postgres::Client) -> std::result::Result<(), LiveFailure> {
|
||||
return match client.batch_execute(LIVE_METADATA_DROP_SQL).await {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("metadata_cleanup")),
|
||||
};
|
||||
}
|
||||
|
||||
async fn sentinel_checksum(client: &tokio_postgres::Client) -> std::result::Result<std::string::String, LiveFailure> {
|
||||
let row_result = client.query_one(LIVE_SENTINEL_CHECKSUM_SQL, &[]).await;
|
||||
let row = match row_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("sentinel_read")),
|
||||
};
|
||||
return match row.try_get::<usize, std::string::String>(0) {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("sentinel_decode")),
|
||||
};
|
||||
}
|
||||
|
||||
async fn set_sentinel_checksum(client: &tokio_postgres::Client, checksum: &str) -> std::result::Result<(), LiveFailure> {
|
||||
let update = client.execute(LIVE_SENTINEL_UPDATE_SQL, &[&checksum]).await;
|
||||
return match update {
|
||||
std::result::Result::Ok(1) => std::result::Result::Ok(()),
|
||||
_ => std::result::Result::Err(LiveFailure::new("sentinel_update")),
|
||||
};
|
||||
}
|
||||
|
||||
async fn prove_transaction_rollback(client: &mut tokio_postgres::Client) -> std::result::Result<(), LiveFailure> {
|
||||
let transaction_result = client.transaction().await;
|
||||
let transaction = match transaction_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("rollback_begin")),
|
||||
};
|
||||
let create = transaction.batch_execute(LIVE_BOOTSTRAP_SQL).await;
|
||||
if create.is_err() {
|
||||
return std::result::Result::Err(LiveFailure::new("rollback_create"));
|
||||
}
|
||||
let insert = transaction.batch_execute(LIVE_SENTINEL_INSERT_SQL).await;
|
||||
if insert.is_err() {
|
||||
return std::result::Result::Err(LiveFailure::new("rollback_insert"));
|
||||
}
|
||||
let injected = transaction.batch_execute("SELECT 1 / 0").await;
|
||||
if injected.is_ok() {
|
||||
return std::result::Result::Err(LiveFailure::new("rollback_injection_missing"));
|
||||
}
|
||||
drop(transaction);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
60
crates/ksp-store-postgres-lib/tests/public_api.rs
Normal file
60
crates/ksp-store-postgres-lib/tests/public_api.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
|
||||
// version: 3
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Narrow physical bridge canaries consumed by `ksp-store-lib` without exposing driver or pool types.
|
||||
|
||||
#[test]
|
||||
fn pre_005_backend_bridge_is_constructible_without_io() {
|
||||
let network = match ksp_store_api::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid backend bridge network rejected: {error:?}"),
|
||||
};
|
||||
let settings = ksp_store_postgres_lib::PostgresBackendSettings::new(
|
||||
network,
|
||||
"postgresql://operator:secret@localhost/ksp",
|
||||
8,
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
|
||||
true,
|
||||
std::time::Duration::from_secs(30),
|
||||
std::time::Duration::from_secs(10),
|
||||
);
|
||||
assert_eq!(settings.network().as_str(), "devnet");
|
||||
assert_eq!(settings.tls_mode().code(), "verify_full");
|
||||
let _open = ksp_store_postgres_lib::PostgresBackend::open;
|
||||
let _close = ksp_store_postgres_lib::PostgresBackend::close;
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_backend_error_projection_is_safe_and_static() {
|
||||
let kinds = [
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed,
|
||||
];
|
||||
assert_eq!(kinds.len(), 9);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_backend_health_bridge_exposes_only_safe_snapshot_types() {
|
||||
let _runtime = std::mem::size_of::<std::option::Option<ksp_store_postgres_lib::PostgresBackendRuntimeSnapshot>>();
|
||||
let _health = std::mem::size_of::<std::option::Option<ksp_store_postgres_lib::PostgresBackendHealthSnapshot>>();
|
||||
let _runtime_snapshot = ksp_store_postgres_lib::PostgresBackend::runtime_snapshot;
|
||||
let _health_probe = ksp_store_postgres_lib::PostgresBackend::health;
|
||||
return;
|
||||
}
|
||||
42
crates/ksp-store-postgres-lib/unit_tests/health.rs
Normal file
42
crates/ksp-store-postgres-lib/unit_tests/health.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
// file: crates/ksp-store-postgres-lib/unit_tests/health.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn pool_status_projection_is_bounded_and_contains_no_physical_handle() {
|
||||
let status = deadpool_postgres::Status { max_size: 8, size: 3, available: 2, waiting: 1 };
|
||||
let snapshot = crate::runtime_snapshot_from_status(status);
|
||||
assert_eq!(snapshot.pool_capacity(), 8);
|
||||
assert_eq!(snapshot.pool_size(), 3);
|
||||
assert_eq!(snapshot.pool_available(), 2);
|
||||
assert_eq!(snapshot.pool_waiting(), 1);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn health_snapshot_distinguishes_ready_and_safe_failure_without_server_text() {
|
||||
let status = deadpool_postgres::Status { max_size: 8, size: 1, available: 1, waiting: 0 };
|
||||
let runtime = crate::runtime_snapshot_from_status(status);
|
||||
let ready = crate::PostgresBackendHealthSnapshot::ready(runtime, 0, 0);
|
||||
assert!(ready.is_ready());
|
||||
assert_eq!(ready.migration_version(), std::option::Option::Some(0));
|
||||
assert_eq!(ready.error_kind(), std::option::Option::None);
|
||||
let failed =
|
||||
crate::PostgresBackendHealthSnapshot::not_ready(ready.runtime().clone(), std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed);
|
||||
assert!(!failed.is_ready());
|
||||
assert_eq!(failed.error_kind(), std::option::Option::Some(crate::PostgresBackendErrorKind::HealthFailed));
|
||||
let rendered = format!("{failed:?}");
|
||||
for forbidden in ["postgresql://", "SELECT ", "ksp_store_schema_migrations", "password", "server error"] {
|
||||
assert!(!rendered.contains(forbidden), "unsafe backend health material detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_health_helpers_keep_unknown_and_pending_counts_safe() {
|
||||
assert_eq!(super::nonnegative_version(-1), std::option::Option::None);
|
||||
assert_eq!(super::nonnegative_version(0), std::option::Option::Some(0));
|
||||
assert_eq!(super::pending_migration_count(-1, 0), 1);
|
||||
assert_eq!(super::pending_migration_count(0, 0), 0);
|
||||
assert_eq!(super::pending_migration_count(1, 0), 0);
|
||||
return;
|
||||
}
|
||||
50
crates/ksp-store-postgres-lib/unit_tests/migration.rs
Normal file
50
crates/ksp-store-postgres-lib/unit_tests/migration.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
// file: crates/ksp-store-postgres-lib/unit_tests/migration.rs
|
||||
// version: 1
|
||||
|
||||
fn applied(version: i64, name: &str, checksum: &str) -> super::AppliedMigration {
|
||||
return super::AppliedMigration { checksum: checksum.to_owned(), name: name.to_owned(), version };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bootstrap_migration_is_static_metadata_only_and_checksum_is_stable_sha256() {
|
||||
assert_eq!(super::BOOTSTRAP_MIGRATION_VERSION, 0);
|
||||
assert_eq!(super::BOOTSTRAP_MIGRATION_NAME, "bootstrap");
|
||||
assert!(super::BOOTSTRAP_MIGRATION_SQL.contains("CREATE TABLE ksp_store_schema_migrations"));
|
||||
for forbidden in ["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED"] {
|
||||
assert!(!super::BOOTSTRAP_MIGRATION_SQL.contains(forbidden), "business schema leaked into bootstrap SQL: {forbidden}");
|
||||
}
|
||||
let checksum = super::bootstrap_checksum();
|
||||
assert_eq!(checksum.len(), 64);
|
||||
assert_eq!(checksum, "d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matching_sentinel_history_is_accepted() {
|
||||
let checksum = super::bootstrap_checksum();
|
||||
let history = [applied(0, "bootstrap", checksum.as_str())];
|
||||
assert!(super::validate_history(&history, checksum.as_str()).is_ok());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn divergent_or_missing_sentinel_is_terminal_mismatch() {
|
||||
let checksum = super::bootstrap_checksum();
|
||||
let wrong_name = [applied(0, "changed", checksum.as_str())];
|
||||
let wrong_checksum = [applied(0, "bootstrap", "00")];
|
||||
let missing: [super::AppliedMigration; 0] = [];
|
||||
for history in [&wrong_name[..], &wrong_checksum[..], &missing[..]] {
|
||||
let result = super::validate_history(history, checksum.as_str());
|
||||
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newer_history_is_rejected_without_down_migration() {
|
||||
let checksum = super::bootstrap_checksum();
|
||||
let history = [applied(0, "bootstrap", checksum.as_str()), applied(1, "future", "future-checksum")];
|
||||
let result = super::validate_history(&history, checksum.as_str());
|
||||
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::SchemaNewer));
|
||||
return;
|
||||
}
|
||||
102
crates/ksp-store-postgres-lib/unit_tests/runtime.rs
Normal file
102
crates/ksp-store-postgres-lib/unit_tests/runtime.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
// file: crates/ksp-store-postgres-lib/unit_tests/runtime.rs
|
||||
// version: 3
|
||||
|
||||
fn network() -> ksp_store_api::RawNetworkId {
|
||||
return match ksp_store_api::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid backend test network rejected: {error:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn settings(connection_uri: &str, tls_mode: crate::PostgresBackendTlsMode) -> crate::PostgresBackendSettings {
|
||||
return crate::PostgresBackendSettings::new(
|
||||
network(),
|
||||
connection_uri,
|
||||
8,
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
tls_mode,
|
||||
true,
|
||||
std::time::Duration::from_secs(30),
|
||||
std::time::Duration::from_secs(10),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn physical_settings_debug_redacts_connection_uri() {
|
||||
let secret = "postgresql://secret-user:secret-password@localhost/ksp";
|
||||
let value = settings(secret, crate::PostgresBackendTlsMode::VerifyFull);
|
||||
let rendered = format!("{value:?}");
|
||||
assert!(!rendered.contains("secret-user"));
|
||||
assert!(!rendered.contains("secret-password"));
|
||||
assert!(rendered.contains("<redacted>"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_or_oversized_uri_is_rejected_without_retaining_input() {
|
||||
let malformed = "not-a-postgresql-uri-secret-canary";
|
||||
let malformed_result = super::normalized_config(&settings(malformed, crate::PostgresBackendTlsMode::Disabled));
|
||||
let malformed_error = malformed_result.err();
|
||||
assert_eq!(malformed_error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||
assert!(!format!("{malformed_error:?}").contains("secret-canary"));
|
||||
let oversized = "x".repeat(super::MAX_CONNECTION_URI_BYTES + 1);
|
||||
let oversized_result = super::normalized_config(&settings(oversized.as_str(), crate::PostgresBackendTlsMode::Disabled));
|
||||
assert_eq!(oversized_result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_disabled_policy_overrides_uri_tls_and_connection_controls() {
|
||||
let value = settings(
|
||||
"postgresql://operator:secret@localhost/ksp?sslmode=require&application_name=hostile&connect_timeout=1&sslnegotiation=direct",
|
||||
crate::PostgresBackendTlsMode::Disabled,
|
||||
);
|
||||
let config = match super::normalized_config(&value) {
|
||||
std::result::Result::Ok(config) => config,
|
||||
std::result::Result::Err(error) => panic!("valid disabled config rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(config.get_ssl_mode(), tokio_postgres::config::SslMode::Disable);
|
||||
assert_eq!(config.get_ssl_negotiation(), tokio_postgres::config::SslNegotiation::Postgres);
|
||||
assert_eq!(config.get_application_name(), std::option::Option::Some(super::APPLICATION_NAME));
|
||||
assert_eq!(config.get_connect_timeout(), std::option::Option::Some(&std::time::Duration::from_secs(10)));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_verify_full_policy_forces_tls_and_rejects_hostaddr_only_identity() {
|
||||
let value = settings(
|
||||
"postgresql://operator:secret@localhost/ksp?sslmode=disable&application_name=hostile&connect_timeout=1",
|
||||
crate::PostgresBackendTlsMode::VerifyFull,
|
||||
);
|
||||
let config = match super::normalized_config(&value) {
|
||||
std::result::Result::Ok(config) => config,
|
||||
std::result::Result::Err(error) => panic!("valid verify_full config rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(config.get_ssl_mode(), tokio_postgres::config::SslMode::Require);
|
||||
assert_eq!(config.get_ssl_negotiation(), tokio_postgres::config::SslNegotiation::Postgres);
|
||||
assert_eq!(config.get_application_name(), std::option::Option::Some(super::APPLICATION_NAME));
|
||||
assert_eq!(config.get_connect_timeout(), std::option::Option::Some(&std::time::Duration::from_secs(10)));
|
||||
let hostaddr_only = settings("hostaddr=127.0.0.1 user=operator dbname=ksp", crate::PostgresBackendTlsMode::VerifyFull);
|
||||
let rejected = super::normalized_config(&hostaddr_only);
|
||||
assert_eq!(rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let unix_socket = settings("host=/var/run/postgresql user=operator dbname=ksp", crate::PostgresBackendTlsMode::VerifyFull);
|
||||
let unix_rejected = super::normalized_config(&unix_socket);
|
||||
assert_eq!(unix_rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libpq_server_options_are_rejected_in_foundation_runtime() {
|
||||
let value = settings("host=localhost user=operator dbname=ksp options='-c statement_timeout=0'", crate::PostgresBackendTlsMode::Disabled);
|
||||
let rejected = super::normalized_config(&value);
|
||||
let error = rejected.err();
|
||||
assert_eq!(error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||
assert_eq!(error.map(|value| return value.phase()), std::option::Option::Some("server_options"));
|
||||
return;
|
||||
}
|
||||
365
deltas/0.3.2/pre.001.md
Normal file
365
deltas/0.3.2/pre.001.md
Normal file
@@ -0,0 +1,365 @@
|
||||
<!-- file: deltas/0.3.2/pre.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.001` — audit Store/PostgreSQL, runtime foundation et réconciliation du graphe
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base directe attendue :
|
||||
|
||||
```text
|
||||
v0.3.1
|
||||
workspace.package.version = 0.3.1
|
||||
```
|
||||
|
||||
Sources obligatoires réellement disponibles :
|
||||
|
||||
```text
|
||||
khadhroony-solana-project-v0.3.1.zip
|
||||
khadhroony-bot3_v0.5.3-pre.005-fix010.zip
|
||||
```
|
||||
|
||||
La metadata Git n'est pas incluse dans l'archive KSP ; le tag `v0.3.1` ne peut donc pas être interrogé localement. La version Cargo, `deltas/0.3.1/rel.001.md`, le prompt 021, la présence de `ksp-store-api` et l'absence des deux crates runtime concordent avec la base stable requise.
|
||||
|
||||
Commit attendu :
|
||||
|
||||
```text
|
||||
v0.3.2-pre.001
|
||||
```
|
||||
|
||||
Archive overlay attendue :
|
||||
|
||||
```text
|
||||
ksp-general-0.3.2-pre.001.zip
|
||||
```
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Ouvrir `0.3.2` uniquement par le gate prévu :
|
||||
|
||||
```text
|
||||
lecture règles + architecture + 0.3.1
|
||||
réaudit kbot3 physique ciblé
|
||||
réaudit PostgreSQL/tokio-postgres/pool/TLS/migrations
|
||||
brainstorming runtime/backend
|
||||
threat model
|
||||
dependency graph exact
|
||||
Config std.store candidate
|
||||
integration test strategy
|
||||
sizing + prévision souple
|
||||
réconciliation des règles Store contradictoires découvertes
|
||||
```
|
||||
|
||||
Aucune crate runtime, connexion, pool, TLS, migration ou Config Store n'est implémenté dans ce delta.
|
||||
|
||||
## 3. Divergence stable découverte
|
||||
|
||||
La base stable avait déjà adopté le backend séparé dans le plan `0.3.1` et les architectures 003/004/005, mais conservait l'ancien modèle dans :
|
||||
|
||||
```text
|
||||
KSP-API-006
|
||||
DEP-STORE-002
|
||||
docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md
|
||||
```
|
||||
|
||||
Le prompt 021 demande en plus de relire `DEP-STORE-001..010`, alors que la base ne définissait que `001..008`.
|
||||
|
||||
La réconciliation livrée fixe :
|
||||
|
||||
```text
|
||||
ksp-store-lib = façade/runtime commune
|
||||
ksp-store-postgres-lib = backend physique officiel
|
||||
backend -> ksp-store-api
|
||||
backend -X-> ksp-store-lib
|
||||
consumers ordinaires -> ksp-store-lib
|
||||
```
|
||||
|
||||
`DEP-STORE-009` et `DEP-STORE-010` sont ajoutées pour rendre la frontière normative explicite.
|
||||
|
||||
## 4. Décisions du gate
|
||||
|
||||
### PostgreSQL/driver
|
||||
|
||||
```text
|
||||
PostgreSQL minimal supporté : 15
|
||||
PostgreSQL de référence : 18.6
|
||||
driver : tokio-postgres 0.7.18
|
||||
```
|
||||
|
||||
### Pool
|
||||
|
||||
```text
|
||||
retenu : deadpool-postgres 0.14.x
|
||||
rejeté : pool KSP maison sans besoin démontré
|
||||
rejeté : bb8-postgres pour cette fondation, ownership de connection task moins adapté au close KSP
|
||||
```
|
||||
|
||||
### TLS
|
||||
|
||||
```text
|
||||
tokio-postgres-rustls 0.14.x
|
||||
rustls 0.23
|
||||
aws-lc-rs
|
||||
native system roots
|
||||
modes KSP : Disabled / VerifyFull uniquement
|
||||
```
|
||||
|
||||
### Migrations
|
||||
|
||||
```text
|
||||
moteur KSP privé
|
||||
SQL embarqué
|
||||
history table ksp_store_schema_migrations
|
||||
sentinel version 0
|
||||
SHA-256
|
||||
advisory transaction lock borné
|
||||
run transactionnel
|
||||
aucun down automatique
|
||||
aucune table RAW métier
|
||||
```
|
||||
|
||||
`refinery 0.9.2` a été audité mais n'est pas retenu.
|
||||
|
||||
### Config
|
||||
|
||||
```text
|
||||
std.store V1 possédé par ksp-config-lib
|
||||
KSP_SECRET_STORE_POSTGRES_URI
|
||||
settings typés
|
||||
aucun serde_json::Value backend_options
|
||||
aucun env/PG*/.pgpass lu par Store/backend
|
||||
```
|
||||
|
||||
### Health
|
||||
|
||||
Un health/readiness portable minimal est retenu pour distinguer Store construit, prêt et fermé sans exposer pool/URI/SQL.
|
||||
|
||||
## 5. Graphe cible
|
||||
|
||||
```text
|
||||
ksp-store-lib
|
||||
├── ksp-store-api
|
||||
├── ksp-logging-lib
|
||||
└── [postgres] ksp-store-postgres-lib
|
||||
|
||||
ksp-store-postgres-lib
|
||||
├── ksp-store-api
|
||||
├── ksp-logging-lib
|
||||
├── tokio
|
||||
├── tokio-postgres
|
||||
├── deadpool-postgres
|
||||
├── tokio-postgres-rustls
|
||||
├── rustls
|
||||
└── sha2
|
||||
```
|
||||
|
||||
Feature :
|
||||
|
||||
```text
|
||||
default = postgres
|
||||
--no-default-features doit compiler
|
||||
Postgres connu sans feature -> STORE_BACKEND_NOT_COMPILED avant I/O
|
||||
```
|
||||
|
||||
## 6. Héritage kbot3 ciblé
|
||||
|
||||
Surfaces relues : ancien `ks-store`, PostgreSQL, migration resources, health, Config et docs Store.
|
||||
|
||||
Résumé :
|
||||
|
||||
```text
|
||||
REPRENDRE façade intentionnelle, pool/timeout bornés, advisory lock, health, SQL privé
|
||||
REDESSINER crates séparées, tokio-postgres, settings typés, TLS, checksum/history, close, errors
|
||||
REPORTER 16 tables N1-N3, 240 SQL métier, 79 index, repositories/replay/ledgers
|
||||
REJETER SQLx, monolithe façade/backend, backend_options JSON, env direct, erreurs backend brutes
|
||||
```
|
||||
|
||||
Le détail est dans le plan 023.
|
||||
|
||||
## 7. Threat model
|
||||
|
||||
Le plan couvre explicitement :
|
||||
|
||||
```text
|
||||
credential leak URI/Debug/error/log
|
||||
PG*/.pgpass bypass Config
|
||||
connection string hostile
|
||||
connection storm/unbounded pool
|
||||
hung connect/migration/shutdown
|
||||
concurrent migration runners
|
||||
modified migration/checksum
|
||||
partial migration
|
||||
SQL/dynamic identifier injection
|
||||
feature/config mismatch
|
||||
connection task leaked/dropped
|
||||
schema history newer than runtime
|
||||
server error echo
|
||||
```
|
||||
|
||||
## 8. PostgreSQL integration strategy
|
||||
|
||||
Le test `#[ignore]` de `pre.008` lira une URI dédiée depuis stdin, refusera une metadata table préexistante, ne créera aucune table métier et ne détruira ni base ni schema.
|
||||
|
||||
Il prouvera :
|
||||
|
||||
```text
|
||||
connect >= PostgreSQL 15
|
||||
bootstrap initial
|
||||
idempotence
|
||||
concurrence
|
||||
checksum mismatch
|
||||
rollback d'un échec injecté
|
||||
health ready
|
||||
close borné
|
||||
cleanup metadata créée par le test
|
||||
```
|
||||
|
||||
La cible de gate est PostgreSQL 18.6.
|
||||
|
||||
## 9. Sizing recalibré
|
||||
|
||||
La prévision reste :
|
||||
|
||||
```text
|
||||
pre.001 audit/design/réconciliation normative
|
||||
pre.002 scaffold + feature graph
|
||||
pre.003 settings + selection/lifecycle contracts
|
||||
pre.004 Config std.store
|
||||
pre.005 connection + deadpool + Rustls
|
||||
pre.006 migration/bootstrap
|
||||
pre.007 composition + health/close
|
||||
pre.008 PostgreSQL integration réelle
|
||||
pre.009 hardening/completeness/dependency matrix
|
||||
pre.010 gate technique final
|
||||
pre.011 réconciliation documentaire finale
|
||||
pre.012 préparation publication minimale
|
||||
rel.001 publication stable
|
||||
```
|
||||
|
||||
La release reste clôturable sans absorber `RawTransaction` ou `RawAccountState`.
|
||||
|
||||
## 10. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
|
||||
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
|
||||
deltas/0.3.2/pre.001.md
|
||||
```
|
||||
|
||||
## 11. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
docs/rules/RULES_KSP.md
|
||||
docs/rules/RULES_DEPENDENCIES.md
|
||||
docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md
|
||||
```
|
||||
|
||||
## 12. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 13. Version Cargo
|
||||
|
||||
La prerelease non-fix synchronise :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2-pre.1
|
||||
```
|
||||
|
||||
Aucune crate runtime n'est encore ajoutée ; le changement versionne le gate `pre.001`.
|
||||
|
||||
## 14. Baseline opérateur `v0.3.1`
|
||||
|
||||
Le journal opérateur fourni à l'ouverture montre notamment :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
audit Rust général / exports / workspace PASS
|
||||
audit Markdown PASS — 186 tables / 138 fichiers
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
tests ciblés dont ksp-store-api PASS
|
||||
cargo test --workspace PASS
|
||||
builds Tauri SOL Prices/Wallet/Config Desk PASS
|
||||
cargo tree -p ksp-store-api --edges normal ksp-store-api -> ksp-core-lib
|
||||
cargo tree --duplicates exécuté
|
||||
```
|
||||
|
||||
Cette preuve de base ne remplace pas le gate après application de `pre.001`.
|
||||
|
||||
## 15. Validations après application
|
||||
|
||||
Dans l’environnement de génération du présent overlay, les contrôles suivants ont été réellement exécutés après modification :
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
Markdown table audit: clean (186 tables, 124 files)
|
||||
```
|
||||
|
||||
`cargo` n’est pas installé dans l’environnement de génération. Les commandes suivantes n’ont donc pas été rejouées ici et ne sont pas déclarées PASS :
|
||||
|
||||
```text
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
```
|
||||
|
||||
Aucun code Rust n’est modifié par `pre.001`; le seul changement Cargo est `workspace.package.version`. Le baseline opérateur `v0.3.1` reste vert, mais ne remplace pas le gate opérateur après application.
|
||||
|
||||
## 16. Validations non requises dans cette tranche
|
||||
|
||||
```text
|
||||
cargo test -p ksp-store-lib crate encore absente
|
||||
cargo test -p ksp-store-postgres-lib crate encore absente
|
||||
PostgreSQL live réservé à pre.008
|
||||
pool/TLS connection tests réservés à pre.005+
|
||||
migration runtime tests réservés à pre.006+
|
||||
```
|
||||
|
||||
## 17. Questions ouvertes
|
||||
|
||||
Aucune question architecturale ne bloque `pre.002`.
|
||||
|
||||
Les détails d'implémentation volontairement réservés aux tranches dédiées sont :
|
||||
|
||||
```text
|
||||
noms Rust exacts des settings/errors
|
||||
mapping deadpool exact des timeouts
|
||||
construction rustls root store exacte
|
||||
DDL final de history metadata
|
||||
forme finale health snapshots
|
||||
```
|
||||
|
||||
Ils ne rouvrent pas :
|
||||
|
||||
```text
|
||||
tokio-postgres
|
||||
deadpool-postgres
|
||||
Rustls VerifyFull/Disabled
|
||||
migrations KSP-owned
|
||||
backend crate séparée
|
||||
Config ownership
|
||||
absence de RAW métier en 0.3.2
|
||||
```
|
||||
|
||||
## 18. Application et validation opérateur
|
||||
|
||||
Après application de l'overlay :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
```
|
||||
|
||||
Aucun scaffold Store, SQL, migration runtime ou Config `std.store` ne doit être ajouté à ce delta.
|
||||
122
deltas/0.3.2/pre.002-fix.001.md
Normal file
122
deltas/0.3.2/pre.002-fix.001.md
Normal file
@@ -0,0 +1,122 @@
|
||||
<!-- file: deltas/0.3.2/pre.002-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.002-fix.001` — ancrage compile-time des targets de tracing
|
||||
|
||||
## 1. Déclencheur
|
||||
|
||||
Le gate opérateur de `pre.002` est fonctionnellement vert :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
audit Rust général / exports / workspace PASS
|
||||
audit Markdown PASS — 186 tables / 125 files
|
||||
cargo check --workspace PASS avec warnings
|
||||
cargo clippy --workspace --all-targets PASS avec warnings
|
||||
cargo test -p ksp-store-api PASS
|
||||
cargo test -p ksp-store-lib PASS
|
||||
cargo test -p ksp-store-postgres-lib PASS
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
cargo tree Store facade/backend fourni et conforme
|
||||
```
|
||||
|
||||
L'écart est limité aux deux nouveaux scaffolds :
|
||||
|
||||
```text
|
||||
unused import: self::constants::TRACING_TARGET
|
||||
constant TRACING_TARGET is never used
|
||||
```
|
||||
|
||||
Le même défaut existe dans `ksp-store-lib` et `ksp-store-postgres-lib`.
|
||||
|
||||
## 2. Diagnostic
|
||||
|
||||
`pre.002` ne contient encore aucun comportement Store/PostgreSQL et n'ajoute volontairement pas `ksp-logging-lib`. Le simple réexport crate-private du `TRACING_TARGET` obligatoire ne possède donc aucun site d'usage réel à cette tranche, ce qui produit les warnings observés.
|
||||
|
||||
Supprimer les targets n'est pas possible : l'audit workspace impose déjà :
|
||||
|
||||
```text
|
||||
KSP-TRACE-102 — constants.rs possède le TRACING_TARGET canonique
|
||||
KSP-TRACE-103 — le crate root réexporte TRACING_TARGET
|
||||
```
|
||||
|
||||
Ajouter un faux log, assouplir les règles ou poser un `allow(dead_code/unused_imports)` masquerait le staging réel.
|
||||
|
||||
## 3. Correction
|
||||
|
||||
Les deux targets et leurs réexports sont conservés à l'identique. Chaque crate ajoute uniquement un canari compile-time privé :
|
||||
|
||||
```rust
|
||||
const _: &str = TRACING_TARGET;
|
||||
```
|
||||
|
||||
Ce canari :
|
||||
|
||||
```text
|
||||
marque le réexport et la constante comme réellement consommés par le scaffold compilé
|
||||
ne crée aucun comportement runtime
|
||||
n'émet aucun log artificiel
|
||||
n'ajoute pas ksp-logging-lib
|
||||
n'assouplit aucun lint
|
||||
préserve KSP-TRACE-102/103
|
||||
```
|
||||
|
||||
Les tests de frontière figent aussi la présence de ce canari pour empêcher le retour des warnings tant que le premier comportement réel n'a pas remplacé cet usage de scaffold.
|
||||
|
||||
## 4. Version
|
||||
|
||||
Le correctif touche des sources Rust ; conformément à `VER-ID-007` et `VER-ID-010`, la version workspace devient :
|
||||
|
||||
```text
|
||||
0.3.2-pre.2.fix.1
|
||||
```
|
||||
|
||||
Identifiant de livraison :
|
||||
|
||||
```text
|
||||
0.3.2-pre.002-fix.001
|
||||
```
|
||||
|
||||
## 5. Documentation durable
|
||||
|
||||
Le plan 023 et la validation 019 sont synchronisés :
|
||||
|
||||
```text
|
||||
graphe pre.002 confirmé par le gate opérateur
|
||||
--no-default-features confirmé
|
||||
cycle backend absent confirmé
|
||||
warning TRACING_TARGET documenté puis corrigé par fix.001
|
||||
TRACING_TARGET conservé et ancré compile-time sans faux log
|
||||
```
|
||||
|
||||
Le delta `pre.002.md` n'est pas réécrit : une livraison déjà émise reste immuable et son correctif est tracé séparément.
|
||||
|
||||
## 6. Hors scope
|
||||
|
||||
Aucun changement de périmètre fonctionnel :
|
||||
|
||||
```text
|
||||
pas de StoreSettings
|
||||
pas de Store runtime public
|
||||
pas de Config std.store
|
||||
pas de ksp-logging-lib
|
||||
pas de Tokio/tokio-postgres/Deadpool/Rustls
|
||||
pas de connexion PostgreSQL
|
||||
pas de migration
|
||||
pas de SQL métier
|
||||
```
|
||||
|
||||
## 7. Validation opérateur requise
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
```
|
||||
|
||||
Le passage à `pre.003` reste interdit si un warning ou un nouvel écart apparaît.
|
||||
244
deltas/0.3.2/pre.002.md
Normal file
244
deltas/0.3.2/pre.002.md
Normal file
@@ -0,0 +1,244 @@
|
||||
<!-- file: deltas/0.3.2/pre.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.002` — scaffold Store runtime/PostgreSQL et feature graph
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
```text
|
||||
0.3.2-pre.001
|
||||
```
|
||||
|
||||
Le gate opérateur de `pre.001` est fourni vert :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
audit Rust général / exports / workspace PASS
|
||||
audit Markdown PASS — 186 tables / 124 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
```
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Créer uniquement les deux crates runtime prévues par le plan `0.3.2` et matérialiser le feature graph PostgreSQL sans avancer les settings, la Config Store, la connexion, le pool, TLS, les migrations ou les vertical slices RAW.
|
||||
|
||||
Crates créées :
|
||||
|
||||
```text
|
||||
ksp-store-lib
|
||||
ksp-store-postgres-lib
|
||||
```
|
||||
|
||||
## 3. Version
|
||||
|
||||
Le workspace passe à :
|
||||
|
||||
```text
|
||||
0.3.2-pre.2
|
||||
```
|
||||
|
||||
Les deux crates sont ajoutées aux membres du workspace.
|
||||
|
||||
## 4. Graphe matérialisé
|
||||
|
||||
```text
|
||||
ksp-store-lib
|
||||
├── ksp-store-api
|
||||
└── [default feature postgres]
|
||||
└── ksp-store-postgres-lib
|
||||
└── ksp-store-api
|
||||
```
|
||||
|
||||
Invariants :
|
||||
|
||||
```text
|
||||
ksp-store-postgres-lib -X-> ksp-store-lib
|
||||
ksp-store-lib -X-> Config/Transport/Program/Materializer
|
||||
ksp-store-postgres-lib -X-> Config/Transport/Program/Materializer
|
||||
```
|
||||
|
||||
Feature exacte :
|
||||
|
||||
```toml
|
||||
[features]
|
||||
default = ["postgres"]
|
||||
postgres = ["dep:ksp-store-postgres-lib"]
|
||||
```
|
||||
|
||||
`ksp-store-postgres-lib` reste une optional dependency de la façade afin que :
|
||||
|
||||
```text
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
```
|
||||
|
||||
puisse compiler sans backend PostgreSQL.
|
||||
|
||||
## 5. Dépendances volontairement différées
|
||||
|
||||
`pre.002` n'ajoute pas encore :
|
||||
|
||||
```text
|
||||
ksp-logging-lib
|
||||
tokio
|
||||
tokio-postgres
|
||||
deadpool-postgres
|
||||
tokio-postgres-rustls
|
||||
rustls
|
||||
sha2
|
||||
```
|
||||
|
||||
Ce n'est pas une remise en cause du graphe cible du plan 023. Aucun comportement de production ne consomme encore ces crates dans le scaffold ; les ajouter maintenant violerait le contrat selon lequel une dépendance de package correspond à un usage réel.
|
||||
|
||||
Le séquencement reste :
|
||||
|
||||
```text
|
||||
ksp-logging-lib façade lors du premier comportement Store réel
|
||||
ksp-logging-lib backend lors du premier comportement PostgreSQL réel
|
||||
tokio/driver/pool/TLS pre.005
|
||||
sha2 pre.006
|
||||
```
|
||||
|
||||
Les deux crates possèdent cependant dès maintenant leur `src/constants.rs` et le `TRACING_TARGET` crate-owned attendu, sans émission de log artificielle.
|
||||
|
||||
## 6. Surface Rust
|
||||
|
||||
`ksp-store-lib` reste un scaffold sans API runtime publique. Il ne matérialise pas encore :
|
||||
|
||||
```text
|
||||
Store
|
||||
StoreSettings
|
||||
StoreBackendSettings
|
||||
PostgresStoreSettings
|
||||
StoreBackendKind
|
||||
errors Store runtime
|
||||
reexports ksp-store-api
|
||||
```
|
||||
|
||||
Ces éléments restent réservés à `pre.003`.
|
||||
|
||||
`ksp-store-postgres-lib` n'expose encore aucun type backend public et ne contient aucun driver, pool, SQL, migration ou health runtime.
|
||||
|
||||
Aucun `pub mod` n'est introduit.
|
||||
|
||||
## 7. Canaris ajoutés
|
||||
|
||||
### `ksp-store-lib`
|
||||
|
||||
`tests/dependency_boundary.rs` vérifie :
|
||||
|
||||
```text
|
||||
default feature postgres exact
|
||||
optional backend dependency exacte
|
||||
façade -> ksp-store-api
|
||||
absence Config/Transport/Program/Materializer
|
||||
absence driver/pool/TLS prématurés
|
||||
absence de settings/Store/backend types publics en pre.002
|
||||
TRACING_TARGET possédé par constants.rs
|
||||
```
|
||||
|
||||
### `ksp-store-postgres-lib`
|
||||
|
||||
`tests/dependency_boundary.rs` vérifie :
|
||||
|
||||
```text
|
||||
backend -> ksp-store-api
|
||||
absence backend -> ksp-store-lib
|
||||
absence Config/Transport/Program/Materializer
|
||||
absence Tokio/tokio-postgres/Deadpool/Rustls/SHA-256
|
||||
absence SQL/migration/runtime surface prématurée
|
||||
TRACING_TARGET possédé par constants.rs
|
||||
```
|
||||
|
||||
## 8. Documentation mise à jour
|
||||
|
||||
```text
|
||||
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
|
||||
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
|
||||
```
|
||||
|
||||
Le plan marque le scaffold comme matérialisé et précise le staging des dépendances runtime. La validation conserve les critères Cargo en `TODO` tant que le gate opérateur de cette tranche n'a pas été fourni.
|
||||
|
||||
## 9. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-store-lib/Cargo.toml
|
||||
crates/ksp-store-lib/src/constants.rs
|
||||
crates/ksp-store-lib/src/lib.rs
|
||||
crates/ksp-store-lib/tests/dependency_boundary.rs
|
||||
crates/ksp-store-postgres-lib/Cargo.toml
|
||||
crates/ksp-store-postgres-lib/src/constants.rs
|
||||
crates/ksp-store-postgres-lib/src/lib.rs
|
||||
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||
deltas/0.3.2/pre.002.md
|
||||
```
|
||||
|
||||
## 10. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
|
||||
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
|
||||
```
|
||||
|
||||
## 11. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 12. Validations exécutées dans l'environnement de génération
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
Markdown table audit: clean (186 tables, 125 files)
|
||||
```
|
||||
|
||||
Une validation statique complémentaire des manifests/features confirme également le graphe exact attendu et les deux canaris de frontière ont été rejoués par équivalent textuel dans l’environnement sans Cargo.
|
||||
|
||||
## 13. Validations opérateur requises
|
||||
|
||||
`cargo`, `rustc` et `rustfmt` ne sont pas disponibles dans l'environnement de génération. Après application :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-api
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
cargo tree -p ksp-store-lib --edges normal
|
||||
cargo tree -p ksp-store-lib -e features
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||
```
|
||||
|
||||
Une commande non exécutée n'est pas déclarée PASS.
|
||||
|
||||
## 14. Hors scope confirmé
|
||||
|
||||
```text
|
||||
StoreSettings / backend selection runtime
|
||||
STORE_BACKEND_NOT_COMPILED
|
||||
Config std.store
|
||||
URI/secrets Store
|
||||
connexion PostgreSQL
|
||||
pool Deadpool
|
||||
TLS Rustls
|
||||
migrations/bootstrap
|
||||
health/readiness runtime
|
||||
SQL métier
|
||||
RawTransaction PostgreSQL
|
||||
RawAccountState PostgreSQL
|
||||
```
|
||||
|
||||
## 15. Suite
|
||||
|
||||
`0.3.2-pre.003` matérialise les settings backend-neutral, l'identité/sélection de backend, les erreurs stables de façade et le lifecycle contract `Store` sans encore ouvrir la connexion PostgreSQL lourde.
|
||||
143
deltas/0.3.2/pre.003-fix.001.md
Normal file
143
deltas/0.3.2/pre.003-fix.001.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<!-- file: deltas/0.3.2/pre.003-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.003-fix.001` — conformité Clippy et canari de frontière Store
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base exacte :
|
||||
|
||||
```text
|
||||
0.3.2-pre.003
|
||||
workspace.package.version = 0.3.2-pre.3
|
||||
```
|
||||
|
||||
Le gate opérateur fourni le 29 août 2026 confirme :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
audit Rust général / exports / workspace PASS
|
||||
audit Markdown PASS — 186 tables / 127 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets FAIL — 5 implicit_return observés dans les tests Store
|
||||
cargo test -p ksp-store-api PASS
|
||||
cargo test -p ksp-store-lib FAIL — faux positif dependency_boundary sur PostgresPoolSettings
|
||||
cargo test -p ksp-store-lib --no-default-features FAIL — même faux positif dependency_boundary
|
||||
cargo test -p ksp-store-postgres-lib PASS
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
```
|
||||
|
||||
Aucun défaut de production/runtime n'est observé : les 6 tests unitaires Store passent dans les deux configurations de feature. Le correctif reste strictement dans le couloir `pre.003`.
|
||||
|
||||
## 2. Objet
|
||||
|
||||
Corriger exclusivement :
|
||||
|
||||
```text
|
||||
les closures de tests non conformes à clippy::implicit_return
|
||||
le canari dependency_boundary trop large qui interdit le mot générique Pool
|
||||
```
|
||||
|
||||
Aucune surface API, borne, erreur, lifecycle, feature, dépendance ou comportement Store n'est modifié.
|
||||
|
||||
## 3. Clippy `implicit_return`
|
||||
|
||||
Les closures de projection de code d'erreur utilisent désormais la forme explicite requise par `RUST-ERR-003` :
|
||||
|
||||
```rust
|
||||
|value| return value.code()
|
||||
```
|
||||
|
||||
La correction couvre toutes les occurrences de la tranche, y compris les branches compilées uniquement avec `--no-default-features`.
|
||||
|
||||
## 4. Canari de frontière physique
|
||||
|
||||
Le test `pre_003_facade_adds_only_backend_neutral_settings_lifecycle_and_api_reexports` interdisait le token nu :
|
||||
|
||||
```text
|
||||
Pool
|
||||
```
|
||||
|
||||
Cette condition est incorrecte car `PostgresPoolSettings` est une surface de configuration backend-neutral explicitement prévue par `pre.003`. Elle provoquait donc un faux positif tout en ne prouvant pas mieux l'absence de fuite physique.
|
||||
|
||||
Le canari interdit désormais les chemins/types physiques ciblés :
|
||||
|
||||
```text
|
||||
pub use ksp_store_postgres_lib
|
||||
tokio_postgres
|
||||
deadpool_postgres
|
||||
rustls::
|
||||
deadpool::managed::Pool
|
||||
tokio_postgres::Client
|
||||
tokio_postgres::Row
|
||||
tokio_postgres::Statement
|
||||
```
|
||||
|
||||
Les protections existantes sur le manifeste et les production sources restent inchangées.
|
||||
|
||||
## 5. Version Cargo
|
||||
|
||||
Le correctif modifie des fichiers Rust de tests consommés par le build. Conformément à `VER-ID-007` et `VER-ID-010` :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2-pre.3.fix.1
|
||||
```
|
||||
|
||||
Livraison :
|
||||
|
||||
```text
|
||||
0.3.2-pre.003-fix.001
|
||||
```
|
||||
|
||||
## 6. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-lib/tests/dependency_boundary.rs
|
||||
crates/ksp-store-lib/tests/feature_mismatch.rs
|
||||
crates/ksp-store-lib/unit_tests/settings.rs
|
||||
crates/ksp-store-lib/unit_tests/store.rs
|
||||
```
|
||||
|
||||
Chaque fichier Rust réellement modifié incrémente son header de version une seule fois. Aucun autre fichier source n'est touché.
|
||||
|
||||
## 7. Fichier ajouté
|
||||
|
||||
```text
|
||||
deltas/0.3.2/pre.003-fix.001.md
|
||||
```
|
||||
|
||||
## 8. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 9. Validations exécutées dans l'environnement de génération
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
```
|
||||
|
||||
`cargo`, `rustc` et `rustfmt` ne sont pas disponibles dans l'environnement de génération. Aucun gate Cargo n'est donc déclaré PASS ici.
|
||||
|
||||
## 10. Gate opérateur requis
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
```
|
||||
|
||||
## 11. Décisions et questions ouvertes
|
||||
|
||||
Décision : un canari de fuite backend doit cibler des symboles/chemins physiques, pas interdire des mots génériques faisant partie de settings publics légitimes.
|
||||
|
||||
Question ouverte : aucune. Si le gate est vert, la suite reste `0.3.2-pre.004` — Config `std.store`.
|
||||
214
deltas/0.3.2/pre.003.md
Normal file
214
deltas/0.3.2/pre.003.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Delta `0.3.2-pre.003` — Store settings, backend selection et lifecycle contract
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base exacte :
|
||||
|
||||
```text
|
||||
0.3.2-pre.002-fix.001
|
||||
workspace.package.version = 0.3.2-pre.2.fix.1
|
||||
```
|
||||
|
||||
Le gate opérateur fourni le 29 août 2026 est entièrement vert et sans warning :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
audit Rust général / exports / workspace PASS
|
||||
audit Markdown PASS — 186 tables / 126 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test -p ksp-store-lib PASS
|
||||
cargo test -p ksp-store-postgres-lib PASS
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
```
|
||||
|
||||
Le correctif `pre.002-fix.001` est déjà commité. `pre.003` ne réécrit pas ce delta. Le crate-root `ksp-store-lib` étant réellement modifié dans cette tranche, son ancrage `TRACING_TARGET` adopte désormais la forme canonique `const _: &str = crate::TRACING_TARGET;`. `crates/ksp-store-lib/src/constants.rs` n'est pas modifié et conserve donc son header/version existant.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Matérialiser uniquement le contrat runtime backend-neutral prévu par le plan 023 :
|
||||
|
||||
```text
|
||||
StoreBackendKind
|
||||
StoreBackendSettings
|
||||
PostgresStoreSettings
|
||||
PostgresPoolSettings
|
||||
PostgresBootstrapSettings
|
||||
PostgresTlsMode
|
||||
StoreSettings
|
||||
Store::open(settings).await
|
||||
Store::close(self).await
|
||||
codes d'erreur Store stables
|
||||
réexports ksp-store-api depuis la façade
|
||||
```
|
||||
|
||||
Aucune connexion, pool physique, TLS connector, migration, Config `std.store` ou SQL n'est introduit.
|
||||
|
||||
## 3. Settings et bornes
|
||||
|
||||
`PostgresStoreSettings` possède une URI explicitement fournie par le caller. Il n'existe aucun `Default` qui invente une URI ou un backend.
|
||||
|
||||
La chaîne URI :
|
||||
|
||||
```text
|
||||
est possédée par le settings
|
||||
est intégralement redacted dans Debug
|
||||
n'a aucun getter public
|
||||
n'entre dans aucun ErrorContext
|
||||
n'est pas parsée avant pre.005
|
||||
```
|
||||
|
||||
Les settings typés matérialisent les bornes décidées en `pre.001` :
|
||||
|
||||
```text
|
||||
max_connections défaut 8 plage 1..64
|
||||
connect_timeout_ms défaut 10_000 plage 100..60_000
|
||||
pool_wait_timeout_ms défaut 5_000 plage 100..60_000
|
||||
pool_create_timeout_ms défaut 10_000 plage 100..60_000
|
||||
pool_recycle_timeout_ms défaut 5_000 plage 100..60_000
|
||||
shutdown_timeout_ms défaut 5_000 plage 100..30_000
|
||||
auto_migrate défaut true
|
||||
migration_timeout_ms défaut 30_000 plage 1_000..300_000
|
||||
migration_lock_ms défaut 10_000 plage 100..120_000
|
||||
```
|
||||
|
||||
Ces bornes restent des garde-fous de ressources/lifecycle et non une policy worker/job.
|
||||
|
||||
## 4. Backend selection et lifecycle
|
||||
|
||||
Backend connu :
|
||||
|
||||
```text
|
||||
StoreBackendKind::Postgres
|
||||
```
|
||||
|
||||
Le dispatch `Store::open` valide d'abord tous les settings puis :
|
||||
|
||||
```text
|
||||
feature postgres absente -> store.backend_not_compiled avant I/O
|
||||
feature postgres présente -> store.backend_open_failed / runtime_foundation_pending avant I/O
|
||||
```
|
||||
|
||||
Le second résultat est volontairement transitoire : `pre.003` ne fabrique jamais un faux `Store` prêt avant la connexion réelle de `pre.005`. La signature finale `Store::close(self).await` est fixée maintenant, mais aucune ressource physique n'existe encore à fermer.
|
||||
|
||||
## 5. Erreurs Store communes
|
||||
|
||||
Codes crate-root :
|
||||
|
||||
```text
|
||||
ERROR_CODE_BACKEND_CLOSED -> store.backend_closed
|
||||
ERROR_CODE_BACKEND_NOT_COMPILED -> store.backend_not_compiled
|
||||
ERROR_CODE_BACKEND_OPEN_FAILED -> store.backend_open_failed
|
||||
ERROR_CODE_SETTINGS_INVALID -> store.settings_invalid
|
||||
ERROR_CODE_SHUTDOWN_TIMEOUT -> store.shutdown_timeout
|
||||
```
|
||||
|
||||
Aucune erreur externe ou chaîne distante n'est attachée à cette tranche.
|
||||
|
||||
## 6. Réexports API
|
||||
|
||||
`ksp-store-lib` réexporte explicitement les 60 éléments crate-root de `ksp-store-api` nécessaires à la consommation de la façade.
|
||||
|
||||
La façade compte donc à cette tranche :
|
||||
|
||||
```text
|
||||
60 reexports ksp-store-api
|
||||
13 éléments runtime Store propres
|
||||
73 exports crate-root au total
|
||||
```
|
||||
|
||||
Aucun type, handle ou symbole de `ksp-store-postgres-lib` n'est réexporté.
|
||||
|
||||
## 7. Fichiers
|
||||
|
||||
Modifiés :
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-lib/src/lib.rs
|
||||
crates/ksp-store-lib/tests/dependency_boundary.rs
|
||||
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
|
||||
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
|
||||
```
|
||||
|
||||
Ajoutés :
|
||||
|
||||
```text
|
||||
crates/ksp-store-lib/src/error.rs
|
||||
crates/ksp-store-lib/src/settings.rs
|
||||
crates/ksp-store-lib/src/store.rs
|
||||
crates/ksp-store-lib/unit_tests/settings.rs
|
||||
crates/ksp-store-lib/unit_tests/store.rs
|
||||
crates/ksp-store-lib/tests/feature_mismatch.rs
|
||||
crates/ksp-store-lib/tests/public_api.rs
|
||||
deltas/0.3.2/pre.003.md
|
||||
```
|
||||
|
||||
Non modifiés intentionnellement :
|
||||
|
||||
```text
|
||||
crates/ksp-store-lib/src/constants.rs
|
||||
crates/ksp-store-postgres-lib/**
|
||||
ksp-store-api/**
|
||||
config/**
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
```
|
||||
|
||||
## 8. Version
|
||||
|
||||
La tranche touche le runtime/API Rust ; la version workspace devient :
|
||||
|
||||
```text
|
||||
0.3.2-pre.3
|
||||
```
|
||||
|
||||
Identifiant de livraison :
|
||||
|
||||
```text
|
||||
0.3.2-pre.003
|
||||
```
|
||||
|
||||
## 9. Validations exécutées pendant la préparation
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||
```
|
||||
|
||||
Les validations Cargo ne sont pas disponibles dans l'environnement de génération et ne sont jamais déclarées PASS.
|
||||
|
||||
## 10. Gate opérateur requis
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-api
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
cargo tree -p ksp-store-lib --edges normal
|
||||
cargo tree -p ksp-store-lib -e features
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||
```
|
||||
|
||||
## 11. Hors scope et suite
|
||||
|
||||
Toujours absents :
|
||||
|
||||
```text
|
||||
Config std.store
|
||||
tokio-postgres
|
||||
Deadpool
|
||||
Rustls
|
||||
connexion PostgreSQL
|
||||
migrations/bootstrap physique
|
||||
health/readiness physique
|
||||
RawTransaction PostgreSQL
|
||||
RawAccountState PostgreSQL
|
||||
```
|
||||
|
||||
Si le gate est vert, `0.3.2-pre.004` ouvre exclusivement Config `std.store`, son schema/example/registry/adaptor et les impacts de packaging strictement requis.
|
||||
77
deltas/0.3.2/pre.004-fix.001.md
Normal file
77
deltas/0.3.2/pre.004-fix.001.md
Normal file
@@ -0,0 +1,77 @@
|
||||
<!-- file: deltas/0.3.2/pre.004-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.004-fix.001` — Store targets réseau-spécifiques + gate fixes
|
||||
|
||||
## 1. Base
|
||||
|
||||
Base exacte : `0.3.2-pre.004`. Le gate opérateur du 29 août 2026 confirme `cargo check`, les tests Config/Store principaux et les trois builds Tauri, mais révèle :
|
||||
|
||||
```text
|
||||
1 warning Clippy collapsible_if dans ksp-config-lib/unit_tests/store.rs
|
||||
1 échec Config Desk : fixture cfg.std.store absente
|
||||
```
|
||||
|
||||
La discussion de design menée pendant ce gate confirme aussi qu'une instance `Store` ne doit jamais multiplexer plusieurs réseaux.
|
||||
|
||||
## 2. Décision target/réseau
|
||||
|
||||
Le mécanisme de profiles Config existant devient directement le mécanisme de targets Store nommés :
|
||||
|
||||
```text
|
||||
profile_id = target Store nommé
|
||||
default_profile = target autonome
|
||||
profile forcé = target explicitement imposé par job/worker/app
|
||||
1 target = 1 network + 1 backend + 1 URI/base
|
||||
1 StoreSettings = 1 RawNetworkId + 1 backend
|
||||
```
|
||||
|
||||
`ksp-store-lib` ne contient aucun registry ni routeur multi-target. Pour changer de réseau, le consumer résout un autre profile `std.store` puis ouvre une autre instance Store.
|
||||
|
||||
## 3. Targets committed
|
||||
|
||||
```text
|
||||
devnet -> network devnet -> KSP_SECRET_STORE_DEVNET_POSTGRES_URI -> ksp_devnet
|
||||
mainnet -> network mainnet-beta -> KSP_SECRET_STORE_MAINNET_POSTGRES_URI -> ksp_mainnet
|
||||
testnet -> network testnet -> KSP_SECRET_STORE_TESTNET_POSTGRES_URI -> ksp_testnet
|
||||
```
|
||||
|
||||
`default_profile = devnet`, cohérent avec le défaut Transport et plus sûr pour les workflows de développement. Le schema reste extensible à d'autres targets/réseaux sûrs.
|
||||
|
||||
## 4. Deuxième protection réseau
|
||||
|
||||
`StoreSettings` exige désormais un `RawNetworkId`. Les futures opérations RAW devront rejeter avant I/O toute identité/query dont le réseau ne correspond pas au réseau de l'instance Store. Cette vérification ne route jamais automatiquement vers un autre target.
|
||||
|
||||
## 5. Correctifs de gate
|
||||
|
||||
- collapse du `if let` imbriqué signalé par Clippy ;
|
||||
- ajout de `crates/ksp-config-lib/unit_tests/fixtures/std.store.json` ;
|
||||
- Config Desk vérifie désormais explicitement que `cfg.std.store` appartient à l'inventaire profilé ;
|
||||
- tests Config ajoutés pour la sélection explicite et indépendante de `devnet`, `mainnet` et `testnet`.
|
||||
|
||||
## 6. Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2-pre.4.fix.1
|
||||
```
|
||||
|
||||
## 7. Gate opérateur
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-config-lib
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
cargo test -p ksp-app-config-desk
|
||||
```
|
||||
|
||||
Les trois builds Tauri `pre.004` sont déjà verts et les resources packagées ne changent pas dans ce fix.
|
||||
|
||||
## 8. Suite
|
||||
|
||||
Après gate propre, `pre.005` peut ouvrir la connexion PostgreSQL physique. Elle consommera un seul target/réseau déjà résolu ; aucun multiplexage réseau n'est à introduire.
|
||||
108
deltas/0.3.2/pre.004.md
Normal file
108
deltas/0.3.2/pre.004.md
Normal file
@@ -0,0 +1,108 @@
|
||||
<!-- file: deltas/0.3.2/pre.004.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.004` — Config `std.store`
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base exacte :
|
||||
|
||||
```text
|
||||
0.3.2-pre.003-fix.001
|
||||
workspace.package.version = 0.3.2-pre.3.fix.1
|
||||
```
|
||||
|
||||
Le gate opérateur fourni le 29 août 2026 est entièrement vert : audits Rust/Markdown, `cargo check`, Clippy, tests Store avec et sans features par défaut, backend et compilation `--no-default-features` passent.
|
||||
|
||||
## 2. Objet
|
||||
|
||||
Matérialiser exclusivement la frontière Config de la fondation Store :
|
||||
|
||||
```text
|
||||
config/std.store.json
|
||||
config/schemas/std.store.schema.json
|
||||
config/examples/std.store.example.json
|
||||
registry cfg.std.store / schema.std.store
|
||||
KSP_SECRET_STORE_POSTGRES_URI
|
||||
adapter Config -> ksp_store_lib::StoreSettings
|
||||
provenance/sensitivity/redaction
|
||||
resources desktop strictement nécessaires
|
||||
```
|
||||
|
||||
Aucune connexion, pool physique, TLS connector, migration ou SQL n'est ajouté.
|
||||
|
||||
## 3. Graphe de dépendance
|
||||
|
||||
`ksp-config-lib` ajoute :
|
||||
|
||||
```text
|
||||
ksp-config-lib -> ksp-store-lib (default-features = false)
|
||||
```
|
||||
|
||||
Config peut ainsi construire les types publics de settings sans forcer `postgres`. `ksp-store-lib` et `ksp-store-postgres-lib` restent interdits de dépendance vers Config.
|
||||
|
||||
## 4. Document V1
|
||||
|
||||
Le profil `postgres_default` sélectionne `backend = postgres` et fournit exactement les groupes `connection_uri`, `pool`, `tls`, `bootstrap` et `shutdown_timeout_ms` décidés en `pre.001/pre.003`. Les bornes JSON Schema reflètent les bornes runtime Store ; la validation runtime reste l'autorité finale.
|
||||
|
||||
Le `connection_uri` committed utilise :
|
||||
|
||||
```text
|
||||
${KSP_SECRET_STORE_POSTGRES_URI:-postgresql://localhost/ksp}
|
||||
```
|
||||
|
||||
Le fallback reste classé `Secret`, donc la safe projection et `Debug` ne rendent jamais l'URI. L'adapter rejette aussi un URI literal ou provenant d'un namespace non secret, même si le JSON Schema l'accepte syntaxiquement.
|
||||
|
||||
## 5. Adapter
|
||||
|
||||
`ResolvedStoreConfig` possède la Config effective et un `StoreSettings`. Il expose un emprunt sûr et `into_settings(self)` pour transférer les settings au runtime. Aucun getter de l'URI n'est ajouté.
|
||||
|
||||
Mapping :
|
||||
|
||||
```text
|
||||
postgres -> StoreBackendSettings::Postgres
|
||||
disabled -> PostgresTlsMode::Disabled
|
||||
verify_full -> PostgresTlsMode::VerifyFull
|
||||
ms -> Duration
|
||||
```
|
||||
|
||||
Les erreurs du contrat Store sont projetées par domaine/code seulement, sans recopier de message ou de contexte arbitraire.
|
||||
|
||||
## 6. Packaging
|
||||
|
||||
`prepare_packaged_runtime` parcourt tout le registry Config. Les trois applications desktop qui utilisent cette préparation doivent donc embarquer les deux nouvelles resources runtime/schema. Leur inventaire passe de 11 à 13 resources. Aucun écran Store n'est ajouté.
|
||||
|
||||
## 7. Réconciliation du plan
|
||||
|
||||
Le prompt de démarrage et les conventions Config utilisent `std.store.schema.json` / `std.store.example.json`. Deux mentions `store.schema.json` / `store.example.json` du plan `pre.001` étaient incohérentes ; elles sont corrigées au moment de la matérialisation, sans changement de contrat.
|
||||
|
||||
## 8. Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2-pre.4
|
||||
```
|
||||
|
||||
## 9. Gate opérateur requis
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-config-lib
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
cargo test -p ksp-app-config-desk
|
||||
cargo test -p ksp-app-solprices-desk
|
||||
cargo test -p ksp-app-wallet-desk
|
||||
cargo tree -p ksp-config-lib --edges normal
|
||||
cargo tree -p ksp-store-lib -e features
|
||||
```
|
||||
|
||||
Les builds Tauri sont recommandés dans cette tranche car les resources packagées ont réellement changé.
|
||||
|
||||
## 10. Suite
|
||||
|
||||
Si le gate est vert, `pre.005` ouvre la connexion PostgreSQL réelle, Deadpool et Rustls.
|
||||
54
deltas/0.3.2/pre.005-fix.001.md
Normal file
54
deltas/0.3.2/pre.005-fix.001.md
Normal file
@@ -0,0 +1,54 @@
|
||||
<!-- file: deltas/0.3.2/pre.005-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.005-fix.001` — correction des assertions `connect_timeout`
|
||||
|
||||
## 1. Base
|
||||
|
||||
Base exacte : `0.3.2-pre.005` telle qu'appliquée et formatée par l'opérateur.
|
||||
|
||||
Le gate du 29 août 2026 confirme :
|
||||
|
||||
```text
|
||||
audits Rust/Markdown propres
|
||||
cargo check --workspace propre
|
||||
production ksp-store-postgres-lib compilable
|
||||
graphes Store/PostgreSQL conformes
|
||||
tests ksp-store-lib avec et sans feature PostgreSQL propres
|
||||
```
|
||||
|
||||
La seule erreur bloquante est dans deux assertions unitaires de `ksp-store-postgres-lib` : `tokio_postgres::Config::get_connect_timeout()` retourne `Option<&Duration>`, tandis que le test comparait à `Option<Duration>`.
|
||||
|
||||
## 2. Correctif
|
||||
|
||||
Les deux assertions de `unit_tests/runtime.rs` comparent désormais la référence retournée par l'API :
|
||||
|
||||
```rust
|
||||
std::option::Option::Some(&std::time::Duration::from_secs(10))
|
||||
```
|
||||
|
||||
Aucun comportement production, réglage, dépendance, policy TLS, pool ou lifecycle n'est modifié.
|
||||
|
||||
## 3. Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2-pre.5.fix.1
|
||||
```
|
||||
|
||||
## 4. Gate opérateur
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
```
|
||||
|
||||
## 5. Suite
|
||||
|
||||
Après gate propre, `pre.006` peut introduire le moteur de migrations PostgreSQL KSP sans rouvrir la fondation connexion/pool/TLS.
|
||||
135
deltas/0.3.2/pre.005.md
Normal file
135
deltas/0.3.2/pre.005.md
Normal file
@@ -0,0 +1,135 @@
|
||||
<!-- file: deltas/0.3.2/pre.005.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.005` — PostgreSQL connection + pool + TLS
|
||||
|
||||
## 1. Base
|
||||
|
||||
Base exacte : `0.3.2-pre.004-fix.001`, incluant l'ajustement opérateur de `.env.example` version `13`.
|
||||
|
||||
Le gate opérateur du 29 août 2026 est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 126 tests Config, ownership, Store avec et sans feature PostgreSQL, compilation `--no-default-features` et Config Desk passent.
|
||||
|
||||
## 2. Objet
|
||||
|
||||
Matérialiser la première ouverture PostgreSQL physique sans ouvrir encore les migrations ou le SQL métier :
|
||||
|
||||
```text
|
||||
tokio-postgres 0.7.18+
|
||||
deadpool-postgres 0.14.x
|
||||
tokio-postgres-rustls 0.14.x
|
||||
rustls 0.23.x / AWS-LC
|
||||
roots système
|
||||
pool borné
|
||||
Store::open physique
|
||||
Store::close borné
|
||||
```
|
||||
|
||||
## 3. Frontière façade/backend
|
||||
|
||||
`ksp-store-lib` reste l'unique surface consumer et ne dépend directement d'aucun driver/pool/TLS. Il convertit ses settings vers un bridge `ksp-store-postgres-lib`, puis mappe les erreurs backend vers des `ErrorCode` Store stables.
|
||||
|
||||
`ksp-store-postgres-lib` possède seul :
|
||||
|
||||
```text
|
||||
parse/normalisation tokio_postgres::Config
|
||||
Deadpool Manager/Pool
|
||||
connector Rustls
|
||||
roots système
|
||||
première acquisition physique
|
||||
fermeture/drain physique
|
||||
```
|
||||
|
||||
Les types physiques `Pool`, `Client`, `Row` et `Statement` ne sont jamais réexportés par la façade.
|
||||
|
||||
## 4. Normalisation de l'URI
|
||||
|
||||
Le backend consomme uniquement l'URI déjà résolue par Config. Après parsing, KSP impose :
|
||||
|
||||
```text
|
||||
application_name = ksp-store
|
||||
connect_timeout = settings typés
|
||||
sslnegotiation = postgres
|
||||
Disabled -> sslmode=disable
|
||||
VerifyFull -> sslmode=require
|
||||
```
|
||||
|
||||
`options=` est rejeté dans cette fondation. `VerifyFull` exige un host TCP afin de disposer d'une identité serveur à vérifier : `hostaddr` seul et les sockets Unix sont rejetés. Les erreurs de parsing ne conservent jamais le texte de l'URI.
|
||||
|
||||
## 5. Pool et lifecycle
|
||||
|
||||
Le pool applique les limites validées en `pre.003` : taille maximale et deadlines `wait/create/recycle`. Le manager utilise `RecyclingMethod::Verified`; la requête de vérification éventuelle reste interne à Deadpool et aucun SQL KSP n'est ajouté.
|
||||
|
||||
La création du pool est lazy, donc `Store::open` exécute explicitement une première `pool.get().await`. Le succès signifie ainsi qu'une connexion physique/auth/TLS a réellement été établie.
|
||||
|
||||
`Store::close(self)` appelle `Pool::close()` puis attend un drain `size == 0` sous `shutdown_timeout`. `Drop` ne fait qu'un close best-effort.
|
||||
|
||||
## 6. TLS
|
||||
|
||||
Modes exacts :
|
||||
|
||||
```text
|
||||
Disabled
|
||||
VerifyFull
|
||||
```
|
||||
|
||||
`VerifyFull` charge les roots système dans `rustls::RootCertStore`, utilise explicitement le provider AWS-LC et conserve la vérification standard de certificat + identité serveur. Aucun mode `Prefer`, aucun verifier permissif et aucun fallback plaintext ne sont introduits.
|
||||
|
||||
## 7. Erreurs sûres
|
||||
|
||||
Le backend retourne uniquement une classification et une phase statique :
|
||||
|
||||
```text
|
||||
ConfigInvalid
|
||||
ConnectFailed
|
||||
PoolTimeout
|
||||
ShutdownTimeout
|
||||
TlsFailed
|
||||
```
|
||||
|
||||
La façade mappe vers :
|
||||
|
||||
```text
|
||||
store.postgres_config_invalid
|
||||
store.postgres_connect_failed
|
||||
store.postgres_pool_timeout
|
||||
store.postgres_tls_failed
|
||||
store.shutdown_timeout
|
||||
```
|
||||
|
||||
Aucune erreur `tokio-postgres`, Deadpool, Rustls ou native-certs n'est attachée comme source publique.
|
||||
|
||||
## 8. Hors scope
|
||||
|
||||
```text
|
||||
migrations/bootstrap SQL
|
||||
ksp_store_schema_migrations
|
||||
health snapshot public
|
||||
RAW transaction/account persistence
|
||||
routing multi-target/multi-réseau dans Store
|
||||
live PostgreSQL integration test
|
||||
```
|
||||
|
||||
Ces éléments restent respectivement aux tranches `pre.006`, `pre.007`, `0.3.3/0.3.4` et `pre.008`.
|
||||
|
||||
## 9. Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2-pre.5
|
||||
```
|
||||
|
||||
## 10. Gate opérateur requis
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||
cargo tree -p ksp-store-lib --edges normal
|
||||
cargo tree --duplicates
|
||||
```
|
||||
143
deltas/0.3.2/pre.006.md
Normal file
143
deltas/0.3.2/pre.006.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<!-- file: deltas/0.3.2/pre.006.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.006` — migration/bootstrap PostgreSQL foundation
|
||||
|
||||
## 1. Base
|
||||
|
||||
Base exacte : `0.3.2-pre.005-fix.001`.
|
||||
|
||||
Le gate opérateur du 29 août 2026 est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-postgres-lib`, tests `ksp-store-lib` avec et sans feature PostgreSQL et compilation `--no-default-features` passent. Les versions physiques réellement résolues observées sont `tokio-postgres 0.7.18`, `deadpool-postgres 0.14.2` et `tokio-postgres-rustls 0.14.0`.
|
||||
|
||||
## 2. Objet
|
||||
|
||||
Matérialiser le moteur privé de migration/bootstrap de la fondation PostgreSQL, sans aucune persistence métier :
|
||||
|
||||
```text
|
||||
ksp_store_schema_migrations uniquement
|
||||
sentinel version 0
|
||||
nom bootstrap immuable
|
||||
SHA-256 du SQL exact
|
||||
advisory transaction lock borné
|
||||
statement timeout transaction-local
|
||||
transaction unique
|
||||
validation metadata/history
|
||||
newer-runtime guard
|
||||
rollback du run courant
|
||||
```
|
||||
|
||||
## 3. Dépendance SHA-256
|
||||
|
||||
`sha2 ^0.11` devient une dépendance workspace explicite et une dépendance directe de `ksp-store-postgres-lib`, car le backend calcule lui-même le checksum persistant.
|
||||
|
||||
Le digest SHA-256 est encodé explicitement en 64 caractères hex minuscules ; aucune dépendance de codec hex supplémentaire n'est ajoutée.
|
||||
|
||||
## 4. Migration bootstrap embarquée
|
||||
|
||||
La seule ressource SQL de production ajoutée est :
|
||||
|
||||
```text
|
||||
crates/ksp-store-postgres-lib/migrations/V000__bootstrap.sql
|
||||
```
|
||||
|
||||
Elle crée uniquement :
|
||||
|
||||
```text
|
||||
ksp_store_schema_migrations
|
||||
```
|
||||
|
||||
avec les champs minimaux :
|
||||
|
||||
```text
|
||||
version BIGINT PRIMARY KEY
|
||||
name TEXT NOT NULL
|
||||
checksum TEXT NOT NULL
|
||||
applied_at TIMESTAMPTZ NOT NULL
|
||||
```
|
||||
|
||||
Aucune table/index RAW, CORE, DECODE ou SPECIALIZED n'est introduit.
|
||||
|
||||
## 5. Algorithme runtime
|
||||
|
||||
Après la première acquisition physique déjà introduite en `pre.005`, le backend conserve ce client dédié et exécute :
|
||||
|
||||
```text
|
||||
begin transaction
|
||||
pg_try_advisory_xact_lock($1) en polling borné
|
||||
set_config('statement_timeout', $1, true)
|
||||
probe metadata
|
||||
si absente et auto_migrate=false -> migration_pending sans DDL
|
||||
si absente et auto_migrate=true -> V000 + sentinel dans la même transaction
|
||||
si présente -> vérification forme minimale + PK version
|
||||
load history ordered
|
||||
missing/divergence -> migration mismatch
|
||||
version > 0 -> schema newer
|
||||
commit unique
|
||||
```
|
||||
|
||||
L'ensemble du bootstrap est aussi borné par `migration_timeout`. Une sortie avant `commit()` droppe la transaction ; le rollback PostgreSQL implicite protège DDL et history du run courant.
|
||||
|
||||
Les colonnes obligatoires de metadata sont vérifiées sans interdire des colonnes supplémentaires futures. Cela permet à un ancien runtime de lire une history plus récente et de produire `schema_newer` plutôt qu'un faux mismatch de forme.
|
||||
|
||||
## 6. Erreurs façade
|
||||
|
||||
Le bridge backend ajoute :
|
||||
|
||||
```text
|
||||
MigrationFailed
|
||||
MigrationMismatch
|
||||
SchemaNewer
|
||||
```
|
||||
|
||||
La façade mappe vers :
|
||||
|
||||
```text
|
||||
store.postgres_migration_failed
|
||||
store.postgres_migration_mismatch
|
||||
store.postgres_schema_newer
|
||||
```
|
||||
|
||||
Aucun texte serveur, SQL, URI ou credential n'est propagé.
|
||||
|
||||
## 7. Tests/canaris
|
||||
|
||||
Les tests unitaires sans serveur couvrent :
|
||||
|
||||
```text
|
||||
checksum exact/stable du V000
|
||||
absence de schéma métier dans le SQL
|
||||
sentinel valide
|
||||
sentinel missing
|
||||
nom/checksum divergent
|
||||
history plus récente
|
||||
bridge backend étendu
|
||||
frontière dépendances + sha2 direct
|
||||
```
|
||||
|
||||
La concurrence réelle, l'idempotence réelle et le rollback injecté restent au test PostgreSQL opt-in de `pre.008`.
|
||||
|
||||
## 8. Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2-pre.6
|
||||
```
|
||||
|
||||
## 9. Gate opérateur
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||
cargo tree -p ksp-store-lib --edges normal
|
||||
```
|
||||
|
||||
## 10. Suite
|
||||
|
||||
Après gate propre, `pre.007` ferme la composition end-to-end et ajoute la projection health/readiness sûre, sans ouvrir encore les tables RAW métier.
|
||||
165
deltas/0.3.2/pre.007.md
Normal file
165
deltas/0.3.2/pre.007.md
Normal file
@@ -0,0 +1,165 @@
|
||||
<!-- file: deltas/0.3.2/pre.007.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.007` — composition runtime et health/readiness portable
|
||||
|
||||
## 1. Base
|
||||
|
||||
Base exacte : `0.3.2-pre.006`.
|
||||
|
||||
Le gate opérateur du 29 août 2026 est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-postgres-lib`, tests `ksp-store-lib` avec et sans feature PostgreSQL, compilation `--no-default-features` et graphes directs passent.
|
||||
|
||||
## 2. Objet
|
||||
|
||||
Fermer la composition runtime de la fondation PostgreSQL sans persistence métier :
|
||||
|
||||
```text
|
||||
StoreSettings
|
||||
-> Store::open
|
||||
-> PostgresBackend
|
||||
-> connexion/pool/TLS
|
||||
-> bootstrap/history
|
||||
-> Store prêt
|
||||
|
||||
Store::runtime_snapshot()
|
||||
-> projection synchrone sans I/O
|
||||
|
||||
Store::health().await
|
||||
-> readiness PostgreSQL légère et bornée
|
||||
-> projection portable/redacted
|
||||
```
|
||||
|
||||
## 3. Surface façade
|
||||
|
||||
`ksp-store-lib` ajoute :
|
||||
|
||||
```text
|
||||
StoreHealthState::{Ready, NotReady}
|
||||
StoreRuntimeSnapshot
|
||||
StoreHealthSnapshot
|
||||
Store::runtime_snapshot()
|
||||
Store::health().await
|
||||
store.postgres_health_failed
|
||||
```
|
||||
|
||||
`StoreRuntimeSnapshot` expose uniquement :
|
||||
|
||||
```text
|
||||
backend kind
|
||||
network logique
|
||||
pool capacity
|
||||
pool size
|
||||
pool available
|
||||
pool waiting
|
||||
```
|
||||
|
||||
`StoreHealthSnapshot` ajoute :
|
||||
|
||||
```text
|
||||
state
|
||||
migration version optionnelle
|
||||
pending migration count
|
||||
last safe ErrorCode optionnel
|
||||
```
|
||||
|
||||
La façade atteint 84 exports crate-root : 60 réexports `ksp-store-api` et 24 éléments runtime Store.
|
||||
|
||||
## 4. Bridge backend
|
||||
|
||||
`ksp-store-postgres-lib` ajoute uniquement les projections safe nécessaires au bridge :
|
||||
|
||||
```text
|
||||
PostgresBackendRuntimeSnapshot
|
||||
PostgresBackendHealthSnapshot
|
||||
```
|
||||
|
||||
Aucun `Pool`, `Client`, `Row`, `Statement`, URI, host, user, database, SQL ou texte serveur n'est exposé par ces snapshots.
|
||||
|
||||
Les compteurs Deadpool `max_size/size/available/waiting` sont saturés en `u32` avant de traverser le bridge.
|
||||
|
||||
## 5. Probe health
|
||||
|
||||
Le probe PostgreSQL reste privé au backend et exécute uniquement :
|
||||
|
||||
```text
|
||||
acquisition Deadpool bornée
|
||||
SELECT 1
|
||||
lecture de MAX(version) dans la metadata privée
|
||||
```
|
||||
|
||||
La deadline globale réutilise le `wait_timeout` configuré du pool ; un fallback interne de 5 secondes n'est utilisé que si ce timeout n'est pas présent dans le pool, état qui ne doit pas être produit par la construction KSP normale.
|
||||
|
||||
Classification :
|
||||
|
||||
```text
|
||||
probe/query/decode failure -> HealthFailed
|
||||
version absente/inférieure -> MigrationMismatch
|
||||
version supérieure -> SchemaNewer
|
||||
version attendue -> Ready
|
||||
```
|
||||
|
||||
La vérification complète nom/checksum reste celle de `Store::open`/bootstrap. Le health est volontairement un diagnostic léger, pas un second moteur de migration.
|
||||
|
||||
## 6. Sémantique readiness
|
||||
|
||||
`Store::open` ne change pas de sémantique : une instance n'est rendue qu'après connexion physique et bootstrap/history validés.
|
||||
|
||||
Le health sert ensuite à observer l'état courant : une panne transitoire produit `NotReady` avec un code KSP sûr, pas une erreur contenant du texte PostgreSQL.
|
||||
|
||||
Une instance reste attachée à exactement un réseau/target Store ; aucun routage ou multiplexage multi-réseaux n'est ajouté.
|
||||
|
||||
## 7. Scope négatif
|
||||
|
||||
Toujours absent de `0.3.2-pre.007` :
|
||||
|
||||
```text
|
||||
RawTransaction PostgreSQL
|
||||
RawAccountState PostgreSQL
|
||||
repository RAW
|
||||
SQL métier
|
||||
CORE/DECODE/SPECIALIZED
|
||||
health HTTP/service global
|
||||
scheduler/polling de health
|
||||
```
|
||||
|
||||
## 8. Tests/canaris
|
||||
|
||||
Les tests sans serveur couvrent :
|
||||
|
||||
```text
|
||||
projection pool safe/bornée
|
||||
Ready vs NotReady
|
||||
migration version/pending safe
|
||||
absence d'URI/SQL/credential dans les snapshots
|
||||
surface façade health crate-root
|
||||
bridge backend health crate-root
|
||||
frontières dépendances et absence de SQL métier
|
||||
```
|
||||
|
||||
La preuve réelle `Ready`, panne/mismatch et close reste au smoke PostgreSQL opt-in de `pre.008`.
|
||||
|
||||
## 9. Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2-pre.7
|
||||
```
|
||||
|
||||
## 10. Gate opérateur
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||
cargo tree -p ksp-store-lib --edges normal
|
||||
```
|
||||
|
||||
## 11. Suite
|
||||
|
||||
Après gate propre, `pre.008` ajoute le test PostgreSQL réel opt-in non destructif couvrant bootstrap initial/idempotent/concurrent, mismatch, rollback/failure, health Ready et close borné.
|
||||
134
deltas/0.3.2/pre.008.md
Normal file
134
deltas/0.3.2/pre.008.md
Normal file
@@ -0,0 +1,134 @@
|
||||
<!-- file: deltas/0.3.2/pre.008.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.008` — PostgreSQL integration réelle opt-in
|
||||
|
||||
## Base exacte
|
||||
|
||||
Cette tranche s'applique sur `0.3.2-pre.007`, dont le gate opérateur du 2026-08-29 est intégralement vert : audits Rust/Markdown, `cargo check --workspace`, Clippy, tests `ksp-store-postgres-lib`, tests `ksp-store-lib` avec et sans feature PostgreSQL, check `--no-default-features` et graphes Cargo.
|
||||
|
||||
## Objet
|
||||
|
||||
Matérialiser le premier test PostgreSQL réel de la fondation `0.3.2` sans ajouter de comportement de production et sans créer de table métier RAW.
|
||||
|
||||
## Changements
|
||||
|
||||
### Test live isolé
|
||||
|
||||
Nouveau fichier :
|
||||
|
||||
```text
|
||||
crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs
|
||||
```
|
||||
|
||||
Le test est `#[ignore]` et :
|
||||
|
||||
```text
|
||||
lit une URI PostgreSQL dédiée uniquement depuis stdin
|
||||
borne l'entrée à 4096 octets
|
||||
ne lit aucune variable d'environnement
|
||||
ne loggue/imprime jamais l'URI
|
||||
refuse PostgreSQL < 15
|
||||
n'imprime que le major PostgreSQL
|
||||
refuse de démarrer si ksp_store_schema_migrations existe déjà
|
||||
```
|
||||
|
||||
Le scénario réel couvre :
|
||||
|
||||
```text
|
||||
connexion PostgreSQL
|
||||
bootstrap initial
|
||||
health Ready version 0 / pending 0
|
||||
close explicite borné
|
||||
réouverture idempotente
|
||||
reset contrôlé de la metadata possédée par le test
|
||||
deux bootstrap concurrents
|
||||
checksum sentinel corrompu -> MigrationMismatch
|
||||
restore checksum -> recovery Ready
|
||||
rollback physique du V000 exact après erreur SQL injectée
|
||||
re-bootstrap après rollback
|
||||
cleanup final de la seule table metadata possédée par le test
|
||||
```
|
||||
|
||||
### Rollback sans hook de production
|
||||
|
||||
Aucun hook `cfg(test)`, feature de test ou API publique supplémentaire n'est ajouté à `ksp-store-postgres-lib`.
|
||||
|
||||
Le test inclut le même `migrations/V000__bootstrap.sql`, l'exécute dans une transaction PostgreSQL de test, ajoute un sentinel transitoire, provoque une erreur SQL contrôlée puis droppe la transaction. `tokio-postgres 0.7.18` rollback implicitement une `Transaction` droppée. Le test prouve ensuite l'absence physique de la metadata avant un nouveau bootstrap normal.
|
||||
|
||||
### Non-destructivité
|
||||
|
||||
Le test ne contient aucun :
|
||||
|
||||
```text
|
||||
DROP DATABASE
|
||||
DROP SCHEMA
|
||||
CREATE DATABASE
|
||||
CREATE SCHEMA
|
||||
DDL RawTransaction
|
||||
DDL RawAccountState
|
||||
SQL CORE/DECODE/SPECIALIZED
|
||||
```
|
||||
|
||||
Les `DROP TABLE` éventuels ciblent uniquement `ksp_store_schema_migrations`, après que le test a prouvé que cette relation n'existait pas avant son démarrage et qu'il en possède donc le cleanup.
|
||||
|
||||
## Documentation de suivi
|
||||
|
||||
Le plan et la matrice validation sont mis à jour pour :
|
||||
|
||||
```text
|
||||
enregistrer le gate opérateur pre.007 vert
|
||||
figer la stratégie live réelle de pre.008
|
||||
remplacer l'idée d'un hook cfg(test) par une injection transactionnelle externe au runtime
|
||||
marquer les critères live comme matérialisés mais non encore exécutés
|
||||
ajouter la commande opérateur stdin sans environnement
|
||||
```
|
||||
|
||||
## Hors scope confirmé
|
||||
|
||||
Aucun changement de production dans :
|
||||
|
||||
```text
|
||||
ksp-store-lib
|
||||
ksp-store-postgres-lib/src
|
||||
ksp-store-api
|
||||
Config
|
||||
migrations committed
|
||||
TLS/pool/runtime
|
||||
```
|
||||
|
||||
Toujours aucun `RawTransaction` ou `RawAccountState` PostgreSQL.
|
||||
|
||||
## Validation locale disponible
|
||||
|
||||
La génération doit exécuter les audits Python et Markdown. Cargo n'est pas disponible dans l'environnement de génération ; le test live doit donc d'abord être compilé par le gate opérateur puis exécuté explicitement sur un PostgreSQL dédié.
|
||||
|
||||
## Gate opérateur déterministe
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
```
|
||||
|
||||
## Gate PostgreSQL réel opt-in
|
||||
|
||||
Sur une database PostgreSQL dédiée dont `ksp_store_schema_migrations` est absente :
|
||||
|
||||
```bash
|
||||
read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo
|
||||
printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_foundation_live -- --ignored --nocapture --test-threads=1
|
||||
unset KSP_PG_TEST_URI
|
||||
```
|
||||
|
||||
La variable shell n'est pas exportée ; le test lit uniquement stdin.
|
||||
|
||||
## Suite
|
||||
|
||||
Si les gates déterministes et live sont verts : `0.3.2-pre.009` — hardening, completeness et dependency matrix.
|
||||
157
deltas/0.3.2/pre.009.md
Normal file
157
deltas/0.3.2/pre.009.md
Normal file
@@ -0,0 +1,157 @@
|
||||
<!-- file: deltas/0.3.2/pre.009.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.009` — hardening, completeness et dependency matrix
|
||||
|
||||
## Base exacte
|
||||
|
||||
Cette tranche s'applique sur `0.3.2-pre.008`.
|
||||
|
||||
Le 29 août 2026, le gate `pre.008` est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-postgres-lib`, façade avec et sans feature PostgreSQL et compilation `--no-default-features` passent. Le gate PostgreSQL réel opt-in passe également sur **PostgreSQL 17** avec bootstrap initial/idempotent/concurrent, mismatch/recovery checksum, rollback transactionnel, health `Ready`, fermeture bornée et cleanup contrôlé.
|
||||
|
||||
## Objet
|
||||
|
||||
Durcir et figer la fondation Store/PostgreSQL avant le gate technique final sans modifier le code de production, les migrations ou les dépendances runtime.
|
||||
|
||||
## Canari façade
|
||||
|
||||
Nouveau fichier :
|
||||
|
||||
```text
|
||||
crates/ksp-store-lib/tests/hardening_completeness.rs
|
||||
```
|
||||
|
||||
Il vérifie :
|
||||
|
||||
```text
|
||||
5 modules privés exacts
|
||||
84 exports crate-root exacts
|
||||
manifest runtime exact
|
||||
feature postgres par défaut exacte
|
||||
backend PostgreSQL toujours optionnel
|
||||
aucune dépendance physique PostgreSQL dans la façade
|
||||
canary secret redacted dans StoreSettings/Debug
|
||||
URI malformed rejetée avant I/O
|
||||
code ConfigInvalid avec postgres / BackendNotCompiled sans feature
|
||||
aucun canary dans Display/Debug Error
|
||||
aucune lecture Config/env/PG*/.pgpass
|
||||
aucun Pool/Client/Row/Statement/SQL physique dans la façade
|
||||
```
|
||||
|
||||
Le même test est conçu pour passer sous le feature set par défaut et sous `--no-default-features`.
|
||||
|
||||
## Canari backend PostgreSQL
|
||||
|
||||
Nouveau fichier :
|
||||
|
||||
```text
|
||||
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
|
||||
```
|
||||
|
||||
Il vérifie :
|
||||
|
||||
```text
|
||||
5 modules privés exacts
|
||||
7 exports bridge exacts
|
||||
9 dépendances physiques exactes du manifest
|
||||
URI malformed rejetée avant I/O
|
||||
URI > 4096 octets rejetée avant I/O
|
||||
hostaddr sans host rejeté sous VerifyFull
|
||||
options= libpq rejeté
|
||||
canary password/URI absent de Debug/error backend
|
||||
PostgresBackendError limité structurellement à kind + phase statique
|
||||
PoolError::Backend(_) abandonne explicitement l'erreur remote
|
||||
aucun rendu d'erreur externe dans runtime/migration/health
|
||||
aucune lecture env/KSP/KSPB/PG*/.pgpass
|
||||
aucun sslrootcert/sslcert/sslkey/.postgresql implicite
|
||||
aucune capability PostgreSQL RawTransaction/RawAccount
|
||||
V000 reste strictement metadata-only
|
||||
```
|
||||
|
||||
Les cas hostiles sont tous forcés avant le premier `.await` physique et n'exigent donc aucun PostgreSQL réel.
|
||||
|
||||
## Non-régression Store API
|
||||
|
||||
`ksp-store-api` n'est pas modifié. Le gate `pre.009` rejoue explicitement ses tests, notamment `tests/external_backend.rs`, afin de confirmer qu'un backend externe peut toujours implémenter les dix capabilities RAW sans dépendre de `ksp-store-lib` ou de PostgreSQL.
|
||||
|
||||
## Documentation de suivi
|
||||
|
||||
Le plan et la matrice validation sont réconciliés avec :
|
||||
|
||||
```text
|
||||
gates pre.006/pre.007 verts
|
||||
pre.008 live vert sur PostgreSQL 17
|
||||
scope exact des canaris pre.009
|
||||
statuts hardening matérialisés mais non encore exécutés
|
||||
commande de dependency matrix ciblée
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-store-lib/tests/hardening_completeness.rs
|
||||
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
|
||||
deltas/0.3.2/pre.009.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
|
||||
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2-pre.9
|
||||
```
|
||||
|
||||
Aucun `src/**`, migration, Config, README/USAGE, CHANGELOG ou ROADMAP n'est modifié dans cette tranche.
|
||||
|
||||
## Validations exécutées dans l'environnement de génération
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
contrôle statique de la version et du payload
|
||||
contrôle statique des exports/manifests attendus
|
||||
contrôle des chaînes interdites no-env/no-business
|
||||
```
|
||||
|
||||
Cargo/rustc/rustfmt ne sont pas disponibles dans l'environnement de génération.
|
||||
|
||||
## Gate opérateur
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-api
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo test -p ksp-config-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
cargo tree -p ksp-store-lib --edges normal
|
||||
cargo tree -p ksp-store-lib -e features
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||
cargo tree --duplicates
|
||||
```
|
||||
|
||||
Le smoke PostgreSQL réel n'a pas besoin d'être rejoué dans `pre.009` si aucun code runtime/migration n'a changé ; il sera rejoué au gate technique final `pre.010`.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question fonctionnelle nouvelle. Tout échec des nouveaux canaris doit produire un correctif `pre.009-fix.NNN` avant `pre.010`.
|
||||
|
||||
## Suite
|
||||
|
||||
Après gate vert : `0.3.2-pre.010` — gate technique final sans nouveau développement fonctionnel.
|
||||
125
deltas/0.3.2/pre.010.md
Normal file
125
deltas/0.3.2/pre.010.md
Normal file
@@ -0,0 +1,125 @@
|
||||
<!-- file: deltas/0.3.2/pre.010.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.010` — gate technique final Store/PostgreSQL
|
||||
|
||||
## Base exacte
|
||||
|
||||
Cette tranche s'applique sur `0.3.2-pre.009`.
|
||||
|
||||
Le gate opérateur `pre.009` du 29 août 2026 est entièrement vert : audits Rust/Markdown, workspace check/Clippy, `ksp-store-api`, backend PostgreSQL, façade avec et sans feature PostgreSQL, Config, compilation `--no-default-features`, graphes Cargo et `cargo test --workspace` passent.
|
||||
|
||||
Le smoke PostgreSQL réel de `pre.008` avait déjà passé sur PostgreSQL 17 ; `pre.010` le rejoue pour la preuve finale.
|
||||
|
||||
## Objectif
|
||||
|
||||
Ouvrir exclusivement le gate technique final de `0.3.2` sans nouveau développement fonctionnel.
|
||||
|
||||
Cette tranche ne modifie aucun :
|
||||
|
||||
```text
|
||||
src/**
|
||||
test fonctionnel
|
||||
migration SQL
|
||||
Config runtime
|
||||
manifest de crate
|
||||
dépendance runtime
|
||||
README/USAGE
|
||||
CHANGELOG/ROADMAP
|
||||
prompt suivant
|
||||
```
|
||||
|
||||
## Gate final attendu
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-api
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo test -p ksp-config-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
cargo test --workspace
|
||||
cargo tree -p ksp-store-lib --edges normal
|
||||
cargo tree -p ksp-store-lib -e features
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||
cargo tree --duplicates
|
||||
```
|
||||
|
||||
Le gate PostgreSQL réel est ensuite rejoué sur une database dédiée où `ksp_store_schema_migrations` est absente avant démarrage :
|
||||
|
||||
```bash
|
||||
read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo
|
||||
printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_foundation_live -- --ignored --nocapture --test-threads=1
|
||||
unset KSP_PG_TEST_URI
|
||||
```
|
||||
|
||||
Comme `pre.004` a réellement ajouté les resources `std.store` aux trois applications desktop, le gate technique final rejoue aussi :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-config-desk && cargo tauri build)
|
||||
(cd crates/ksp-app-solprices-desk && cargo tauri build)
|
||||
(cd crates/ksp-app-wallet-desk && cargo tauri build)
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.3.2/pre.010.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
|
||||
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2-pre.10
|
||||
```
|
||||
|
||||
## Décisions
|
||||
|
||||
```text
|
||||
pre.010 = preuve technique finale uniquement
|
||||
aucun développement fonctionnel nouveau
|
||||
PostgreSQL live doit être rejoué
|
||||
cargo test --workspace est obligatoire
|
||||
les trois builds Tauri sont rejoués car le packaging std.store a changé en pre.004
|
||||
un échec ouvre pre.010-fix.NNN
|
||||
pre.011 reste la réconciliation documentaire finale
|
||||
```
|
||||
|
||||
## Validations exécutées dans l'environnement de génération
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
contrôle statique version/payload
|
||||
```
|
||||
|
||||
Cargo/rustc/rustfmt ne sont pas disponibles dans l'environnement de génération ; aucun gate Cargo n'est déclaré PASS ici.
|
||||
|
||||
## Validations non exécutées
|
||||
|
||||
Toutes les commandes Cargo et le smoke PostgreSQL réel restent à exécuter par l'opérateur après application de l'overlay.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune. Tout défaut découvert par le gate final doit être corrigé par `pre.010-fix.NNN` avant `pre.011`.
|
||||
|
||||
## Suite
|
||||
|
||||
Après gate final entièrement vert : `0.3.2-pre.011` — réconciliation documentaire finale, README/USAGE des deux crates Store et fermeture de la matrice de validation.
|
||||
268
deltas/0.3.2/pre.011.md
Normal file
268
deltas/0.3.2/pre.011.md
Normal file
@@ -0,0 +1,268 @@
|
||||
<!-- file: deltas/0.3.2/pre.011.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.011` — réconciliation documentaire finale Store/PostgreSQL
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base directe attendue :
|
||||
|
||||
```text
|
||||
0.3.2-pre.010
|
||||
```
|
||||
|
||||
Commit attendu :
|
||||
|
||||
```text
|
||||
v0.3.2-pre.011
|
||||
```
|
||||
|
||||
Archive overlay attendue :
|
||||
|
||||
```text
|
||||
ksp-general-0.3.2-pre.011.zip
|
||||
```
|
||||
|
||||
Cette tranche suit le gate technique final `pre.010` et n'ajoute aucun comportement runtime.
|
||||
|
||||
## 2. Résultat du gate technique `pre.010`
|
||||
|
||||
Le gate opérateur `0.3.2-pre.010` a été rejoué après `cargo clean` et fournit les preuves suivantes :
|
||||
|
||||
```text
|
||||
audits Rust PASS
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
tests ciblés de toutes les crates PASS
|
||||
ksp-store-lib default + --no-default-features PASS
|
||||
cargo test --workspace PASS
|
||||
graphes Cargo normal/features/duplicates exécutés
|
||||
3 builds Tauri Linux PASS
|
||||
PostgreSQL live foundation PASS — major 17
|
||||
```
|
||||
|
||||
La ligne d'audit Markdown du journal opérateur `pre.010` a utilisé par erreur `deltas/0.3.1` au lieu de `deltas/0.3.2`. L'overlay `pre.010` avait été audité dans l'environnement de génération avec le chemin correct ; `pre.011` rejoue explicitement le scope complet `deltas/0.3.2`. Cette erreur de scope de commande ne constitue pas un défaut runtime et n'ouvre pas un `pre.010-fix`.
|
||||
|
||||
## 3. Objectif
|
||||
|
||||
Appliquer `VER-LIFECYCLE-006` et figer les documents durables de la fondation Store/PostgreSQL avant la préparation de publication minimale :
|
||||
|
||||
```text
|
||||
README/USAGE ksp-store-lib
|
||||
README/USAGE ksp-store-postgres-lib
|
||||
README/USAGE ksp-config-lib pour std.store
|
||||
index docs global
|
||||
index plans
|
||||
index validations
|
||||
plan 023
|
||||
validation 019
|
||||
```
|
||||
|
||||
Cette tranche ne finalise pas `CHANGELOG.md`, `ROADMAP.md` ni le prompt `0.3.3`.
|
||||
|
||||
## 4. Documentation de `ksp-store-lib`
|
||||
|
||||
Le nouveau README fixe durablement :
|
||||
|
||||
```text
|
||||
façade runtime backend-neutral
|
||||
feature postgres par défaut
|
||||
1 Store = 1 RawNetworkId + 1 backend physique
|
||||
aucun multiplexage automatique de bases/réseaux
|
||||
Config propriétaire des targets et secrets
|
||||
aucun type PostgreSQL physique réexporté
|
||||
aucune capability RAW PostgreSQL encore implémentée
|
||||
```
|
||||
|
||||
Le nouveau USAGE documente :
|
||||
|
||||
```text
|
||||
dépendance consumer normale uniquement vers ksp-store-lib
|
||||
construction programmatique de StoreSettings
|
||||
Store::open / runtime_snapshot / health / close
|
||||
résolution recommandée via ksp-config-lib
|
||||
settings pool/bootstrap/TLS
|
||||
surface health sûre
|
||||
limite fonctionnelle de la fondation
|
||||
```
|
||||
|
||||
## 5. Documentation de `ksp-store-postgres-lib`
|
||||
|
||||
Le README et le USAGE fixent la crate comme backend physique, non comme seconde façade :
|
||||
|
||||
```text
|
||||
consumer ordinaire -X-> ksp-store-postgres-lib
|
||||
bridge façade/tests seulement
|
||||
pool Deadpool borné
|
||||
TLS Disabled / VerifyFull via Rustls
|
||||
roots système + AWS-LC
|
||||
moteur de migrations KSP privé
|
||||
V000 bootstrap + ksp_store_schema_migrations
|
||||
history version/name/SHA-256 + advisory transaction lock
|
||||
health/runtime snapshots sûrs
|
||||
PostgresBackendError = kind + phase statique
|
||||
aucune table/repository/capability RAW métier
|
||||
```
|
||||
|
||||
La politique durable reste PostgreSQL >= 15 pour la validation de fondation ; le major effectivement exercé reste enregistré dans la matrice et non figé comme dépendance runtime maximale.
|
||||
|
||||
## 6. Réconciliation Config
|
||||
|
||||
`ksp-config-lib/README.md` et `USAGE.md` documentent désormais :
|
||||
|
||||
```text
|
||||
cfg.std.store / schema.std.store
|
||||
Config -> Store avec default-features = false
|
||||
sélection de target nommée
|
||||
RawNetworkId explicite
|
||||
provenance Secret obligatoire de l'URI PostgreSQL
|
||||
variables KSP_SECRET_STORE_DEVNET_POSTGRES_URI
|
||||
KSP_SECRET_STORE_MAINNET_POSTGRES_URI
|
||||
KSP_SECRET_STORE_TESTNET_POSTGRES_URI
|
||||
targets committed devnet/mainnet/testnet
|
||||
bases PostgreSQL indépendantes par target
|
||||
absence de routage multi-target dans Store
|
||||
```
|
||||
|
||||
Aucune lecture d'environnement n'est déplacée vers Store/backend.
|
||||
|
||||
## 7. Indexes et documents de release
|
||||
|
||||
Les indexes durables référencent maintenant le plan `023` et la validation `019` ainsi que la documentation des deux crates runtime.
|
||||
|
||||
Le plan `023` enregistre le résultat réel de `pre.010`, la correction de scope Markdown à rejouer et la matérialisation documentaire de `pre.011`.
|
||||
|
||||
La matrice `019` est réconciliée avec les preuves réelles de `pre.002` à `pre.010`, y compris :
|
||||
|
||||
```text
|
||||
ksp-store-api non régressé
|
||||
pool/TLS/bootstrap/health/close
|
||||
hardening/security
|
||||
feature mismatch
|
||||
no-env
|
||||
PostgreSQL live major 17 en pre.008 et pre.010
|
||||
3 builds Tauri au gate final
|
||||
aucun TODO technique applicable à 0.3.2
|
||||
```
|
||||
|
||||
Le seul statut restant avant fermeture de `pre.011` est son propre gate documentaire opérateur.
|
||||
|
||||
## 8. Documents relus et volontairement inchangés
|
||||
|
||||
Les architectures suivantes ont été relues sur la surface finale `pre.010` :
|
||||
|
||||
```text
|
||||
docs/architecture/003-COMPONENT_CONTRACTS.md
|
||||
docs/architecture/004-COMPONENT_INVENTORY.md
|
||||
docs/architecture/005-DEPENDENCY_GRAPH.md
|
||||
docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md
|
||||
```
|
||||
|
||||
Elles sont déjà cohérentes avec le split façade/backend et ne sont pas modifiées artificiellement.
|
||||
|
||||
Le choix documentaire de `0.3.1-pre.009` de ne pas créer un README/USAGE propre à `ksp-store-api` n'est pas rouvert par cette release runtime/backend.
|
||||
|
||||
## 9. Hors scope préservé
|
||||
|
||||
Aucun élément suivant n'est introduit :
|
||||
|
||||
```text
|
||||
RawTransaction PostgreSQL persistence/query/retention
|
||||
RawAccountState PostgreSQL persistence/query/retention
|
||||
SQL métier RAW
|
||||
repository métier
|
||||
batch/backlog/priority worker/job
|
||||
Store Desk
|
||||
nouvelle migration
|
||||
nouvelle dépendance
|
||||
nouveau test fonctionnel
|
||||
modification src/**
|
||||
```
|
||||
|
||||
## 10. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-store-lib/README.md
|
||||
crates/ksp-store-lib/USAGE.md
|
||||
crates/ksp-store-postgres-lib/README.md
|
||||
crates/ksp-store-postgres-lib/USAGE.md
|
||||
deltas/0.3.2/pre.011.md
|
||||
```
|
||||
|
||||
## 11. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-config-lib/README.md
|
||||
crates/ksp-config-lib/USAGE.md
|
||||
docs/000-README.md
|
||||
docs/plans/000-README.md
|
||||
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
|
||||
docs/validation/000-README.md
|
||||
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
|
||||
```
|
||||
|
||||
## 12. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 13. Version Cargo
|
||||
|
||||
La prerelease non-fix synchronise :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2-pre.11
|
||||
```
|
||||
|
||||
Aucune dépendance, feature ou configuration Cargo n'est modifiée en dehors de cette version.
|
||||
|
||||
## 14. Validation de génération
|
||||
|
||||
L'environnement de génération ne fournit pas `cargo`/`rustc`; aucun résultat Cargo local n'est donc déclaré PASS par ce delta.
|
||||
|
||||
Les audits Python applicables ont été rejoués après matérialisation de l'overlay, avec le chemin Markdown exact `deltas/0.3.2` :
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
Markdown table audit: clean (186 table(s), 142 file(s))
|
||||
```
|
||||
|
||||
Cette preuve corrige explicitement le défaut de scope de la commande Markdown observé dans le journal opérateur `pre.010`.
|
||||
|
||||
## 15. Gate opérateur après application
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
|
||||
cargo test -p ksp-store-api
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-config-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
```
|
||||
|
||||
Le smoke PostgreSQL réel, les graphes Cargo complets, `cargo test --workspace` et les trois builds Tauri ne sont pas rejoués par nécessité dans cette tranche documentaire : ils sont déjà le gate technique vert de `pre.010` et `pre.011` ne modifie aucun code, dépendance, config runtime, resource Tauri ou migration.
|
||||
|
||||
## 16. Suite
|
||||
|
||||
Si le gate documentaire `pre.011` est vert, la seule prerelease restante est `pre.012`, strictement limitée à la préparation de publication :
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
prompts/022-V0_3_3_START_PROMPT.md
|
||||
deltas/0.3.2/pre.012.md
|
||||
```
|
||||
|
||||
Aucun README, USAGE, plan, validation, architecture, code, test, schema ou config ne devra être rouvert dans `pre.012`.
|
||||
320
deltas/0.3.2/pre.012.md
Normal file
320
deltas/0.3.2/pre.012.md
Normal file
@@ -0,0 +1,320 @@
|
||||
<!-- file: deltas/0.3.2/pre.012.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-pre.012` — préparation de publication et prompt `0.3.3`
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base directe attendue :
|
||||
|
||||
```text
|
||||
0.3.2-pre.011
|
||||
workspace.package.version = 0.3.2-pre.11
|
||||
```
|
||||
|
||||
Le gate opérateur de `pre.011`, exécuté le **30 août 2026**, est entièrement vert :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
python3 scripts/audit_rust_workspace_rules.py PASS / clean
|
||||
python3 scripts/audit_markdown_tables.py PASS / clean — 186 tables / 142 fichiers
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test -p ksp-store-api PASS
|
||||
cargo test -p ksp-store-lib PASS
|
||||
cargo test -p ksp-store-lib --no-default-features PASS
|
||||
cargo test -p ksp-store-postgres-lib PASS
|
||||
cargo test -p ksp-config-lib PASS — 126 unit tests + ownership/public API
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
```
|
||||
|
||||
Le gate technique complet de référence de `pre.010` reste également acquis : il a été rejoué après `cargo clean`, passe `cargo test --workspace`, les graphes Cargo, les trois builds Tauri Linux et le live PostgreSQL sur **major 17**.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Dernière prerelease avant `rel.001`, strictement limitée à la préparation de publication minimale définie par `VER-LIFECYCLE-003` :
|
||||
|
||||
```text
|
||||
bump workspace vers 0.3.2-pre.12
|
||||
entrée stable candidate 0.3.2 dans CHANGELOG.md
|
||||
fermeture 0.3.2 et précision de 0.3.3 dans ROADMAP.md
|
||||
création du prompt 0.3.3
|
||||
delta pre.012
|
||||
```
|
||||
|
||||
Aucun code, test, manifest de crate, README/USAGE, plan, validation, architecture, règle, config, schema ou migration n'est rouvert.
|
||||
|
||||
## 3. Version Cargo
|
||||
|
||||
```text
|
||||
0.3.2-pre.11
|
||||
-> 0.3.2-pre.12
|
||||
```
|
||||
|
||||
Le header `Cargo.toml` passe de la version fichier `345` à `346` parce que le fichier est réellement modifié.
|
||||
|
||||
## 4. Changelog `0.3.2`
|
||||
|
||||
`CHANGELOG.md` enregistre la fondation stable candidate réellement livrée :
|
||||
|
||||
```text
|
||||
ksp-store-lib + ksp-store-postgres-lib
|
||||
feature postgres par défaut
|
||||
Store lié à exactement un RawNetworkId
|
||||
Config std.store avec targets devnet/mainnet/testnet et URI Secret séparées
|
||||
tokio-postgres + Deadpool + Rustls
|
||||
TLS Disabled / VerifyFull
|
||||
pool/connect/wait/create/recycle/shutdown bornés
|
||||
V000 bootstrap + ksp_store_schema_migrations
|
||||
history version/name/SHA-256
|
||||
advisory transaction lock
|
||||
health/readiness portable et redacted
|
||||
84 exports crate-root façade au gate hardening
|
||||
aucune capability/table RawTransaction ou RawAccountState métier
|
||||
```
|
||||
|
||||
Il enregistre également le gate technique complet, le live PostgreSQL major 17 et la policy supportée PostgreSQL >= 15.
|
||||
|
||||
## 5. Roadmap
|
||||
|
||||
`ROADMAP.md` :
|
||||
|
||||
```text
|
||||
passe 0.3.2 à [X]
|
||||
remplace les formulations prospectives par la fondation réellement livrée
|
||||
conserve 0.3.3 ouverte comme vertical slice RawTransaction complète
|
||||
précise que 0.3.3 couvre les six capabilities transaction/observation/rétention
|
||||
conserve RawAccountState + complétude RAW pour 0.3.4
|
||||
```
|
||||
|
||||
La séparation durable reste :
|
||||
|
||||
```text
|
||||
Store = persistence/navigation
|
||||
workers/jobs/executors = batch-size, priorité, backlog et policy
|
||||
```
|
||||
|
||||
## 6. Prompt `0.3.3`
|
||||
|
||||
Le nouveau prompt :
|
||||
|
||||
```text
|
||||
prompts/022-V0_3_3_START_PROMPT.md
|
||||
```
|
||||
|
||||
est confronté à :
|
||||
|
||||
```text
|
||||
docs/rules/PROMPT_STRUCTURE.md
|
||||
docs/rules/VERSION_WORKFLOW.md
|
||||
docs/rules/FILE_CONTRACTS.md
|
||||
docs/rules/RULES_KSP.md
|
||||
docs/rules/RULES_DEPENDENCIES.md
|
||||
|
||||
docs/architecture/003-COMPONENT_CONTRACTS.md
|
||||
docs/architecture/004-COMPONENT_INVENTORY.md
|
||||
docs/architecture/005-DEPENDENCY_GRAPH.md
|
||||
docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md
|
||||
docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md
|
||||
|
||||
docs/plans/022-V0_3_1_STORE_RAW_PLAN.md
|
||||
docs/validation/018-V0_3_1_STORE_RAW.md
|
||||
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
|
||||
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
|
||||
|
||||
crates/ksp-store-api/src/**
|
||||
crates/ksp-store-lib/src/**
|
||||
crates/ksp-store-postgres-lib/src/**
|
||||
```
|
||||
|
||||
Il impose une première tranche `pre.001` d'audit/design avant tout SQL métier lourd.
|
||||
|
||||
## 7. Archive historique requise pour la session suivante
|
||||
|
||||
L'archive :
|
||||
|
||||
```text
|
||||
khadhroony-bot3_v0.5.3-pre.005-fix010.zip
|
||||
```
|
||||
|
||||
reste **obligatoire au `0.3.3-pre.001`**, mais sa portée est réduite à l'ancien RAW transaction PostgreSQL :
|
||||
|
||||
```text
|
||||
raw transaction table
|
||||
transaction observations
|
||||
indexes/constraints
|
||||
raw queries
|
||||
raw transaction repository
|
||||
contrats DTO/entity/repository pertinents
|
||||
```
|
||||
|
||||
Elle sert à produire une matrice :
|
||||
|
||||
```text
|
||||
REPRENDRE / REDESSINER / REPORTER / REJETER
|
||||
```
|
||||
|
||||
Elle n'est ni une base de code ni une autorité sur `ksp-store-api` ou l'architecture KSP actuelle.
|
||||
|
||||
## 8. Scope strict du prompt suivant
|
||||
|
||||
`0.3.3` doit implémenter les six capabilities :
|
||||
|
||||
```text
|
||||
RawTransactionRead
|
||||
RawTransactionWrite
|
||||
RawTransactionObservationRead
|
||||
RawTransactionObservationWrite
|
||||
RawTransactionRetentionRead
|
||||
RawTransactionRetentionWrite
|
||||
```
|
||||
|
||||
sur le même couple Store/PostgreSQL, avec notamment :
|
||||
|
||||
```text
|
||||
migration métier à partir de V001
|
||||
atomicité canonical + observation
|
||||
idempotence identical
|
||||
ERROR_CODE_RAW_CONFLICT pour contenu divergent
|
||||
get/list cursorisé
|
||||
network mismatch avant I/O
|
||||
retention/tombstone/ForceRehydrate
|
||||
concurrence/rollback
|
||||
PostgreSQL live gate
|
||||
```
|
||||
|
||||
Le prompt force `pre.001` à fermer plusieurs questions physiques importantes avant code lourd :
|
||||
|
||||
```text
|
||||
binding de la database au RawNetworkId
|
||||
représentation PostgreSQL des u64/u32 sans narrowing
|
||||
ordre total + format du cursor
|
||||
concurrence/idempotence
|
||||
rétention physique honnête pour Full/Compacted/Archived/Purged
|
||||
error mapping SQL sûr
|
||||
```
|
||||
|
||||
Sont explicitement réservés :
|
||||
|
||||
```text
|
||||
0.3.4 -> RawAccountState PostgreSQL + complétude RAW
|
||||
0.3.5 -> Interface events partagés si consumer réel
|
||||
0.3.6 -> Job API + premier backfill
|
||||
0.3.7 -> application backfill/inspection
|
||||
```
|
||||
|
||||
## 9. Prévision souple intégrée au prompt
|
||||
|
||||
Le prompt réserve :
|
||||
|
||||
```text
|
||||
pre.001 audit contrats/kbot3/schema/concurrence/sizing
|
||||
pre.002 migration V001 + schema/indexes
|
||||
pre.003 mapping physique + lectures
|
||||
pre.004 persistence acquisition atomique + observations
|
||||
pre.005 list/query + cursor
|
||||
pre.006 retention/tombstone/ForceRehydrate
|
||||
pre.007 composition façade + conformance six capabilities
|
||||
pre.008 PostgreSQL integration réelle RawTransaction
|
||||
pre.009 hardening/completeness
|
||||
pre.010 gate technique final
|
||||
pre.011 réconciliation documentaire
|
||||
pre.012 préparation de publication
|
||||
rel.001 publication stable
|
||||
```
|
||||
|
||||
La prévision reste souple et doit être recalibrée par `0.3.3-pre.001` selon le budget de 15–20 minutes par tranche intermédiaire.
|
||||
|
||||
## 10. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
prompts/022-V0_3_3_START_PROMPT.md
|
||||
deltas/0.3.2/pre.012.md
|
||||
```
|
||||
|
||||
## 11. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
```
|
||||
|
||||
## 12. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 13. Surfaces explicitement non rouvertes
|
||||
|
||||
```text
|
||||
README.md
|
||||
RULES.md
|
||||
.env.example
|
||||
config/**
|
||||
crates/**
|
||||
docs/**
|
||||
prompts/001..021
|
||||
```
|
||||
|
||||
En particulier :
|
||||
|
||||
```text
|
||||
crates/ksp-store-api/**
|
||||
crates/ksp-store-lib/**
|
||||
crates/ksp-store-postgres-lib/**
|
||||
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
|
||||
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
|
||||
docs/architecture/**
|
||||
```
|
||||
|
||||
## 14. Validations de préparation
|
||||
|
||||
À exécuter sur l'arbre reconstruit :
|
||||
|
||||
```text
|
||||
audit Rust workspace
|
||||
audit Markdown
|
||||
contrôle exact du payload overlay
|
||||
contrôle versions/file headers
|
||||
contrôle qu'aucun fichier hors lane n'est modifié
|
||||
```
|
||||
|
||||
Aucun résultat Cargo nouveau n'est revendiqué par l'environnement de génération si Cargo n'y est pas disponible.
|
||||
|
||||
## 15. Gate opérateur
|
||||
|
||||
La lane est documentaire/de publication ; le gate ciblé suffit :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-api
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-config-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
```
|
||||
|
||||
Le live PostgreSQL, les trois builds Tauri et le workspace lourd n'ont pas besoin d'être rejoués dans cette tranche puisque `pre.012` ne touche aucune surface runtime/build/config/dependency.
|
||||
|
||||
## 16. Étape suivante
|
||||
|
||||
Si le gate reste vert :
|
||||
|
||||
```text
|
||||
0.3.2-rel.001
|
||||
```
|
||||
|
||||
La publication stable doit être strictement mécanique :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2
|
||||
deltas/0.3.2/rel.001.md
|
||||
```
|
||||
|
||||
`rel.001` ne doit rouvrir ni `CHANGELOG.md`, ni `ROADMAP.md`, ni le prompt `0.3.3`, ni aucun autre document/code.
|
||||
329
deltas/0.3.2/rel.001.md
Normal file
329
deltas/0.3.2/rel.001.md
Normal file
@@ -0,0 +1,329 @@
|
||||
<!-- file: deltas/0.3.2/rel.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.2-rel.001` — publication stable Store/PostgreSQL foundation
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base directe attendue :
|
||||
|
||||
```text
|
||||
0.3.2-pre.012
|
||||
workspace.package.version = 0.3.2-pre.12
|
||||
```
|
||||
|
||||
Commit attendu pour cette livraison :
|
||||
|
||||
```text
|
||||
v0.3.2-rel.001
|
||||
```
|
||||
|
||||
Tag stable attendu après validation :
|
||||
|
||||
```text
|
||||
v0.3.2
|
||||
```
|
||||
|
||||
## 2. Responsabilité de `rel.001`
|
||||
|
||||
Cette livraison effectue uniquement la mécanique de publication stable définie par `VER-LIFECYCLE-012`.
|
||||
|
||||
Elle :
|
||||
|
||||
- passe `workspace.package.version` de `0.3.2-pre.12` à `0.3.2` ;
|
||||
- ajoute le présent delta `rel.001` ;
|
||||
- ne corrige ni code, ni test, ni manifest de crate, ni Config, ni migration, ni README/USAGE, ni plan/validation, ni architecture/règle, ni prompt, ni `CHANGELOG.md`, ni `ROADMAP.md`.
|
||||
|
||||
Tout défaut appartenant à un couloir antérieur doit renvoyer vers une prerelease appropriée ; `rel.001` n'est jamais une tranche de rattrapage.
|
||||
|
||||
## 3. Preuves acquises avant publication
|
||||
|
||||
Le gate technique final `0.3.2-pre.010`, exécuté le **29 août 2026** après `cargo clean`, a reconstruit et validé la workspace depuis zéro.
|
||||
|
||||
Il a notamment validé :
|
||||
|
||||
```text
|
||||
cargo clean PASS
|
||||
cargo fmt --all PASS
|
||||
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||
General Rust rule audit clean
|
||||
Rust export completeness audit 0 candidate(s)
|
||||
KSP workspace Rust rule audit clean
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test --workspace PASS
|
||||
cargo tree -p ksp-store-lib --edges normal inspecté
|
||||
cargo tree -p ksp-store-lib -e features inspecté
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal inspecté
|
||||
cargo tree --duplicates inspecté
|
||||
cargo tauri build — SOL Prices Desk PASS
|
||||
cargo tauri build — Wallet Desk PASS
|
||||
cargo tauri build — Config Desk PASS
|
||||
```
|
||||
|
||||
Le même gate a rejoué le test PostgreSQL réel opt-in sur un serveur **PostgreSQL major 17** :
|
||||
|
||||
```text
|
||||
postgres_foundation_live PASS
|
||||
bootstrap initial PASS
|
||||
réouverture idempotente PASS
|
||||
bootstrap concurrent sous advisory lock PASS
|
||||
health Ready PASS
|
||||
mismatch checksum PASS
|
||||
recovery après restauration PASS
|
||||
rollback transactionnel injecté PASS
|
||||
close borné PASS
|
||||
```
|
||||
|
||||
Le gate `0.3.2-pre.011`, exécuté le **30 août 2026**, a validé la réconciliation documentaire finale et le bon scope Markdown `deltas/0.3.2` :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||
General Rust rule audit clean
|
||||
Rust export completeness audit 0 candidate(s)
|
||||
KSP workspace Rust rule audit clean
|
||||
python3 scripts/audit_markdown_tables.py PASS — 186 tables / 142 fichiers
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test -p ksp-store-api PASS
|
||||
cargo test -p ksp-store-lib PASS
|
||||
cargo test -p ksp-store-lib --no-default-features PASS
|
||||
cargo test -p ksp-store-postgres-lib PASS
|
||||
cargo test -p ksp-config-lib PASS
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
```
|
||||
|
||||
Enfin, le gate `0.3.2-pre.012`, exécuté le **30 août 2026**, a validé la préparation minimale de publication :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||
General Rust rule audit clean
|
||||
Rust export completeness audit 0 candidate(s)
|
||||
KSP workspace Rust rule audit clean
|
||||
python3 scripts/audit_markdown_tables.py PASS — 186 tables / 144 fichiers
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test -p ksp-store-api PASS
|
||||
cargo test -p ksp-store-lib PASS
|
||||
cargo test -p ksp-store-postgres-lib PASS
|
||||
cargo test -p ksp-config-lib PASS
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
```
|
||||
|
||||
`pre.011` et `pre.012` n'ont rouvert aucun code runtime, aucune migration ni aucune dépendance Store/PostgreSQL.
|
||||
|
||||
## 4. Version stable publiée
|
||||
|
||||
La version Cargo devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.2
|
||||
```
|
||||
|
||||
La release stable publiée est :
|
||||
|
||||
```text
|
||||
0.3.2 — Store/PostgreSQL runtime foundation
|
||||
```
|
||||
|
||||
La surface stable comprend notamment :
|
||||
|
||||
```text
|
||||
ksp-store-lib
|
||||
ksp-store-postgres-lib
|
||||
feature postgres par défaut dans la façade
|
||||
mode --no-default-features sans backend PostgreSQL
|
||||
Config std.store avec targets nommés devnet/mainnet/testnet
|
||||
1 Store = 1 RawNetworkId + 1 backend physique
|
||||
tokio-postgres + deadpool-postgres
|
||||
TLS Disabled / VerifyFull via Rustls
|
||||
pool borné et timeouts connect/wait/create/recycle/shutdown
|
||||
Store::open / Store::close
|
||||
runtime_snapshot sans I/O
|
||||
health/readiness portable et redacted
|
||||
moteur privé de migrations KSP
|
||||
V000 bootstrap metadata-only
|
||||
ksp_store_schema_migrations
|
||||
checksum SHA-256
|
||||
advisory lock transactionnel borné
|
||||
bootstrap idempotent et concurrent-safe
|
||||
refus du schema plus récent / historique divergent
|
||||
URI et erreurs PostgreSQL non exposées
|
||||
PostgreSQL >= 15, validé réellement sur major 17
|
||||
```
|
||||
|
||||
Restent volontairement absents de `0.3.2` :
|
||||
|
||||
```text
|
||||
persistence PostgreSQL RawTransaction
|
||||
persistence PostgreSQL RawAccountState
|
||||
repositories SQL métier RAW
|
||||
queries/cursors PostgreSQL métier
|
||||
retention/tombstone/ForceRehydrate PostgreSQL métier
|
||||
CORE / DECODE / SPECIALIZED
|
||||
worker / job / executor
|
||||
Store Desk
|
||||
```
|
||||
|
||||
La frontière stable reste :
|
||||
|
||||
```text
|
||||
ksp-store-api
|
||||
= contrats RAW backend-agnostic
|
||||
|
||||
ksp-store-lib
|
||||
= façade/runtime commun + sélection backend par feature
|
||||
|
||||
ksp-store-postgres-lib
|
||||
= driver/pool/TLS/SQL/migrations/health PostgreSQL privés
|
||||
|
||||
ksp-config-lib
|
||||
= résolution std.store + secrets/env + target réseau/backend
|
||||
```
|
||||
|
||||
`ksp-store-postgres-lib` ne dépend jamais de `ksp-store-lib`, et les consommateurs ordinaires dépendent uniquement de la façade.
|
||||
|
||||
## 5. Trajectoire suivante confirmée
|
||||
|
||||
La suite reste :
|
||||
|
||||
```text
|
||||
0.3.3 vertical slice PostgreSQL RawTransaction complète
|
||||
0.3.4 vertical slice PostgreSQL RawAccountState + complétude Store RAW
|
||||
0.3.5 Interface events partagés réellement nécessaires
|
||||
0.3.6 Job API + premier backfill RAW
|
||||
0.3.7 application backfill/inspection RAW
|
||||
```
|
||||
|
||||
Le prompt actif de la prochaine release est :
|
||||
|
||||
```text
|
||||
prompts/022-V0_3_3_START_PROMPT.md
|
||||
```
|
||||
|
||||
`0.3.3-pre.001` doit commencer par audit/brainstorming/sizing avant SQL lourd et exige l'archive historique :
|
||||
|
||||
```text
|
||||
khadhroony-bot3_v0.5.3-pre.005-fix010.zip
|
||||
```
|
||||
|
||||
Cette archive sert uniquement à réauditer l'ancienne vertical slice transaction PostgreSQL ; elle n'a aucune autorité normative sur l'architecture KSP actuelle.
|
||||
|
||||
## 6. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.3.2/rel.001.md
|
||||
```
|
||||
|
||||
## 7. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
```
|
||||
|
||||
## 8. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 9. Validations exécutées pour préparer le delta
|
||||
|
||||
Contrôles statiques de l'overlay :
|
||||
|
||||
```text
|
||||
Cargo.toml version 0.3.2
|
||||
payload rel.001 2 fichiers exactement
|
||||
CHANGELOG.md non rouvert
|
||||
ROADMAP.md non rouvert
|
||||
prompt 022 non rouvert
|
||||
README/USAGE non rouverts
|
||||
plan / validation non rouverts
|
||||
architecture / règles non rouvertes
|
||||
Config / schemas / migrations non rouverts
|
||||
code / tests non rouverts
|
||||
aucun fichier de rattrapage présent
|
||||
```
|
||||
|
||||
La préparation de l'archive ne rejoue aucun test Cargo dans l'environnement de génération. Les preuves techniques acquises sont celles des gates opérateur ci-dessus ; le gate stable final reste à exécuter après application.
|
||||
|
||||
## 10. Validation stable après application
|
||||
|
||||
Exécuter avant commit/tag :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Il n'est pas nécessaire de rejouer les trois builds Tauri, les graphes Cargo ni le smoke PostgreSQL réel : `rel.001` ne modifie ni code/runtime, ni dépendance, ni Config, ni migration, ni surface desktop.
|
||||
|
||||
## 11. Questions ouvertes
|
||||
|
||||
Aucune question ouverte ne bloque la publication de `0.3.2`.
|
||||
|
||||
Les décisions métier/physiques suivantes appartiennent explicitement à `0.3.3-pre.001` :
|
||||
|
||||
```text
|
||||
binding vérifiable database <-> RawNetworkId
|
||||
représentation PostgreSQL sans narrowing des valeurs unsigned
|
||||
V001 RawTransaction + observations
|
||||
clés/contraintes/indexes
|
||||
ordre total et version du cursor
|
||||
atomicité canonical + observation
|
||||
idempotence et conflits concurrents
|
||||
rétention/tombstone/ForceRehydrate physiques
|
||||
mapping SQL -> erreurs Store redacted
|
||||
```
|
||||
|
||||
`RawAccountState` PostgreSQL reste réservé à `0.3.4`.
|
||||
|
||||
## 12. Commit et tag stable
|
||||
|
||||
Après succès du gate stable :
|
||||
|
||||
```text
|
||||
commit : v0.3.2-rel.001
|
||||
tag : v0.3.2
|
||||
```
|
||||
|
||||
Aucun tag intermédiaire `rel.001` n'est requis.
|
||||
|
||||
Commandes Git typiques :
|
||||
|
||||
```bash
|
||||
git add Cargo.toml deltas/0.3.2/rel.001.md
|
||||
git commit -m "v0.3.2-rel.001"
|
||||
git tag v0.3.2
|
||||
git push
|
||||
git push origin v0.3.2
|
||||
```
|
||||
|
||||
## 13. Suite
|
||||
|
||||
Après publication du tag stable `v0.3.2`, ouvrir exclusivement :
|
||||
|
||||
```text
|
||||
0.3.3-pre.001 — PostgreSQL RawTransaction vertical slice
|
||||
```
|
||||
|
||||
avec :
|
||||
|
||||
```text
|
||||
prompts/022-V0_3_3_START_PROMPT.md
|
||||
```
|
||||
|
||||
La session suivante doit recevoir :
|
||||
|
||||
```text
|
||||
archive opérateur exacte de v0.3.2
|
||||
khadhroony-bot3_v0.5.3-pre.005-fix010.zip
|
||||
```
|
||||
|
||||
`0.3.3-pre.001` reste une tranche d'audit/brainstorming/sizing et ne doit pas commencer l'implémentation SQL lourde avant fermeture des décisions de schéma, concurrence, cursorisation et rétention.
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# Data, Materialization et Store
|
||||
|
||||
@@ -291,26 +291,39 @@ La première implementation `0.3.1` est volontairement **RAW-only** : elle ne cr
|
||||
|
||||
Les surfaces CORE/DECODE/SPECIALIZED sont ajoutées quand leurs couches sont réellement ouvertes.
|
||||
|
||||
## `ksp-store-lib`
|
||||
## `ksp-store-lib` et backends physiques
|
||||
|
||||
`ksp-store-lib` fournit PostgreSQL comme backend officiel de référence derrière `ksp-store-api`.
|
||||
`ksp-store-lib` est la façade/runtime Store commune derrière `ksp-store-api`. Il sélectionne uniquement les backends compilés par ses features, convertit ses settings KSP-owned vers le backend retenu, expose le lifecycle commun et masque les objets physiques du moteur.
|
||||
|
||||
Il possède :
|
||||
Il possède notamment :
|
||||
|
||||
- migrations ;
|
||||
- SQL ;
|
||||
- transactions ;
|
||||
- mapping backend ;
|
||||
- pagination ;
|
||||
- claim/lease lorsque nécessaire ;
|
||||
- notifications backend si retenues.
|
||||
- identité et sélection de backend côté façade ;
|
||||
- settings Store publics KSP-owned indépendants de Config ;
|
||||
- lifecycle commun `open` / `close` ;
|
||||
- dispatch vers le backend compilé ;
|
||||
- mapping des diagnostics/health backend vers une projection portable lorsque cette surface est justifiée ;
|
||||
- réexport de la surface `ksp-store-api` utile aux consumers.
|
||||
|
||||
Il ne possède pas :
|
||||
Le backend PostgreSQL officiel appartient à `ksp-store-postgres-lib`. Cette crate dépend de `ksp-store-api`, ne dépend jamais de `ksp-store-lib` et possède seule :
|
||||
|
||||
- transport réseau ;
|
||||
- `tokio-postgres` et le pool PostgreSQL ;
|
||||
- le connecteur TLS PostgreSQL ;
|
||||
- SQL et statements physiques ;
|
||||
- transactions PostgreSQL ;
|
||||
- migrations/bootstrap et metadata de schéma ;
|
||||
- mapping rows/backend ;
|
||||
- détails de cursorisation physique ;
|
||||
- diagnostics PostgreSQL internes.
|
||||
|
||||
`ksp-store-lib` et les crates backend ne possèdent pas :
|
||||
|
||||
- transport réseau d’acquisition ;
|
||||
- decoder Program ;
|
||||
- materializer ;
|
||||
- orchestration de worker/job.
|
||||
- orchestration de worker/job ;
|
||||
- politique de batch, backlog, priorité ou retry de processing.
|
||||
|
||||
La pagination/cursorisation reste un contrat de navigation Store. Une limite demandée par l’appelant peut être validée pour sa forme/sécurité, mais aucun plafond métier global arbitraire ni batch-size de worker n’est introduit par le runtime Store.
|
||||
|
||||
## `ksp-materializer-api` et `ksp-materializer-lib`
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/000-README.md -->
|
||||
<!-- version: 68 -->
|
||||
<!-- version: 69 -->
|
||||
|
||||
# Plans KSP
|
||||
|
||||
@@ -31,6 +31,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
|
||||
- [`020-V0_2_13_INTERFACE_PLAN.md`](020-V0_2_13_INTERFACE_PLAN.md) — plan historique clôturé de la release stable `0.2.13 — Interface / wire foundation`; il fixe la surface finale `ProgramAccountMeta` + `ProgramInstruction`, les bornes `255` / `10_240`, le firewall Interface -> Core, les canaris de complétude/consumer externe et la frontière avec Program API/RAW/CORE.
|
||||
- [`021-V0_2_14_PROGRAM_API_PLAN.md`](021-V0_2_14_PROGRAM_API_PLAN.md) — plan historique clôturé de la release stable `0.2.14 — Program API foundation`; il fixe la façade instruction-only ouverte, les enums Recognition/Outcome, `ProgramInstructionDecoder`, l'output associé possédé par l'implémentation, le canari externe avec Program Pubkey non enregistré, le firewall Core/Interface et le report du payload canonique D3, du registry runtime et de `ProgramExecutionPreparer`.
|
||||
- [`022-V0_3_1_STORE_RAW_PLAN.md`](022-V0_3_1_STORE_RAW_PLAN.md) — plan candidat réconcilié de `0.3.1 — Store API RAW foundation`; il fixe `ksp-store-api` seul, les modèles transaction/account + observations, queries/outcomes/capabilities, rétention/tombstone, la frontière event-only/Interface et le report de `ksp-store-lib` + PostgreSQL à `0.3.2`.
|
||||
- [`023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md`](023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md) — plan candidat réconcilié de `0.3.2 — Store/PostgreSQL runtime foundation`; il fixe le split façade/backend, `std.store` multi-target réseau-spécifique, tokio-postgres/Deadpool/Rustls, migrations metadata-only, health/readiness, live PostgreSQL et les reports des vertical slices RAW vers `0.3.3`/`0.3.4`.
|
||||
|
||||
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
||||
|
||||
|
||||
1269
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
Normal file
1269
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/rules/RULES_DEPENDENCIES.md -->
|
||||
<!-- version: 16 -->
|
||||
<!-- version: 17 -->
|
||||
|
||||
# Règles des dépendances KSP
|
||||
|
||||
@@ -79,13 +79,15 @@ Elles complètent les règles Rust générales et le graphe de `docs/architectur
|
||||
- **DEP-MAT-002** — `ksp-materializer-api` et `ksp-materializer-lib` ne dépendent pas de `ksp-store-api` ou `ksp-store-lib`.
|
||||
- **DEP-MAT-003** — Une matérialisation générique doit pouvoir produire un output compatible avec le journal D3 sans imposer une table PostgreSQL spécialisée par materializer.
|
||||
- **DEP-STORE-001** — `ksp-store-api` ne dépend pas de Program, Materializer ou Transport.
|
||||
- **DEP-STORE-002** — `ksp-store-lib` dépend de `ksp-store-api` et contient l'implémentation PostgreSQL de référence ; il ne dépend pas des implémentations Program/Materializer/Transport.
|
||||
- **DEP-STORE-002** — `ksp-store-lib` dépend de `ksp-store-api`, porte la façade/runtime Store commune et peut dépendre optionnellement de crates backend compilées par feature ; il ne dépend pas des implémentations Program/Materializer/Transport.
|
||||
- **DEP-STORE-003** — Les workers/jobs spécialisés sont propriétaires des conversions entre modèles runtime et DTO persistants.
|
||||
- **DEP-STORE-004** — Les niveaux durables sont D1 Raw, D2 Core canonique, D3 journal de matérialisation générique et D4 projections spécialisées.
|
||||
- **DEP-STORE-005** — Les replays D1 -> D2, D2 -> D3 et D3 -> D4 doivent pouvoir être exécutés indépendamment.
|
||||
- **DEP-STORE-006** — Une notification de donnée persistée ne constitue jamais la source de vérité du backlog ; les queries Store et marqueurs durables d'idempotence/version de processor font autorité.
|
||||
- **DEP-STORE-007** — Une notification de donnée est publiée seulement après persistence/commit réussis.
|
||||
- **DEP-STORE-008** — D4 est organisé par faits canoniques quand les invariants le permettent, et non par familles de tables propres aux protocoles.
|
||||
- **DEP-STORE-009** — `ksp-store-postgres-lib` dépend de `ksp-store-api`, possède seul le driver, le pool, TLS, SQL et les migrations PostgreSQL physiques, et ne dépend jamais de `ksp-store-lib`.
|
||||
- **DEP-STORE-010** — Les consumers runtime ordinaires — workers, jobs, services et apps — dépendent de `ksp-store-lib` et non directement d’une crate backend ; Config/composition traduit la configuration effective vers les settings publics Store sans créer de dépendance Store -> Config.
|
||||
|
||||
## Transport
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/rules/RULES_KSP.md -->
|
||||
<!-- version: 38 -->
|
||||
<!-- version: 39 -->
|
||||
|
||||
# Règles spécifiques à KSP
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
- **KSP-API-003** — KSP ne crée pas de `ksp-api-lib` monolithique regroupant les contrats de domaines indépendants.
|
||||
- **KSP-API-004** — Un contrat public extensible doit pouvoir être implémenté depuis une crate séparée du workspace principal lorsque cela est techniquement pertinent.
|
||||
- **KSP-API-005** — Les signatures des contrats publics utilisent en priorité des types publics KSP et les primitives externes explicitement admises ; elles ne doivent pas imposer des détails internes instables.
|
||||
- **KSP-API-006** — `ksp-store-lib` contient PostgreSQL comme implémentation officielle de référence derrière `ksp-store-api`.
|
||||
- **KSP-API-006** — `ksp-store-lib` est la façade/runtime Store commune derrière `ksp-store-api` et sélectionne uniquement des backends compilés via ses features ; l’implémentation PostgreSQL officielle appartient à `ksp-store-postgres-lib`, qui dépend de `ksp-store-api` et ne dépend jamais de `ksp-store-lib`.
|
||||
- **KSP-API-007** — Une crate `*-api` n'est créée que lorsqu'un vrai besoin d'extension, backend ou lifecycle le justifie ; la symétrie de nommage n'est jamais une justification suffisante.
|
||||
|
||||
## Configuration et environnement
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/000-README.md -->
|
||||
<!-- version: 32 -->
|
||||
<!-- version: 33 -->
|
||||
|
||||
# Validations KSP
|
||||
|
||||
@@ -27,3 +27,4 @@ Documents :
|
||||
- [`016-V0_2_13_INTERFACE.md`](016-V0_2_13_INTERFACE.md) — matrice historique clôturée de la release stable `0.2.13` : ownership Interface/Core/Transport/Program, surface passive, bornes/adversarial, façade publique exacte, consumer externe, release completeness et dependency firewall.
|
||||
- [`017-V0_2_14_PROGRAM_API.md`](017-V0_2_14_PROGRAM_API.md) — matrice historique clôturée de la release stable `0.2.14 — Program API foundation` : façade instruction-only ouverte, Recognition/Outcome, trait externe, Program Pubkey non enregistré, hardening adversarial, release completeness et firewall Core/Interface.
|
||||
- [`018-V0_3_1_STORE_RAW.md`](018-V0_3_1_STORE_RAW.md) — matrice candidate finale de `0.3.1 — Store API RAW foundation` : modèles transaction/account, observations, capabilities backend, pagination sans policy executor, outcomes, rétention/tombstone, hardening, gate complet `pre.008` et reports explicites vers `0.3.2+`.
|
||||
- [`019-V0_3_2_STORE_POSTGRES_FOUNDATION.md`](019-V0_3_2_STORE_POSTGRES_FOUNDATION.md) — matrice candidate finale de `0.3.2 — Store/PostgreSQL runtime foundation` : feature graph, settings/Config, pool/TLS, migrations metadata-only, health, hardening, graphes, builds Tauri et preuve PostgreSQL réelle major 17.
|
||||
|
||||
726
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
Normal file
726
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
Normal file
@@ -0,0 +1,726 @@
|
||||
<!-- file: docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md -->
|
||||
<!-- version: 13 -->
|
||||
|
||||
# Validation `0.3.2` — Store/PostgreSQL runtime foundation
|
||||
|
||||
## 1. Objet
|
||||
|
||||
Cette matrice est ouverte par `0.3.2-pre.001`. Elle fixe les critères de preuve de la fondation Store/PostgreSQL sans préjuger des résultats non encore exécutés.
|
||||
|
||||
Statuts :
|
||||
|
||||
```text
|
||||
TODO preuve requise non encore exécutée
|
||||
PASS preuve réellement exécutée et verte
|
||||
FAIL preuve exécutée et en échec
|
||||
N/A non applicable avec justification
|
||||
```
|
||||
|
||||
Aucun `TODO` n'est présenté comme validé.
|
||||
|
||||
## 2. Base et audit `pre.001`
|
||||
|
||||
### V32-BASE-001 — Base stable
|
||||
|
||||
Critère :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.1 sur l'archive source
|
||||
deltas/0.3.1/rel.001.md présent
|
||||
prompt 021 présent
|
||||
ksp-store-api présent
|
||||
ksp-store-lib absent
|
||||
ksp-store-postgres-lib absent
|
||||
```
|
||||
|
||||
Statut : `PASS` au gate documentaire `pre.001`.
|
||||
|
||||
Note : metadata Git absente de l'archive ; le tag `v0.3.1` n'est pas interrogé localement.
|
||||
|
||||
### V32-AUDIT-001 — Sources internes
|
||||
|
||||
Critère : règles, architecture, plan/validation `0.3.1`, source/tests `ksp-store-api`, CHANGELOG et ROADMAP relus avant design.
|
||||
|
||||
Statut : `PASS` au gate documentaire `pre.001`.
|
||||
|
||||
### V32-AUDIT-002 — Archive kbot3
|
||||
|
||||
Critère : ancien Store/config/PostgreSQL/migrations/health/pool/erreurs relus et classés reprendre/redessiner/reporter/rejeter.
|
||||
|
||||
Statut : `PASS` au gate documentaire `pre.001`.
|
||||
|
||||
### V32-AUDIT-003 — Audit externe actuel
|
||||
|
||||
Critère : PostgreSQL, tokio-postgres, pool, TLS et helper migrations réaudités avec sources actuelles.
|
||||
|
||||
Statut : `PASS` au gate documentaire `pre.001`.
|
||||
|
||||
Référence du gate :
|
||||
|
||||
```text
|
||||
PostgreSQL 18.6
|
||||
tokio-postgres 0.7.18
|
||||
deadpool-postgres 0.14.2
|
||||
tokio-postgres-rustls 0.14.0
|
||||
sha2 0.11.0
|
||||
refinery 0.9.2 audité/rejeté
|
||||
```
|
||||
|
||||
Le gate opérateur après application de `pre.001`, fourni le 29 août 2026, est vert : audits Rust/Markdown, `cargo check --workspace` et `cargo clippy --workspace --all-targets` passent sur `0.3.2-pre.1`.
|
||||
|
||||
Le gate opérateur de `pre.002`, fourni le 29 août 2026, confirme également les tests ciblés, `--no-default-features` et les `cargo tree`. `cargo check`/Clippy terminent avec succès mais signalent deux warnings par nouvelle crate (`TRACING_TARGET` importé mais inutilisé et constant `dead_code`) ; `pre.002-fix.001` traite cet écart par un canari compile-time privé, tout en conservant les invariants KSP-TRACE-102/103 et sans ajouter de logging runtime.
|
||||
|
||||
Le gate opérateur de `pre.002-fix.001`, fourni le 29 août 2026, est entièrement vert et sans warning : audits Rust/Markdown, workspace check/Clippy, tests des deux crates et compilation `ksp-store-lib --no-default-features` passent. Cette base est l'entrée effective de `pre.003`.
|
||||
|
||||
Le gate opérateur de `pre.003-fix.001`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-lib` avec et sans feature par défaut, tests backend et compilation `--no-default-features` passent. Cette base est l'entrée effective de `pre.004`.
|
||||
|
||||
Le gate opérateur de `pre.004-fix.001`, après correction manuelle des commentaires `.env.example`, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 126 tests `ksp-config-lib`, ownership, `ksp-store-lib` avec et sans feature PostgreSQL, compilation `--no-default-features` et 63 tests Config Desk passent. Cette base est l'entrée effective de `pre.005`.
|
||||
|
||||
Le gate opérateur de `pre.005-fix.001`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 5 tests runtime backend, canaris de dépendances/API, façade avec et sans feature PostgreSQL et compilation `--no-default-features` passent. Cette base est l'entrée effective de `pre.006`.
|
||||
|
||||
Le gate opérateur de `pre.006`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 9 tests backend incluant la migration V000, façade avec et sans feature PostgreSQL, compilation `--no-default-features` et graphes Cargo passent.
|
||||
|
||||
Le gate opérateur de `pre.007`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 12 tests backend incluant health/readiness, façade avec et sans feature PostgreSQL, compilation `--no-default-features` et graphes Cargo passent.
|
||||
|
||||
Le gate `pre.008` du 29 août 2026 est doublement vert : le gate déterministe passe intégralement puis `postgres_foundation_live` passe sur **PostgreSQL 17**. La preuve réelle couvre bootstrap initial/idempotent/concurrent, mismatch/recovery checksum, rollback transactionnel, health `Ready`, fermeture bornée et cleanup de la metadata possédée par le test. Cette base est l'entrée effective de `pre.009`.
|
||||
|
||||
Le gate opérateur de `pre.009`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, `ksp-store-api`, backend PostgreSQL, façade avec et sans feature, Config, compilation `--no-default-features`, graphes/features/duplicates et `cargo test --workspace` passent. Les canaris hardening/completeness ajoutés par `pre.009` sont donc validés. Cette base est l'entrée effective de `pre.010`.
|
||||
|
||||
## 3. Frontières Cargo
|
||||
|
||||
### V32-DEP-001 — Façade -> API
|
||||
|
||||
```text
|
||||
ksp-store-lib -> ksp-store-api
|
||||
```
|
||||
|
||||
Matérialisé par `0.3.2-pre.002` dans le manifest de `ksp-store-lib`.
|
||||
|
||||
Statut : `PASS pre.002 opérateur` — test ciblé et `cargo tree` fournis.
|
||||
|
||||
### V32-DEP-002 — Backend -> API
|
||||
|
||||
```text
|
||||
ksp-store-postgres-lib -> ksp-store-api
|
||||
```
|
||||
|
||||
Matérialisé par `0.3.2-pre.002` dans le manifest de `ksp-store-postgres-lib`.
|
||||
|
||||
Statut : `PASS pre.002 opérateur` — test ciblé et `cargo tree` fournis.
|
||||
|
||||
### V32-DEP-003 — Pas de cycle backend
|
||||
|
||||
```text
|
||||
ksp-store-postgres-lib -X-> ksp-store-lib
|
||||
```
|
||||
|
||||
Preuves : manifest scanner + cargo tree.
|
||||
|
||||
Le canari source de `pre.002` interdit explicitement la dépendance inverse ; le `cargo tree` opérateur confirme l'absence de cycle. La frontière sera durcie à nouveau en `pre.009`.
|
||||
|
||||
Statut : `PASS pre.002 opérateur / PASS pre.009 opérateur`.
|
||||
|
||||
### V32-DEP-004 — Feature PostgreSQL
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
postgres est default feature de ksp-store-lib
|
||||
ksp-store-postgres-lib est optional dependency
|
||||
cargo check -p ksp-store-lib --no-default-features passe
|
||||
```
|
||||
|
||||
Le manifest de `ksp-store-lib` matérialise `default = ["postgres"]` et l'optional dependency `ksp-store-postgres-lib`. La compilation sans default feature est une preuve opérateur obligatoire.
|
||||
|
||||
Statut : `PASS pre.002 opérateur` — default feature, optional backend edge et compilation `--no-default-features` confirmés.
|
||||
|
||||
### V32-DEP-005 — Firewall domaines
|
||||
|
||||
Interdictions :
|
||||
|
||||
```text
|
||||
Store/backend -> Config
|
||||
Store/backend -> Transport
|
||||
Store/backend -> Program
|
||||
Store/backend -> Materializer
|
||||
consumer ordinaire -> ksp-store-postgres-lib
|
||||
```
|
||||
|
||||
Matérialisé par `pre.005` : les types physiques nécessaires sont publics uniquement dans la crate backend pour la frontière inter-crates et ne sont jamais réexportés par `ksp-store-lib`; `Pool/Client/Row/Statement` restent absents de sa crate-root.
|
||||
|
||||
Statut : `PASS pre.005-fix.001 opérateur / PASS pre.009 opérateur`.
|
||||
|
||||
### V32-DEP-006 — `ksp-store-api` non régressé
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
dépendance runtime exacte ksp-core-lib seulement
|
||||
exports/capabilities existants conservés
|
||||
aucun type backend ajouté pour PostgreSQL
|
||||
```
|
||||
|
||||
Statut : `PASS pre.009 opérateur / PASS pre.010 opérateur` — le canari backend externe de `ksp-store-api` est rejoué sans modification de la crate.
|
||||
|
||||
## 4. Public API et settings
|
||||
|
||||
### V32-API-001 — Settings Config-independent
|
||||
|
||||
Critère : `StoreSettings` est constructible sans `ksp-config-lib`, sans env et sans serde requis par la façade.
|
||||
|
||||
Matérialisé par `pre.003` : `StoreSettings`, `StoreBackendSettings`, `PostgresStoreSettings`, pool/bootstrap/TLS typés, sans dépendance Config/serde/env.
|
||||
|
||||
Statut : `PASS pre.003-fix.001 opérateur`.
|
||||
|
||||
### V32-API-002 — Backend connu non compilé
|
||||
|
||||
Critère : `Postgres` reste un backend connu sans feature et `Store::open` échoue avant I/O avec un code stable.
|
||||
|
||||
Matérialisé par `pre.003` : le test `feature_mismatch` appelle réellement `Store::open` sous `--no-default-features` et exige `store.backend_not_compiled`.
|
||||
|
||||
Statut : `PASS pre.003-fix.001 opérateur / PASS pre.009 opérateur`.
|
||||
|
||||
### V32-API-003 — Aucun type backend physique public
|
||||
|
||||
Interdits dans crate-root `ksp-store-lib` :
|
||||
|
||||
```text
|
||||
tokio_postgres::*
|
||||
deadpool_postgres::*
|
||||
rustls::*
|
||||
PostgresBackend / Pool / Client / Row / Statement
|
||||
```
|
||||
|
||||
Statut : `PASS pre.009 opérateur`.
|
||||
|
||||
### V32-API-004 — Réexports Store API
|
||||
|
||||
Critère : un consumer de `ksp-store-lib` accède aux contrats Store API utiles sans dépendre directement de la crate backend.
|
||||
|
||||
`pre.003` réexporte explicitement les 60 symboles crate-root acquis de `ksp-store-api` depuis `ksp-store-lib`, sans glob et sans réexport backend.
|
||||
|
||||
Statut : `PASS pre.003-fix.001 opérateur / PASS pre.009 opérateur exact exports`.
|
||||
|
||||
### V32-API-005 — Lifecycle
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
Store::open async
|
||||
Store::close(self) async
|
||||
pas de pool/client échappé
|
||||
close borné
|
||||
Drop best-effort seulement
|
||||
```
|
||||
|
||||
`pre.003` fixe les signatures `Store::open(settings).await` et `Store::close(self).await`. `pre.005` remplace le stop de staging PostgreSQL par l'ouverture physique : un succès exige `pool.get().await` après construction du pool, puis `close(self)` ferme et draine ce pool sous la deadline configurée. Aucun pool/client n'est exposé par la façade.
|
||||
|
||||
Statut : `PASS pre.003-fix.001 opérateur` pour les signatures / `PASS pre.005-fix.001 opérateur` pour l'ouverture et le shutdown physiques / `PASS pre.007 opérateur` pour la composition health.
|
||||
|
||||
## 5. Config ownership
|
||||
|
||||
### V32-CONFIG-001 — Document/schema/example
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
std.store enregistré
|
||||
schema V1 valide
|
||||
example valide
|
||||
profiles/targets nommés typés
|
||||
un réseau explicite par target
|
||||
backend postgres explicite
|
||||
```
|
||||
|
||||
Matérialisé par `pre.004` puis corrigé par `pre.004-fix.001` avec `cfg.std.store` / `schema.std.store`, trois targets committed `devnet`/`mainnet`/`testnet`, `default_profile = devnet`, un `network` explicite par target et des URI PostgreSQL séparées.
|
||||
|
||||
Statut : `PASS pre.004-fix.001 opérateur`.
|
||||
|
||||
### V32-CONFIG-002 — Secrets/provenance
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
KSP_SECRET_STORE_{DEVNET,MAINNET,TESTNET}_POSTGRES_URI classés Secret
|
||||
safe projection redacted
|
||||
provenance sans valeur
|
||||
dotenv inventory à jour
|
||||
```
|
||||
|
||||
Matérialisé par `pre.004-fix.001` : les trois URI réseau-spécifiques sont inventoriées, chaque fallback reste `Secret`, la safe projection est redacted et la provenance n'embarque aucune valeur.
|
||||
|
||||
Statut : `PASS pre.004-fix.001 opérateur`.
|
||||
|
||||
### V32-CONFIG-002B — Target/réseau sans multiplexage
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
profile_id std.store = identifiant de target Store
|
||||
default_profile choisit exactement un target
|
||||
StoreSettings porte exactement un RawNetworkId
|
||||
résolution explicite mainnet/devnet/testnet retourne URI + network du target choisi
|
||||
ksp-store-lib ne possède aucun routeur multi-target/multi-réseau
|
||||
future RAW mismatch Store.network != entity/query.network rejeté avant I/O
|
||||
```
|
||||
|
||||
Matérialisé contractuellement par `pre.004-fix.001`; l'enforcement sur opérations RAW sera exercé dans `0.3.3`/`0.3.4`.
|
||||
|
||||
Statut : `PASS pre.004-fix.001 opérateur` pour la sélection target/réseau / `N/A 0.3.2` pour le mismatch d'opérations RAW, dont l'enforcement appartient aux vertical slices `0.3.3`-`0.3.4`.
|
||||
|
||||
### V32-CONFIG-003 — No-env Store/backend
|
||||
|
||||
Critère : production sources `ksp-store-lib` et `ksp-store-postgres-lib` ne lisent aucun :
|
||||
|
||||
```text
|
||||
std::env
|
||||
dotenv
|
||||
KSP_*
|
||||
KSPB_*
|
||||
PG*
|
||||
.pgpass
|
||||
```
|
||||
|
||||
`pre.004` renforce aussi le canari d'ownership avec les nouveaux filenames Store ; `pre.004-fix.001` conserve cette frontière tout en ajoutant le réseau au contrat `StoreSettings`. Store/backend restent sans dépendance Config et sans lecture KSP/KSPB.
|
||||
|
||||
Statut : `PASS pre.004-fix.001 opérateur / PASS pre.009 opérateur`.
|
||||
|
||||
### V32-CONFIG-004 — Adapter Config -> Store
|
||||
|
||||
Critère : `ksp-config-lib` seul transforme un profil/target résolu en `StoreSettings` réseau-spécifique et ne transmet aucun secret dans diagnostics.
|
||||
|
||||
Matérialisé par `pre.004` puis `pre.004-fix.001` : seul `ksp-config-lib` dépend de `ksp-store-lib` avec `default-features = false`; il mappe le `profile_id` sélectionné vers un target, construit son `RawNetworkId` et ses settings backend sans forcer la feature backend.
|
||||
|
||||
Statut : `PASS pre.004-fix.001 opérateur`.
|
||||
|
||||
## 6. Pool et lifecycle PostgreSQL
|
||||
|
||||
### V32-POOL-001 — Pool borné
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
max_connections 1..64
|
||||
wait/create/recycle timeouts bornés
|
||||
aucune taille zéro
|
||||
aucune valeur pathologique
|
||||
```
|
||||
|
||||
Matérialisé par `pre.005` : `deadpool-postgres` possède un pool `max_size` explicite et applique les deadlines `wait/create/recycle`; `tokio-postgres::Config` reçoit aussi le `connect_timeout` typé après parsing de l'URI.
|
||||
|
||||
Statut : `PASS pre.005-fix.001 opérateur`.
|
||||
|
||||
### V32-POOL-002 — Connection task ownership
|
||||
|
||||
Critère : chaque connection future tokio-postgres est pilotée par le manager retenu et son task handle reste possédé jusqu'au drop/close.
|
||||
|
||||
`pre.005` délègue la création/recycle des connexions au `Manager` Deadpool retenu ; aucun `tokio::spawn` KSP n'est introduit dans Store. La preuve de non-régression détaillée reste au hardening.
|
||||
|
||||
Statut : `PASS pre.005-fix.001 opérateur / PASS pre.009 opérateur`.
|
||||
|
||||
### V32-POOL-003 — Open failure safe
|
||||
|
||||
Critère : DNS/connect/auth/server errors ne copient ni URI ni texte remote arbitraire dans Display/Debug public.
|
||||
|
||||
Matérialisé par `pre.005` : parsing/connexion/pool/TLS sont ramenés à une classification backend locale sans conserver le texte des erreurs externes ni l'URI. La façade mappe vers des codes `store.postgres_*` stables avec contexte statique sûr.
|
||||
|
||||
Statut : `PASS pre.005-fix.001 opérateur`.
|
||||
|
||||
### V32-POOL-004 — Close
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
pool fermé
|
||||
nouvelles acquisitions refusées
|
||||
shutdown respecte timeout
|
||||
aucune tâche volontairement laissée orpheline
|
||||
```
|
||||
|
||||
`pre.005` matérialise déjà `Pool::close()` et un drain borné par `shutdown_timeout`; `Drop` ne fait qu'un `close()` best-effort. La preuve end-to-end avec backend réel reste réservée à `pre.007/pre.008`.
|
||||
|
||||
Statut : `PASS pre.005-fix.001 opérateur / PASS pre.007 opérateur / PASS live pre.008 PostgreSQL 17`.
|
||||
|
||||
## 7. TLS
|
||||
|
||||
### V32-TLS-001 — Modes exacts
|
||||
|
||||
Surface initiale :
|
||||
|
||||
```text
|
||||
Disabled
|
||||
VerifyFull
|
||||
```
|
||||
|
||||
Matérialisé par `pre.005` : la surface physique accepte exactement `Disabled` et `VerifyFull`; l'URI parsée est ensuite normalisée vers `SslMode::Disable` ou `SslMode::Require`, donc elle ne peut pas modifier la policy typée.
|
||||
|
||||
Statut : `PASS pre.005-fix.001 opérateur`.
|
||||
|
||||
### V32-TLS-002 — VerifyFull
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
TLS requis
|
||||
root store système
|
||||
authenticité du certificat vérifiée
|
||||
nom serveur vérifié
|
||||
aucun fallback plaintext
|
||||
```
|
||||
|
||||
Matérialisé par `pre.005` : `VerifyFull` construit un `rustls::RootCertStore` à partir des roots système, utilise explicitement le provider AWS-LC, requiert un host TCP pour l'identité serveur, rejette `hostaddr` seul et les sockets Unix, et ne permet aucun fallback plaintext. Les erreurs de roots/certificats sont réduites à des compteurs sûrs.
|
||||
|
||||
Statut : `PASS pre.005-fix.001 opérateur`.
|
||||
|
||||
### V32-TLS-003 — Pas de fichier TLS implicite
|
||||
|
||||
Critère : backend ne lit pas `sslrootcert`, `sslcert`, `sslkey`, `.postgresql/*` ou autre fichier implicite hors settings KSP.
|
||||
|
||||
Statut : `PASS pre.009 opérateur`.
|
||||
|
||||
## 8. Migration/bootstrap
|
||||
|
||||
### V32-MIG-001 — Metadata privée uniquement
|
||||
|
||||
Critère : `0.3.2` crée au plus la relation d'infrastructure :
|
||||
|
||||
```text
|
||||
ksp_store_schema_migrations
|
||||
```
|
||||
|
||||
et aucune table métier RAW/CORE/DECODE/SPECIALIZED.
|
||||
|
||||
`pre.006` embarque exactement `migrations/V000__bootstrap.sql`, dont le seul DDL de production crée `ksp_store_schema_migrations`. Le canari source interdit les identifiants métier RAW/CORE/DECODE/SPECIALIZED dans le moteur et la ressource SQL.
|
||||
|
||||
Statut : `PASS pre.006 opérateur / PASS live pre.008 PostgreSQL 17`.
|
||||
|
||||
### V32-MIG-002 — Version/checksum
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
version entière monotone
|
||||
nom immuable
|
||||
SHA-256 du SQL exact
|
||||
sentinel bootstrap version 0
|
||||
mismatch historique terminal
|
||||
```
|
||||
|
||||
`pre.006` calcule explicitement SHA-256 sur les octets exacts du SQL embarqué puis encode les 32 octets en 64 caractères hex minuscules. Le sentinel `(0, bootstrap, checksum)` est inséré dans la même transaction que la création metadata. Les tests unitaires figent le checksum du SQL committed et couvrent sentinel valide, missing, nom/checksum divergents et historique plus récent.
|
||||
|
||||
Statut : `PASS pre.006 opérateur / PASS live pre.008 PostgreSQL 17`.
|
||||
|
||||
### V32-MIG-003 — Concurrence
|
||||
|
||||
Critère : deux runners concurrents sont sérialisés par advisory transaction lock avec attente bornée.
|
||||
|
||||
`pre.006` utilise une clé KSP fixe et `pg_try_advisory_xact_lock($1)` dans une boucle bornée par `migration_lock_timeout`, avec polling de 25 ms maximum. Aucun lock bloquant illimité n'est utilisé.
|
||||
|
||||
Statut : `PASS pre.006 opérateur / PASS concurrence live pre.008 PostgreSQL 17`.
|
||||
|
||||
### V32-MIG-004 — Atomicité/recovery
|
||||
|
||||
Critère : échec d'une migration du run courant rollback DDL + history de ce run ; un rerun depuis état précédent reste sûr.
|
||||
|
||||
`pre.006` place lock, metadata DDL, sentinel et validation dans une transaction unique ; toute sortie d'erreur avant `commit()` droppe la transaction et PostgreSQL rollback le run courant. Un timeout externe borne également l'ensemble du bootstrap. L'injection d'échec et la preuve physique du rollback restent au smoke réel.
|
||||
|
||||
Statut : `PASS pre.006 opérateur / PASS rollback live pre.008 PostgreSQL 17`.
|
||||
|
||||
### V32-MIG-005 — Newer runtime guard
|
||||
|
||||
Critère : migration appliquée inconnue/supérieure à la liste embarquée produit `STORE_POSTGRES_SCHEMA_NEWER`, sans down migration.
|
||||
|
||||
`pre.006` connaît uniquement la version `0`; toute history `> 0` est classée `SchemaNewer` par le backend puis `store.postgres_schema_newer` par la façade. Aucun chemin de down migration n'existe.
|
||||
|
||||
Statut : `PASS pre.006 opérateur`.
|
||||
|
||||
### V32-MIG-006 — SQL injection
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
SQL de migration statique embarqué
|
||||
values paramétrées
|
||||
aucun identifier physique user-configurable en 0.3.2
|
||||
```
|
||||
|
||||
`pre.006` garde le DDL versionné sous `include_str!` et toutes les values variables des requêtes de contrôle (`lock key`, `statement_timeout`, history values) passent par paramètres. Les noms physiques sont des constantes KSP, jamais des settings.
|
||||
|
||||
Statut : `PASS pre.006 opérateur / PASS pre.009 opérateur hardening`.
|
||||
|
||||
### V32-MIG-007 — No business capability
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
0 impl PostgreSQL de capability RawTransaction
|
||||
0 impl PostgreSQL de capability RawAccountState
|
||||
0 repository RAW métier
|
||||
```
|
||||
|
||||
Statut : `PASS pre.009 opérateur / PASS pre.010 opérateur`.
|
||||
|
||||
## 9. Health/readiness
|
||||
|
||||
### V32-HEALTH-001 — Projection portable
|
||||
|
||||
Critère : façade expose uniquement état/backend/network/counts/migration safe, jamais URI/SQL/pool/client.
|
||||
|
||||
`pre.007` matérialise `StoreRuntimeSnapshot`, `StoreHealthState::{Ready, NotReady}` et `StoreHealthSnapshot`. Les compteurs pool sont bornés en `u32`; la version migration est optionnelle si le probe ne peut pas la lire.
|
||||
|
||||
Statut : `PASS pre.007 opérateur`.
|
||||
|
||||
### V32-HEALTH-002 — Readiness réelle
|
||||
|
||||
Critère : `Store::open` ne retourne Ready qu'après connect + bootstrap/verify selon settings, et `Store::health()` sonde ensuite la disponibilité physique sous deadline.
|
||||
|
||||
`pre.007` conserve l'ouverture stricte acquise en `pre.006` et ajoute un probe borné par le `wait_timeout` du pool : acquisition, `SELECT 1`, lecture de version migration. Le smoke réel reste nécessaire pour prouver ce chemin contre PostgreSQL.
|
||||
|
||||
Statut : `PASS pre.007 opérateur / PASS live pre.008 PostgreSQL 17`.
|
||||
|
||||
### V32-HEALTH-003 — Error redaction
|
||||
|
||||
Critère : un health failure n'expose pas server error string, query text ou credential.
|
||||
|
||||
Le backend ne conserve que `PostgresBackendErrorKind`; la façade mappe vers un `ErrorCode` KSP optionnel. Le snapshot public ne contient aucune string serveur, URI, SQL, host, user, database ou handle.
|
||||
|
||||
Statut : `PASS pre.007 opérateur / PASS pre.009 opérateur hardening`.
|
||||
|
||||
## 10. PostgreSQL integration réelle
|
||||
|
||||
### V32-LIVE-001 — Input opérateur explicite
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
#[ignore]
|
||||
URI lue depuis stdin
|
||||
aucun env requis
|
||||
URI jamais imprimée par le test
|
||||
```
|
||||
|
||||
`pre.008` ajoute `tests/postgres_foundation_live.rs`. Le test borne l'entrée à 4096 octets, ne conserve dans ses erreurs que des phases statiques et ne rend jamais l'erreur PostgreSQL brute. L'opérateur peut masquer la saisie côté shell puis piper une seule ligne sur stdin ; aucune variable d'environnement n'est lue par le test.
|
||||
|
||||
Statut : `PASS live pre.008 PostgreSQL 17`.
|
||||
|
||||
### V32-LIVE-002 — Non destructif
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
refus si metadata table préexiste
|
||||
aucune base/schema drop
|
||||
aucune table métier créée
|
||||
cleanup seulement de metadata créée par le test
|
||||
```
|
||||
|
||||
Le test interroge `information_schema.tables` avant toute mutation. Il ne prend ownership du cleanup qu'après avoir prouvé que le bootstrap initial a créé la metadata alors que la précondition était absente. Les seuls `DROP` exécutés ciblent `ksp_store_schema_migrations` pendant les resets contrôlés et le cleanup final.
|
||||
|
||||
Statut : `PASS live pre.008 PostgreSQL 17`.
|
||||
|
||||
### V32-LIVE-003 — Scénario foundation
|
||||
|
||||
Preuves :
|
||||
|
||||
```text
|
||||
connect
|
||||
bootstrap initial
|
||||
bootstrap idempotent
|
||||
bootstrap concurrent
|
||||
checksum mismatch + restore/recovery
|
||||
failure rollback du V000 exact
|
||||
health ready
|
||||
close borné
|
||||
cleanup metadata
|
||||
```
|
||||
|
||||
La preuve rollback évite tout hook public de test : une transaction `tokio-postgres` de test exécute le V000 exact par `include_str!`, insère un sentinel transitoire, provoque ensuite une erreur SQL puis est droppée. Le test vérifie que la metadata n'existe pas après le rollback implicite et que le bootstrap KSP normal peut repartir proprement.
|
||||
|
||||
Statut : `PASS live pre.008 PostgreSQL 17 / PASS live pre.010 PostgreSQL 17`.
|
||||
|
||||
### V32-LIVE-004 — PostgreSQL support
|
||||
|
||||
Critère : test refuse major < 15 et enregistre seulement le major safe réellement testé.
|
||||
|
||||
Cible release : PostgreSQL 18.6. `pre.008` interroge uniquement `SHOW server_version_num`, dérive le major et n'imprime aucune identité de serveur. Le gate opérateur réel a été exécuté avec succès sur PostgreSQL 17.
|
||||
|
||||
Statut : `PASS live pre.008 PostgreSQL 17 / PASS live pre.010 PostgreSQL 17`.
|
||||
|
||||
## 11. Security/adversarial
|
||||
|
||||
### V32-SEC-001 — URI hostile
|
||||
|
||||
Cas : vide, surdimensionnée, malformed, paramètres conflictuels, password contenant contrôles/URL-like.
|
||||
|
||||
Attendu : rejet borné sans echo.
|
||||
|
||||
`pre.009` ajoute un test d'intégration backend qui force avant I/O : URI malformed, URI > 4096 octets, `hostaddr` sans identité sous `VerifyFull` et `options=` libpq. Chaque cas contient un canary secret et exige une erreur `ConfigInvalid` avec phase statique seulement.
|
||||
|
||||
Statut : `PASS pre.009 opérateur`.
|
||||
|
||||
### V32-SEC-002 — Secret canary
|
||||
|
||||
Injecter un canary dans URI/password et prouver son absence de :
|
||||
|
||||
```text
|
||||
Debug
|
||||
Display
|
||||
ErrorContext
|
||||
tracing snapshots
|
||||
health snapshots
|
||||
```
|
||||
|
||||
`pre.009` injecte des canaris distincts dans la façade et le backend. Les settings Debug doivent afficher `<redacted>`; les erreurs pré-I/O publiques/backend ne doivent contenir aucun canary. Les snapshots health restent couverts par les canaris déterministes de `pre.007`.
|
||||
|
||||
Statut : `PASS pre.009 opérateur`.
|
||||
|
||||
### V32-SEC-003 — Timeouts hostiles
|
||||
|
||||
Cas : zéro, inversion, dépassement bornes pour pool/connect/migration/close.
|
||||
|
||||
Les bornes backend-neutral décidées en `pre.001` sont matérialisées et couvertes par tests unitaires en `pre.003`. `pre.005` applique physiquement `connect/wait/create/recycle/shutdown`; `pre.006` applique physiquement `migration_timeout` et `migration_lock_timeout`.
|
||||
|
||||
Statut : `PASS pre.003-fix.001 opérateur` pour les bornes backend-neutral / `PASS pre.005-fix.001 opérateur` pour pool/connect/shutdown / `PASS pre.006 opérateur` pour migration / `PASS pre.009 opérateur` hardening`.
|
||||
|
||||
### V32-SEC-004 — Feature mismatch avant I/O
|
||||
|
||||
`pre.003` matérialise un canari d'intégration compilé avec et sans `postgres`. Sans feature, `Store::open` retourne le code stable `store.backend_not_compiled` avant tout chemin backend physique.
|
||||
|
||||
Statut : `PASS pre.003-fix.001 opérateur / PASS pre.009 opérateur`.
|
||||
|
||||
### V32-SEC-005 — Server error sanitization
|
||||
|
||||
Un serveur/test double qui renvoie un message contenant un canary ne doit pas le faire traverser l'erreur publique.
|
||||
|
||||
Statut : bridge de sanitization matérialisé en `pre.005`; `pre.009` ajoute un canari structurel prouvant que `PostgresBackendError` ne peut retenir que `kind + phase` statiques et que `PoolError::Backend(_)` abandonne le texte remote. `PASS pre.009 opérateur`.
|
||||
|
||||
## 12. Hardening/completeness `pre.009`
|
||||
|
||||
Deux nouveaux canaris d'intégration figent sans modifier la production :
|
||||
|
||||
```text
|
||||
crates/ksp-store-lib/tests/hardening_completeness.rs
|
||||
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
|
||||
```
|
||||
|
||||
Ils couvrent :
|
||||
|
||||
```text
|
||||
84 exports façade exacts / 5 modules privés exacts
|
||||
7 exports backend exacts / 5 modules privés exacts
|
||||
manifests et feature postgres exacts
|
||||
URI hostiles et canaris secrets avant I/O
|
||||
sanitization structurelle des erreurs serveur/pool
|
||||
no-env / no-.pgpass / no-libpq-TLS-files
|
||||
aucune capability PostgreSQL RawTransaction/RawAccount
|
||||
V000 metadata-only
|
||||
compatibilité backend externe ksp-store-api rejouée au gate
|
||||
```
|
||||
|
||||
Statut global `pre.009` : `PASS opérateur` — tests ciblés, canaris hardening, Config, graphes et `cargo test --workspace` verts.
|
||||
|
||||
## 13. Gate technique final `pre.010`
|
||||
|
||||
`pre.010` ne contient aucun développement fonctionnel. Il doit revalider sur la version `0.3.2-pre.10` :
|
||||
|
||||
```text
|
||||
audits Rust/Markdown
|
||||
workspace check + Clippy
|
||||
Store API
|
||||
backend PostgreSQL
|
||||
façade default + no-default-features
|
||||
Config std.store
|
||||
cargo test --workspace
|
||||
graphes normal/features/duplicates
|
||||
PostgreSQL live foundation
|
||||
3 builds Tauri avec resources std.store packagées
|
||||
```
|
||||
|
||||
Statut global `pre.010` : `PASS technique opérateur`.
|
||||
|
||||
Le gate a été rejoué après `cargo clean` sur `0.3.2-pre.10` : audits Rust, workspace check/Clippy, tests ciblés, `cargo test --workspace`, graphes Cargo, trois builds Tauri et PostgreSQL live major 17 sont verts. La ligne d'audit Markdown opérateur a utilisé `deltas/0.3.1` par erreur ; l'overlay `pre.010` avait été audité avec `deltas/0.3.2` dans l'environnement de génération et `pre.011` rejoue le scope Markdown exact avant clôture documentaire.
|
||||
|
||||
## 14. Gates Rust/workspace
|
||||
|
||||
À chaque tranche applicable :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
```
|
||||
|
||||
Après création des crates :
|
||||
|
||||
```bash
|
||||
cargo test -p ksp-store-api
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-config-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
cargo tree -p ksp-store-lib --edges normal
|
||||
cargo tree -p ksp-store-lib -e features
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||
cargo tree --duplicates
|
||||
```
|
||||
|
||||
Gate PostgreSQL réel `pre.010` après les gates déterministes :
|
||||
|
||||
```bash
|
||||
read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo
|
||||
printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_foundation_live -- --ignored --nocapture --test-threads=1
|
||||
unset KSP_PG_TEST_URI
|
||||
```
|
||||
|
||||
`KSP_PG_TEST_URI` est ici une variable shell non exportée servant uniquement à masquer la saisie ; le test lit exclusivement stdin et ne lit aucun environnement.
|
||||
|
||||
Gate technique final :
|
||||
|
||||
```bash
|
||||
cargo test --workspace
|
||||
(cd crates/ksp-app-config-desk && cargo tauri build)
|
||||
(cd crates/ksp-app-solprices-desk && cargo tauri build)
|
||||
(cd crates/ksp-app-wallet-desk && cargo tauri build)
|
||||
```
|
||||
|
||||
Les trois builds Tauri sont requis au gate final parce que `pre.004` a réellement ajouté les resources `std.store` au packaging desktop.
|
||||
|
||||
Statut global technique : `PASS pre.010 opérateur`; audit Markdown `deltas/0.3.2` à rejouer dans le gate documentaire `pre.011`.
|
||||
|
||||
## 15. Réconciliation documentaire `pre.011`
|
||||
|
||||
Critères :
|
||||
|
||||
```text
|
||||
README/USAGE ksp-store-lib présents et alignés sur la façade publique
|
||||
README/USAGE ksp-store-postgres-lib présents et explicitement backend-only
|
||||
ksp-config-lib README/USAGE documentent cfg.std.store et les targets réseau
|
||||
indexes docs/plans/validation référencent 0.3.2
|
||||
plan 023 reflète le gate technique réel
|
||||
matrice 019 ne conserve aucun TODO applicable à 0.3.2
|
||||
CHANGELOG/ROADMAP/prompt suivant inchangés
|
||||
```
|
||||
|
||||
Les architectures générales Store ont été relues et restent cohérentes ; aucune modification artificielle n'est nécessaire.
|
||||
|
||||
Statut : `MATÉRIALISÉ pre.011 / TODO gate documentaire opérateur`.
|
||||
|
||||
## 16. Critères de fermeture
|
||||
|
||||
La matrice ne peut passer en finale que si tous les critères applicables sont `PASS` et que :
|
||||
|
||||
```text
|
||||
aucun RawTransaction PostgreSQL
|
||||
aucun RawAccountState PostgreSQL
|
||||
aucun SQL métier RAW
|
||||
aucune policy worker/job dans Store
|
||||
aucune fuite backend dans façade
|
||||
aucune lecture env par Store/backend
|
||||
PostgreSQL live vert
|
||||
workspace/clippy/tests verts
|
||||
```
|
||||
|
||||
La réconciliation finale de cette matrice est matérialisée par `pre.011`. Après son gate documentaire vert, la seule étape restante est la préparation de publication minimale `pre.012`.
|
||||
1172
prompts/022-V0_3_3_START_PROMPT.md
Normal file
1172
prompts/022-V0_3_3_START_PROMPT.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user