80 Commits

Author SHA1 Message Date
8c65096f4c v0.2.5-rel.001 2026-08-20 12:31:08 +02:00
cb142b1cb8 v0.2.5-pre.010.fix.003 2026-08-20 12:15:36 +02:00
08c8046262 v0.2.5-pre.010.fix.002 2026-08-20 11:53:55 +02:00
c0b131bf6f v0.2.5-pre.010.fix.001 2026-08-20 11:17:12 +02:00
5bf9651038 v0.2.5-pre.010 2026-08-20 09:58:14 +02:00
e91bf36e9e v0.2.5-pre.009 2026-08-20 09:24:10 +02:00
1125ade4c1 v0.2.5-pre.008 2026-08-19 18:47:06 +02:00
518cc9257f v0.2.5-pre.007-fix.001 2026-08-19 18:12:31 +02:00
46afe10423 v0.2.5-pre.007 2026-08-19 18:08:01 +02:00
25edcc231e v0.2.5-pre.006-fix.001 2026-08-19 17:35:14 +02:00
6094d33120 v0.2.5-pre.006 2026-08-19 17:31:29 +02:00
dcd23abb92 v0.2.5-pre.005-fix.001 2026-08-19 13:15:09 +02:00
36b98e0abc v0.2.5-pre.005 2026-08-19 13:09:16 +02:00
3c069347b7 v0.2.5-pre.004-fix.002 2026-08-19 11:47:32 +02:00
a0a5595186 v0.2.5-pre.004-fix.001 2026-08-19 11:37:36 +02:00
6c1172efc3 v0.2.5-pre.004 2026-08-19 11:29:05 +02:00
d5536b0778 v0.2.5-pre.003-fix.001 2026-08-19 11:24:32 +02:00
c6794af05b v0.2.5-pre.003 2026-08-19 10:52:21 +02:00
bcda6db11f v0.2.5-pre.002-fix.002 2026-08-19 10:08:47 +02:00
51b5114ae2 v0.2.5-pre.002-fix.001 2026-08-19 10:03:37 +02:00
c97a8739ab v0.2.5-pre.002 2026-08-19 09:56:01 +02:00
c2ccef3849 v0.2.5-pre.001-fix.001 2026-08-19 09:37:07 +02:00
58134c81d1 v0.2.5-pre.001 2026-08-19 09:11:36 +02:00
5b3967327a v0.2.4-rel.001 2026-08-18 23:51:56 +02:00
804b92b957 v0.2.4-pre.009-fix.001 2026-08-18 23:26:17 +02:00
e6772c109d v0.2.4-pre.009 2026-08-18 22:43:09 +02:00
7e84522787 v0.2.4-pre.008-fix.001 2026-08-18 21:53:33 +02:00
37461ec248 v0.2.4-pre.008 2026-08-18 21:52:00 +02:00
4252a21140 v0.2.4-pre.007 2026-08-18 21:33:21 +02:00
7a889dd466 v0.2.4-pre.006 2026-08-18 21:21:02 +02:00
d3cc7c93f0 v0.2.4-pre.005 2026-08-18 21:03:00 +02:00
561b6678ed v0.2.4-pre.004 2026-08-18 20:54:16 +02:00
e500024c3a v0.2.4-pre.003 2026-08-18 20:51:52 +02:00
033800f897 v0.2.4-pre.002-fix.001 2026-08-18 20:21:01 +02:00
cf8d79567d v0.2.4-pre.002 2026-08-18 20:12:46 +02:00
cb19742cf7 v0.2.4-pre.001-fix.001 2026-08-18 19:51:00 +02:00
c03a3eaf5d v0.2.4-pre.001 2026-08-18 19:36:57 +02:00
f7acac3fc8 v0.2.3-rel.001 2026-08-18 17:42:12 +02:00
039b93cfb3 v0.2.3-pre.009 2026-08-18 17:33:46 +02:00
c9a0f7fd37 v0.2.3-pre.008 2026-08-18 16:47:54 +02:00
704110d509 v0.2.3-pre.007 2026-08-18 16:12:07 +02:00
fb32ed03f8 v0.2.3-pre.006-fix.001 2026-08-18 12:40:24 +02:00
4439589f0d v0.2.3-pre.006 2026-08-18 12:35:29 +02:00
1e1bc0d421 v0.2.3-pre.005 2026-08-18 12:33:16 +02:00
bd26d4f636 v0.2.3-pre.004 2026-08-18 12:14:59 +02:00
2a7f3e8f40 v0.2.3-pre.003 2026-08-18 11:35:51 +02:00
1d37f04d63 v0.2.3-pre.002-fix.001 2026-08-18 11:27:24 +02:00
1ccf11bcc1 v0.2.3-pre.002 2026-08-18 11:13:04 +02:00
d06284fca2 v0.2.3-pre.001 2026-08-18 10:47:19 +02:00
fff69ead9c v0.2.2-rel.001 2026-08-18 10:14:43 +02:00
c93a833bac v0.2.2-pre.007-fix.002 2026-08-18 10:04:18 +02:00
05e0e96083 v0.2.2-pre.007-fix.001 2026-08-18 09:56:55 +02:00
b57f796187 v0.2.2-pre.007 2026-08-18 09:48:11 +02:00
ecfb9500eb v0.2.2-pre.006-fix.001 2026-08-18 09:37:03 +02:00
e14ce1c36f v0.2.2-pre.006 2026-08-18 08:27:42 +02:00
83cb861e54 v0.2.2-pre.005 2026-08-18 08:17:10 +02:00
1d1bc6a4d6 v0.2.2-pre.004 2026-08-18 08:09:20 +02:00
c4636ac8b9 v0.2.2-pre.003-fix.001 2026-08-18 07:53:17 +02:00
f15448ff6e v0.2.2-pre.003 2026-08-18 07:31:07 +02:00
bec1f2ec08 v0.2.2-pre.002-fix.002 2026-08-18 07:20:44 +02:00
f625ee5979 v0.2.2-pre.002-fix.001 2026-08-18 07:17:49 +02:00
e68f073505 v0.2.2-pre.002 2026-08-18 07:15:59 +02:00
bffb4f9a31 v0.2.2-pre.001-fix.001 2026-08-18 06:46:59 +02:00
9059a2dc45 v0.2.2-pre.001 2026-08-18 06:41:41 +02:00
6c3ecf1f18 v0.2.1-rel.001 2026-08-17 22:50:56 +02:00
79b67f8eae v0.2.1-pre.007 2026-08-17 22:43:07 +02:00
e0a7ac0bf8 v0.2.1-pre.006-fix.002 2026-08-17 21:41:20 +02:00
598474438b v0.2.1-pre.006-fix.001 2026-08-17 21:16:26 +02:00
f5d98c4e69 v0.2.1-pre.006 2026-08-17 21:03:51 +02:00
14bcbf2cfb v0.2.1-pre.005-fix.001 2026-08-17 20:40:08 +02:00
ac1b1033c4 v0.2.1-pre.005 2026-08-17 20:27:44 +02:00
bc71fba289 v0.2.1-pre.004-fix.002 2026-08-17 19:47:24 +02:00
9c1568ee1c v0.2.1-pre.004-fix.001 2026-08-17 19:40:12 +02:00
c1cea6e813 v0.2.1-pre.004 2026-08-17 19:26:26 +02:00
ed978179d8 v0.2.1-pre.003-fix.001 2026-08-17 18:59:01 +02:00
babe7d9f2b v0.2.1-pre.003 2026-08-17 18:55:14 +02:00
d3fc0c6d69 v0.2.1-pre.002-fix.001 2026-08-17 18:46:52 +02:00
0cff0406ab v0.2.1-pre.002 2026-08-17 18:02:18 +02:00
d98d152f08 v0.2.1-pre.001-fix.001 2026-08-17 17:36:35 +02:00
624202c363 0.2.1-pre.1 2026-08-17 17:20:41 +02:00
414 changed files with 56843 additions and 946 deletions

View File

@@ -1,10 +1,22 @@
# file: .env.example # file: .env.example
# version: 2 # version: 3
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths. # 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. # The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
KSP_LOGS_DIRECTORY=logs KSP_LOGS_DIRECTORY=logs
# Optional public Solana Devnet HTTP endpoint override used by config/std.transport.json.
# The committed Transport document falls back to https://api.devnet.solana.com when this variable is absent.
KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com
# Optional public Solana Mainnet HTTP endpoint override used by config/std.transport.json and its example.
# The committed Transport document falls back to https://api.mainnet-beta.solana.com when this variable is absent.
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet-beta.solana.com
# Optional complete private-provider HTTP endpoint URL used only by the Transport example when explicitly selected.
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
# KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me
# Minimum time in milliseconds that a KSP desk splash remains visible after its frontend is ready. # Minimum time in milliseconds that a KSP desk splash remains visible after its frontend is ready.
KSP_DESK_SPLASH_MINIMUM_MS=1200 KSP_DESK_SPLASH_MINIMUM_MS=1200

5
.pydevproject Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>
</pydev_project>

View File

@@ -1,10 +1,32 @@
<!-- file: CHANGELOG.md --> <!-- file: CHANGELOG.md -->
<!-- version: 4 --> <!-- version: 9 -->
# Changelog KSP # Changelog KSP
## 0.2.5 — Wallet foundation — 2026-08-20
`0.2.5` publie `ksp-wallet-lib` et le format natif interopérable `.kspwallet` V1. La release stabilise les capacités cryptographiquement indépendantes VIEW/OWNER, le masquage de la Pubkey/alias/notes à létat verrouillé, Argon2id v19 avec paramètres sérialisés et profil de création `65 536 KiB / 3 / 1`, XChaCha20-Poly1305, une autorité Ed25519 OWNER distincte de la keypair Solana, les compartiments `owner_control`/`metadata`/`secret`, la création et louverture mémoire/fichier, la persistence no-clobber, la signature Solana OWNER sans getter secret, ladministration alias/notes, les rotations OWNER/VIEW, la révocation forte VIEW avec rekey metadata, la détection de handles stale et les adapters Solana CLI JSON/Base58 pour inspection/import/export. Le wire V1 est documenté indépendamment de Rust dans `docs/formats/KSPWALLET_V1.md` et couvert par des vecteurs publics ainsi que des canaris adversariaux et une reproduction externe. La release conserve Wallet indépendant de Config/Transport/ExecutionPolicy/Store/Tauri et expose la Pubkey via `ksp-core-lib` tandis que `solana-keypair` reste encapsulée dans Wallet.
La clôture technique `pre.010-fix.001` à `fix.003` met la dépendance directe KSP à `ed25519-dalek 3.0.0`, accepte la génération `2.2.0` transitive encore imposée par `solana-keypair 3.1.2`, normalise les sources Rust du workspace et introduit `python3 scripts/audit_rust_workspace_rules.py` comme gate structurel complémentaire à rustfmt/Clippy. Le checkpoint final `pre.010-fix.003` est validé avec audit Python clean, `cargo check`, Clippy, tests Wallet et workspace complet verts. Le prompt `prompts/011-V0_2_6_START_PROMPT.md` ouvre ensuite `0.2.6 — Wallet Desk` par audit/sizing avant implémentation.
## 0.2.4 — HTTP Blocks + Economics + compliance HTTP finale — 2026-08-18
`0.2.4` achève la surface HTTP Solana standard de `ksp-onchain-transport-lib` : les 10 wrappers Blocks et 5 wrappers Economics portent la couverture typée à **52/52 méthodes HTTP courantes**, tandis que les **14/14 méthodes historiques Deprecated/runtime Removed** restent conservées pour compliance. La release stabilise notamment `getBlock` moderne avec sa forme bare encoding legacy dépréciée, les quatre variantes `transactionDetails`, les versions transaction numériques génériques, `numRewardPartitions` (SIMD-0118), `commissionBps` (SIMD-0291), les overloads de ranges/performance, `getBlockProduction`, les valeurs Economics fournies par le runtime et `getInflationReward` positionnel sans plafond d'adresses inventé. Le réaudit final confirme l'égalité exacte entre l'inventaire HTTP officiel courant et le registre KSP, applique `KSP-TRANSPORT-007` aux **52/52 wrappers**, et ne matérialise aucune extension spéculative issue de la watchlist SIMD. Les canaries de frontières, le workspace complet et les deux smokes Devnet — Transport pur couvrant Accounts/Tokens/Cluster/Transactions/Blocks/Economics et composition Config -> Transport transitoire — sont validés avant publication. Le prompt `prompts/010-V0_2_5_START_PROMPT.md`, enrichi par `pre.009-fix.001`, ouvre `0.2.5 — Wallet foundation` avec threat model, interopérabilité `.kspwallet` et capacités indépendantes VIEW/OWNER.
Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`. Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`.
## 0.2.3 — HTTP Transactions — 2026-08-18
`0.2.3` complète la famille HTTP Transactions de `ksp-onchain-transport-lib` avec 11 wrappers typés, portant la surface HTTP typée courante à 37 méthodes : 4 foundation, 22 Accounts/Tokens/Cluster et 11 Transactions. La release stabilise la classification exacte `8 Read / RetrySafe`, `2 WriteSubmission / NeverAfterDispatch` (`requestAirdrop`, `sendTransaction`) et `1 Simulation / RetrySafe` (`simulateTransaction`), avec preuves end-to-end qu'une write submission n'est jamais resoumise après dispatch ambigu mais peut retenter lorsqu'un échec est prouvé `NotDispatched`. `getTransaction` couvre la config moderne complète et la forme bare encoding legacy dépréciée ; les variantes de transaction, metadata, version et `transactionIndex` restent préservées losslessly. `simulateTransaction` couvre sa config complète, ses invariants déterministes et son résultat riche sans introduire de client RPC Solana haut niveau ni de décodage transactionnel dans Transport. La release formalise `KSP-TRANSPORT-007` et réaudite rétroactivement les 37 wrappers courants sans remédiation fonctionnelle. Les canaries conservent 52 méthodes HTTP courantes, 14 historiques Deprecated/Removed et 15 Blocks/Economics réservées à `0.2.4`. Les graphes Cargo et les deux smokes Devnet — Transport pur read-only et composition Config -> Transport transitoire — ont été validés avant publication. Le prompt `prompts/009-V0_2_4_START_PROMPT.md` ouvre `0.2.4 — HTTP Blocks + Economics + compliance HTTP finale`.
## 0.2.2 — HTTP Accounts + Tokens + Cluster — 2026-08-18
`0.2.2` complète la surface HTTP typée de `ksp-onchain-transport-lib` avec 22 wrappers supplémentaires : 5 Accounts, 5 Tokens et 12 Cluster, portant la couverture typée totale à 26 méthodes avec les quatre canaris foundation de `0.2.1`. La release stabilise les DTOs/configs wire communs, préserve les formes Account encodées/`jsonParsed`, les `null` et champs optionnels, les selectors Token exclusifs, les contextes RPC, les structures Cluster/leader/vote et les limites locales auditées (`getMultipleAccounts <= 100`, `getProgramAccounts <= 4` filtres, `memcmp` raw <= 128 octets, `getSlotLeaders` entre 1 et 5000). Les 22 descriptors restent `Read / RetrySafe` et passent exclusivement par le flux central `descriptor -> execute_standard_rpc -> pool/admission -> executor HTTP`. La release n'ajoute aucune dépendance externe, ne crée aucun client HTTP parallèle et conserve le firewall Transport -> Config/Store/Program. Elle ajoute un smoke Devnet pur à Transport avec settings programmatiques couvrant Accounts/Tokens/Cluster, conserve séparément le smoke historique Config -> Transport comme exception transitoire, et valide les deux explicitement avant publication. Les canaries figent toujours 52 méthodes HTTP courantes, 14 historiques Deprecated/Removed et la partition typed `4 / 22 / 11 / 15`. Le prompt `prompts/008-V0_2_3_START_PROMPT.md` ouvre `0.2.3 — HTTP Transactions`.
## 0.2.1 — HTTP Solana foundation — 2026-08-17
`0.2.1` stabilise `ksp-onchain-transport-lib` comme foundation HTTP JSON-RPC Solana provider-neutral : settings publics, endpoints/pool/rôles, priorités et fairness, RPS/burst/concurrence/cooldown, deadline commune, retry/backoff, classification no-resend après dispatch ambigu, snapshots sûrs et exécution HTTP réelle via `reqwest`/rustls. La release fige un registre audité de 52 méthodes HTTP courantes et 14 méthodes historiques Deprecated/Removed, avec quatre wrappers typés canari (`getBalance`, `getGenesisHash`, `getHealth`, `getVersion`) et une partition explicite des 48 méthodes restantes sur `0.2.2``0.2.4`. Elle ajoute `std.transport`, son schema/exemple et l'adapter `ksp-config-lib -> ksp-onchain-transport-lib`, sans dépendance inverse, ainsi que la redaction des URLs/provider credentials, la neutralisation des URLs contenues dans les `reqwest::Error`, un sink Logging Transport dédié à `info`, des fixtures HTTP déterministes, des canaries de complétude et un smoke Devnet opt-in validant la composition Config -> Transport. Le smoke cross-crates reste temporairement hébergé dans Config et doit migrer vers une future surface d'intégration/orchestration ; ce placement n'est pas un modèle pour les futurs smokes `Config + autre crate`. Le prompt `prompts/007-V0_2_2_START_PROMPT.md` ouvre `0.2.2 — HTTP Accounts + Tokens + Cluster`.
## 0.2.0 — Audit bot3 et planification de la série `0.2.x` — 2026-08-17 ## 0.2.0 — Audit bot3 et planification de la série `0.2.x` — 2026-08-17
`0.2.0` stabilise le cadrage de la prochaine phase fonctionnelle de KSP après audit de `khadhroony-bot3`. La release fixe l'ordre `0.2.1+` autour du transport HTTP Solana, du Wallet `.kspwallet`, de Wallet Desk, des transports WebSocket/LaserStream/Yellowstone, du transport off-chain de prix, de `ksp-interface-lib` et de `ksp-program-api`; elle impose la couverture exhaustive des surfaces Transport documentées avec warnings KSP pour les opérations deprecated/obsolete encore fonctionnelles et unstable/experimental. Elle stabilise également la progression durable `RAW -> CORE -> DECODE -> SPECIALIZED`, RAW/CORE sans décodage Program, puis des vertical slices complets par groupe à partir de DECODE, avec priorité Solana Core, SPL token/trading, metadata token, Anchor, Meteora/Raydium/Pump/Orca, routing et Market Desk progressive. Le prompt `prompts/006-V0_2_1_START_PROMPT.md` ouvre `0.2.1 — ksp-onchain-transport-lib / HTTP Solana foundation` avec un gate de sizing imposant qu'une release concrète reste clôturable dans une seule session. `0.2.0` stabilise le cadrage de la prochaine phase fonctionnelle de KSP après audit de `khadhroony-bot3`. La release fixe l'ordre `0.2.1+` autour du transport HTTP Solana, du Wallet `.kspwallet`, de Wallet Desk, des transports WebSocket/LaserStream/Yellowstone, du transport off-chain de prix, de `ksp-interface-lib` et de `ksp-program-api`; elle impose la couverture exhaustive des surfaces Transport documentées avec warnings KSP pour les opérations deprecated/obsolete encore fonctionnelles et unstable/experimental. Elle stabilise également la progression durable `RAW -> CORE -> DECODE -> SPECIALIZED`, RAW/CORE sans décodage Program, puis des vertical slices complets par groupe à partir de DECODE, avec priorité Solana Core, SPL token/trading, metadata token, Anchor, Meteora/Raydium/Pump/Orca, routing et Market Desk progressive. Le prompt `prompts/006-V0_2_1_START_PROMPT.md` ouvre `0.2.1 — ksp-onchain-transport-lib / HTTP Solana foundation` avec un gate de sizing imposant qu'une release concrète reste clôturable dans une seule session.

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 95 # version: 163
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"] members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
[workspace.package] [workspace.package]
version = "0.2.0" version = "0.2.5"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
@@ -14,20 +14,29 @@ authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
publish = false publish = false
[workspace.dependencies] [workspace.dependencies]
argon2 = { version = "^0.5", default-features = false }
chacha20poly1305 = { version = "^0.11", default-features = false }
ed25519-dalek = { version = "^3.0", default-features = false }
getrandom = { version = "^0.4", default-features = false }
base64 = { version = "^0.23" }
fs2 = { version = "^0.4" } fs2 = { version = "^0.4" }
serde = { version = "^1.0", features = ["derive"] } serde = { version = "^1.0" }
serde_json = { version = "^1.0" } serde_json = { version = "^1.0" }
jsonschema = { version = "^0.49", default-features = false } jsonschema = { version = "^0.49", default-features = false }
reqwest = { version = "^0.13", default-features = false }
solana-keypair = { version = "^3.1", default-features = false }
solana-pubkey = { version = "^4.3", default-features = false } solana-pubkey = { version = "^4.3", default-features = false }
tracing = { version = "^0.1", default-features = false, features = ["std"] } tracing = { version = "^0.1", default-features = false }
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt", "json", "ansi"] } tracing-subscriber = { version = "^0.3", default-features = false }
tracing-appender = { version = "^0.2", default-features = false } tracing-appender = { version = "^0.2", default-features = false }
tokio = { version = "^1.53", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time"] } tokio = { version = "^1.53", default-features = false }
chrono = { version = "^0.4", default-features = false, features = ["std", "now"] } tempfile = { version = "^3.27" }
chrono = { version = "^0.4", default-features = false }
tauri = { version = "^2.11" } tauri = { version = "^2.11" }
tauri-build = { version = "^2.6" } tauri-build = { version = "^2.6" }
tauri-plugin-tracing = { version = "^0.3" } tauri-plugin-tracing = { version = "^0.3" }
ts-rs = { version = "^12.0" } ts-rs = { version = "^12.0" }
zeroize = { version = "^1.9" }
[workspace.lints.rust] [workspace.lints.rust]
missing_docs = "warn" missing_docs = "warn"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 24 --> <!-- version: 55 -->
# Roadmap KSP # Roadmap KSP
@@ -17,6 +17,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
- Une prerelease vise environ **15 à 20 minutes de travail effectif**. - Une prerelease vise environ **15 à 20 minutes de travail effectif**.
- Une release concrète doit être dimensionnée pour pouvoir être ouverte et clôturée dans **une seule session de chat**. - Une release concrète doit être dimensionnée pour pouvoir être ouverte et clôturée dans **une seule session de chat**.
- Cette règle fixe une durée maximale par release, pas une obligation de changer de session après chaque release : si une release est clôturée plus vite que prévu, la même session peut ouvrir puis clôturer la release suivante si son propre sizing reste positif et si toutes les frontières version/delta/validation sont conservées.
- Si `pre.001` révèle qu'une release est trop grosse, elle est scindée avant implémentation fonctionnelle lourde. - Si `pre.001` révèle qu'une release est trop grosse, elle est scindée avant implémentation fonctionnelle lourde.
- À partir des couches Program/Decode, KSP progresse verticalement groupe par groupe plutôt que par grandes vagues horizontales de decoders/materializers/executors séparés. - À partir des couches Program/Decode, KSP progresse verticalement groupe par groupe plutôt que par grandes vagues horizontales de decoders/materializers/executors séparés.
@@ -39,24 +40,24 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
### Cadrage ### Cadrage
- [X] `0.2.0` — Audit bot3, ordre de `0.2.x`, architecture durable et prompt `0.2.1` stabilisés par `0.2.0-rel.001`. - [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/CORE/DECODE/SPECIALIZED stabilisés.
- [X] `0.2.0-pre.001` — Méthode d'audit, cartographie initiale et matrice provisoire. - [X] `0.2.1` — HTTP foundation stable : matrice 52+14, runtime/routing/résilience/exécution HTTP, 4 canaris typés, `std.transport`, adapter Config -> Transport, canaries de clôture, smoke Devnet opt-in et documentation durable validés ; les 48 wrappers typés restants sont reportés à `0.2.2``0.2.4`.
- [X] `0.2.0-pre.002` — Fixer l'ordre fonctionnel, la discipline de sizing, le pipeline RAW/CORE/DECODE/SPECIALIZED et préparer le prompt `0.2.1`.
- [X] `0.2.0-pre.003` — Audit de cohérence final : règles résiduelles supersédées corrigées, fiches `0.2.1+` complétées, TODO bot3 utiles préservés et prompt `0.2.1` finalisé.
- [X] `0.2.0-rel.001` — Publication stable du cadrage `0.2.x`; prochaine release : `0.2.1 — ksp-onchain-transport-lib / HTTP Solana foundation`.
### Releases fonctionnelles décidées/pressenties ### Releases fonctionnelles décidées/pressenties
- [ ] `0.2.1`Introduire `ksp-onchain-transport-lib` avec HTTP Solana/JSON-RPC, settings publics, document Config standard + adapter, pools, rôles, priorités, limites, retry/backoff et couverture complète de la documentation HTTP ciblée. - [X] `0.2.1`**HTTP transport foundation réduite par le gate `pre.001`** : crate/settings/JSON-RPC/registry 52 current + 14 deprecated historiques, pool/rôles/limites/retry, Config adapter, documentation et 4 méthodes typées canari (`getBalance`, `getGenesisHash`, `getHealth`, `getVersion`) publiés stables.
- [ ] `0.2.2`Introduire `ksp-wallet-lib`, le format `.kspwallet`, la gestion sûre des secrets et une architecture d'import/export extensible ; exclure `WalletPolicy`. - [X] `0.2.2`HTTP Accounts + Tokens + Cluster : 22 wrappers typés (5 Accounts + 5 Tokens + 12 Cluster), canaries de complétude 52+14, smoke Devnet Transport pur et smoke historique Config -> Transport validés, documentation durable et prompt `0.2.3` publiés stables.
- [ ] `0.2.3`Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet. - [X] `0.2.3`HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
- [ ] `0.2.4`Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe. - [X] `0.2.4`HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
- [ ] `0.2.5`Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client. - [X] `0.2.5`Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001``fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe laudit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
- [ ] `0.2.6`Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle. - [ ] `0.2.6`Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet ; le prompt de démarrage est préparé par `0.2.5-pre.010`.
- [ ] `0.2.7`Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR. - [ ] `0.2.7`Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe.
- [ ] `0.2.8`Introduire une petite application desk de visualisation des prix. - [ ] `0.2.8`Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
- [ ] `0.2.9`Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes. - [ ] `0.2.9`Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.
- [ ] `0.2.10` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet. - [ ] `0.2.10` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR.
- [ ] `0.2.11` — Introduire une petite application desk de visualisation/validation des prix offchain, puis intégrer cette capacité dans `ksp-app-wallet-desk` sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé.
- [ ] `0.2.12` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes.
- [ ] `0.2.13` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.
### Règles Transport pour toute la série ### Règles Transport pour toute la série

View File

@@ -0,0 +1,64 @@
{
"format_version": 1,
"retry": {
"max_retries": 3,
"initial_backoff_ms": 150,
"max_backoff_ms": 3000
},
"default_profile": "mainnet_mixed",
"profiles": [
{
"profile_id": "mainnet_mixed",
"endpoints": [
{
"name": "mainnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet-beta",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"priority": 200,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
},
{
"name": "mainnet_private",
"enabled": true,
"provider": "private-provider",
"cluster": "mainnet-beta",
"url": "${KSP_SECRET_SOLANA_HTTP_URL:-https://example.invalid}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 10000,
"max_idle_connections_per_host": 16,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"priority": 100,
"limits": {
"requests_per_second": 20,
"burst_capacity": 40,
"max_concurrent_requests": 16,
"pause_after_rate_limit_ms": 750
}
}
]
}
]
}
]
}

View File

@@ -0,0 +1,124 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:ksp:schema:std.transport:v1",
"title": "KSP standard HTTP Transport configuration",
"type": "object",
"additionalProperties": false,
"required": ["format_version", "retry", "default_profile", "profiles"],
"properties": {
"format_version": {"const": 1},
"retry": {"$ref": "#/$defs/retry"},
"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._-]*$"
},
"descriptor": {
"type": "string",
"minLength": 1,
"pattern": "^\\S(?:.*\\S)?$"
},
"positiveMs": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295
},
"positiveU32": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295
},
"retry": {
"type": "object",
"additionalProperties": false,
"required": ["max_retries", "initial_backoff_ms", "max_backoff_ms"],
"properties": {
"max_retries": {"type": "integer", "minimum": 0, "maximum": 100},
"initial_backoff_ms": {"$ref": "#/$defs/positiveMs"},
"max_backoff_ms": {"$ref": "#/$defs/positiveMs"}
}
},
"limits": {
"type": "object",
"additionalProperties": false,
"properties": {
"requests_per_second": {"$ref": "#/$defs/positiveU32"},
"burst_capacity": {"$ref": "#/$defs/positiveU32"},
"max_concurrent_requests": {"$ref": "#/$defs/positiveU32"},
"pause_after_rate_limit_ms": {"$ref": "#/$defs/positiveMs"}
}
},
"role": {
"type": "object",
"additionalProperties": false,
"required": ["role", "enabled", "request_kinds", "priority", "limits"],
"properties": {
"role": {"$ref": "#/$defs/descriptor"},
"enabled": {"type": "boolean"},
"request_kinds": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {"$ref": "#/$defs/descriptor"},
"allOf": [
{
"if": {"contains": {"const": "*"}},
"then": {"maxItems": 1}
}
]
},
"priority": {"type": "integer", "minimum": 0, "maximum": 4294967295},
"limits": {"$ref": "#/$defs/limits"}
}
},
"endpoint": {
"type": "object",
"additionalProperties": false,
"required": [
"name",
"enabled",
"provider",
"cluster",
"url",
"connect_timeout_ms",
"request_timeout_ms",
"roles"
],
"properties": {
"name": {"$ref": "#/$defs/descriptor"},
"enabled": {"type": "boolean"},
"provider": {"$ref": "#/$defs/descriptor"},
"cluster": {"$ref": "#/$defs/descriptor"},
"url": {"type": "string", "minLength": 1},
"connect_timeout_ms": {"$ref": "#/$defs/positiveMs"},
"request_timeout_ms": {"$ref": "#/$defs/positiveMs"},
"max_idle_connections_per_host": {"type": "integer", "minimum": 1},
"roles": {
"type": "array",
"minItems": 1,
"items": {"$ref": "#/$defs/role"}
}
}
},
"profile": {
"type": "object",
"additionalProperties": false,
"required": ["profile_id", "endpoints"],
"properties": {
"profile_id": {"$ref": "#/$defs/profileId"},
"endpoints": {
"type": "array",
"minItems": 1,
"items": {"$ref": "#/$defs/endpoint"}
}
}
}
}
}

View File

@@ -40,6 +40,23 @@
] ]
} }
}, },
{
"output_id": "file.onchain_transport.info",
"enabled": true,
"path": "transport/onchain/ksp-onchain-transport.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "info",
"targets": [
"ksp-onchain-transport-lib"
],
"domains": [
"*"
]
}
},
{ {
"output_id": "file.config.error", "output_id": "file.config.error",
"enabled": true, "enabled": true,
@@ -70,6 +87,10 @@
{ {
"target_prefix": "ksp-app-config-desk", "target_prefix": "ksp-app-config-desk",
"level": "info" "level": "info"
},
{
"target_prefix": "ksp-onchain-transport-lib",
"level": "info"
} }
] ]
} }

69
config/std.transport.json Normal file
View File

@@ -0,0 +1,69 @@
{
"format_version": 1,
"retry": {
"max_retries": 2,
"initial_backoff_ms": 100,
"max_backoff_ms": 2000
},
"default_profile": "devnet_public",
"profiles": [
{
"profile_id": "devnet_public",
"endpoints": [
{
"name": "solana_devnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "devnet",
"url": "${KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL:-https://api.devnet.solana.com}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
}
]
},
{
"profile_id": "mainnet_public",
"endpoints": [
{
"name": "solana_mainnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet-beta",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
}
]
}
]
}

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-app-config-desk/Cargo.toml # file: crates/ksp-app-config-desk/Cargo.toml
# version: 7 # version: 8
[package] [package]
name = "ksp-app-config-desk" name = "ksp-app-config-desk"
@@ -26,12 +26,12 @@ fs2.workspace = true
ksp-config-lib = { path = "../ksp-config-lib" } ksp-config-lib = { path = "../ksp-config-lib" }
ksp-core-lib = { path = "../ksp-core-lib" } ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" } ksp-logging-lib = { path = "../ksp-logging-lib" }
serde.workspace = true serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
tauri.workspace = true tauri.workspace = true
tauri-plugin-tracing.workspace = true tauri-plugin-tracing.workspace = true
chrono.workspace = true chrono = { workspace = true, features = ["std", "now"] }
tokio.workspace = true tokio = { workspace = true, features = ["time"] }
ts-rs.workspace = true ts-rs.workspace = true
[lints] [lints]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/sass/_bootswatch.scss // file: crates/ksp-app-config-desk/frontend/sass/_bootswatch.scss
// version: 1 // version: 2
// Pulse 5.3.8 // Pulse 5.3.8
// Bootswatch // Bootswatch

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/sass/_fontawesome.scss // file: crates/ksp-app-config-desk/frontend/sass/_fontawesome.scss
// version: 1 // version: 2
//@use '@fortawesome/fontawesome-free/scss/variables' with ( //@use '@fortawesome/fontawesome-free/scss/variables' with (
// // customizing $font-path - make sure it points to where your webfonts are stored in your project // // customizing $font-path - make sure it points to where your webfonts are stored in your project

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/sass/_simplebar.scss // file: crates/ksp-app-config-desk/frontend/sass/_simplebar.scss
// version: 1 // version: 2
/* Rtl support */ /* Rtl support */
[data-simplebar] { [data-simplebar] {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/sass/_variables.scss // file: crates/ksp-app-config-desk/frontend/sass/_variables.scss
// version: 1 // version: 2
// Pulse 5.3.8 // Pulse 5.3.8
// Bootswatch // Bootswatch

View File

@@ -1,17 +1,8 @@
// file: crates/ksp-app-config-desk/src/app_state.rs // file: crates/ksp-app-config-desk/src/app_state.rs
// version: 6 // version: 8
//! Shared backend state owned by the Tauri application. //! Shared backend state owned by the Tauri application.
struct LoggingRuntimeState {
guard: ksp_logging_lib::LoggingGuard,
active_profile_id: std::option::Option<String>,
selection_source: String,
generation: u32,
fallback_active: bool,
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}
/// Shared Config Desk application state managed by Tauri. /// Shared Config Desk application state managed by Tauri.
pub(crate) struct AppState { pub(crate) struct AppState {
config_management: ksp_config_lib::ConfigManagement, config_management: ksp_config_lib::ConfigManagement,
@@ -28,7 +19,7 @@ impl AppState {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let runtime_identity = crate::logging_runtime::launch_identity(); let runtime_identity = crate::launch_identity();
let runtime_identity = match runtime_identity { let runtime_identity = match runtime_identity {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -175,7 +166,7 @@ impl AppState {
let dropped = runtime.guard.dropped_lines(); let dropped = runtime.guard.dropped_lines();
let mut files = std::vec::Vec::<crate::LoggingRuntimeFileDto>::new(); let mut files = std::vec::Vec::<crate::LoggingRuntimeFileDto>::new();
for metadata in runtime.guard.active_file_outputs() { for metadata in runtime.guard.active_file_outputs() {
files.push(crate::logging_runtime::project_file(&metadata)); files.push(crate::project_logging_file(&metadata));
} }
return std::result::Result::Ok(crate::LoggingRuntimeStatusDto { return std::result::Result::Ok(crate::LoggingRuntimeStatusDto {
active_profile: runtime.active_profile_id.clone(), active_profile: runtime.active_profile_id.clone(),
@@ -185,9 +176,9 @@ impl AppState {
application_id: identity.application_id().to_owned(), application_id: identity.application_id().to_owned(),
launch_timestamp: identity.launch_timestamp().to_owned(), launch_timestamp: identity.launch_timestamp().to_owned(),
console_enabled, console_enabled,
dropped_console_lines: crate::logging_runtime::count_to_u64(dropped.console()), dropped_console_lines: crate::count_to_u64(dropped.console()),
dropped_file_lines: crate::logging_runtime::count_to_u64(dropped.file()), dropped_file_lines: crate::count_to_u64(dropped.file()),
dropped_total_lines: crate::logging_runtime::count_to_u64(dropped.total()), dropped_total_lines: crate::count_to_u64(dropped.total()),
files, files,
}); });
} }
@@ -206,3 +197,12 @@ impl AppState {
.is_ok(); .is_ok();
} }
} }
struct LoggingRuntimeState {
guard: ksp_logging_lib::LoggingGuard,
active_profile_id: std::option::Option<String>,
selection_source: String,
generation: u32,
fallback_active: bool,
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}

View File

@@ -1,13 +1,19 @@
// file: crates/ksp-app-config-desk/src/bootstrap.rs // file: crates/ksp-app-config-desk/src/bootstrap.rs
// version: 3 // version: 6
//! Config and Logging bootstrap for the desktop application. //! Config and Logging bootstrap for the desktop application.
/// Crate-internal `LoggingStartup` state shared across the owning crate.
pub(crate) struct LoggingStartup { pub(crate) struct LoggingStartup {
/// Stores the guard value for this state.
pub(crate) guard: ksp_logging_lib::LoggingGuard, pub(crate) guard: ksp_logging_lib::LoggingGuard,
/// Stores the active profile id value for this state.
pub(crate) active_profile_id: std::option::Option<String>, pub(crate) active_profile_id: std::option::Option<String>,
/// Stores the selection source value for this state.
pub(crate) selection_source: String, pub(crate) selection_source: String,
/// Stores the fallback active value for this state.
pub(crate) fallback_active: bool, pub(crate) fallback_active: bool,
/// Stores the startup diagnostic value for this state.
pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>, pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
} }
@@ -23,6 +29,7 @@ enum LoggingStartupPlan {
}, },
} }
/// Executes the crate-internal config management operation for the owning module.
pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> { pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments); let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
let bootstrap = match bootstrap { let bootstrap = match bootstrap {
@@ -38,10 +45,11 @@ pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_li
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine)); return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine));
} }
/// Executes the crate-internal initialize logging operation for the owning module.
pub(crate) fn initialize_logging( pub(crate) fn initialize_logging(
management: &ksp_config_lib::ConfigManagement, management: &ksp_config_lib::ConfigManagement,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> { ) -> ksp_core_lib::Result<crate::LoggingStartup> {
let plan = resolve_logging_startup(management); let plan = resolve_logging_startup(management);
return match plan { return match plan {
LoggingStartupPlan::Managed { active_profile_id, settings } => { LoggingStartupPlan::Managed { active_profile_id, settings } => {
@@ -71,6 +79,15 @@ pub(crate) fn initialize_logging(
}; };
} }
fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
return ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::vec::Vec::new(),
);
}
fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan { fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
let environment = ksp_config_lib::ConfigEnvironment::load(); let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment { let environment = match environment {
@@ -93,7 +110,7 @@ fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPl
fn initialize_fallback_logging( fn initialize_fallback_logging(
initial_error: ksp_core_lib::Error, initial_error: ksp_core_lib::Error,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> { ) -> ksp_core_lib::Result<crate::LoggingStartup> {
let diagnostic = crate::CommandErrorDto::from_error(&initial_error); let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity); return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity);
} }
@@ -103,7 +120,7 @@ fn initialize_planned_fallback_logging(
diagnostic: crate::CommandErrorDto, diagnostic: crate::CommandErrorDto,
settings: ksp_logging_lib::LoggingSettings, settings: ksp_logging_lib::LoggingSettings,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> { ) -> ksp_core_lib::Result<crate::LoggingStartup> {
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity); let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
let guard = match guard { let guard = match guard {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -132,15 +149,6 @@ fn initialize_planned_fallback_logging(
}); });
} }
pub(crate) fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
return ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::vec::Vec::new(),
);
}
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/bootstrap.rs"] #[path = "../unit_tests/bootstrap.rs"]
mod tests; mod tests;

View File

@@ -1,8 +1,28 @@
// file: crates/ksp-app-config-desk/src/constants.rs // file: crates/ksp-app-config-desk/src/constants.rs
// version: 9 // version: 10
//! Application-owned tracing targets and domains. //! Application-owned tracing targets and domains.
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "config.bootstrap";
/// Structured domain for Config document inventory, diagnostics and repair.
pub(crate) const TRACING_DOMAIN_DOCUMENTS: &str = "config.documents";
/// Structured domain for safe Config environment reports.
pub(crate) const TRACING_DOMAIN_ENVIRONMENT: &str = "config.environment";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain for typed Logging editor inspection.
pub(crate) const TRACING_DOMAIN_LOGGING_EDITOR: &str = "config.logging_editor";
/// Structured domain for active Logging runtime metadata and explicit profile application.
pub(crate) const TRACING_DOMAIN_LOGGING_RUNTIME: &str = "config.logging_runtime";
/// Structured domain used by controlled Logging test events.
pub(crate) const TRACING_DOMAIN_LOGGING_TEST: &str = "config.logging_test";
/// Structured domain for Config profile selection and provenance inspection.
pub(crate) const TRACING_DOMAIN_PROFILES: &str = "config.profiles";
/// Structured domain for explicit privileged Secret reveal operations.
pub(crate) const TRACING_DOMAIN_SECRETS: &str = "config.secrets";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Owning target for backend events emitted by Config Desk. /// Owning target for backend events emitted by Config Desk.
pub(crate) const TRACING_TARGET: &str = "ksp-app-config-desk"; pub(crate) const TRACING_TARGET: &str = "ksp-app-config-desk";
/// Owning target for generic frontend events emitted through the KSP bridge. /// Owning target for generic frontend events emitted through the KSP bridge.
@@ -13,23 +33,3 @@ pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-config-desk.front
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-config-desk.frontend.splash"; pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-config-desk.frontend.splash";
/// Dedicated target used by the controlled Logging test panel. /// Dedicated target used by the controlled Logging test panel.
pub(crate) const TRACING_TARGET_LOGGING_TEST: &str = "ksp-app-config-desk.logging-test"; pub(crate) const TRACING_TARGET_LOGGING_TEST: &str = "ksp-app-config-desk.logging-test";
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "config.bootstrap";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Structured domain for Config document inventory, diagnostics and repair.
pub(crate) const TRACING_DOMAIN_DOCUMENTS: &str = "config.documents";
/// Structured domain for Config profile selection and provenance inspection.
pub(crate) const TRACING_DOMAIN_PROFILES: &str = "config.profiles";
/// Structured domain for safe Config environment reports.
pub(crate) const TRACING_DOMAIN_ENVIRONMENT: &str = "config.environment";
/// Structured domain for explicit privileged Secret reveal operations.
pub(crate) const TRACING_DOMAIN_SECRETS: &str = "config.secrets";
/// Structured domain for typed Logging editor inspection.
pub(crate) const TRACING_DOMAIN_LOGGING_EDITOR: &str = "config.logging_editor";
/// Structured domain for active Logging runtime metadata and explicit profile application.
pub(crate) const TRACING_DOMAIN_LOGGING_RUNTIME: &str = "config.logging_runtime";
/// Structured domain used by controlled Logging test events.
pub(crate) const TRACING_DOMAIN_LOGGING_TEST: &str = "config.logging_test";

View File

@@ -1,19 +1,19 @@
// file: crates/ksp-app-config-desk/src/documents.rs // file: crates/ksp-app-config-desk/src/documents.rs
// version: 1 // version: 3
//! Generic Config document inventory, diagnostics and validated repair services. //! Generic Config document inventory, diagnostics and validated repair services.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
const STATUS_VALID: &str = "valid"; const STAGE_EFFECTIVE: &str = "effective";
const STATUS_INVALID: &str = "invalid";
const STAGE_VALID: &str = "valid";
const STAGE_READ: &str = "read";
const STAGE_JSON: &str = "json"; const STAGE_JSON: &str = "json";
const STAGE_OTHER: &str = "other";
const STAGE_READ: &str = "read";
const STAGE_SCHEMA: &str = "schema"; const STAGE_SCHEMA: &str = "schema";
const STAGE_SEMANTIC: &str = "semantic"; const STAGE_SEMANTIC: &str = "semantic";
const STAGE_EFFECTIVE: &str = "effective"; const STAGE_VALID: &str = "valid";
const STAGE_OTHER: &str = "other"; const STATUS_INVALID: &str = "invalid";
const STATUS_VALID: &str = "valid";
/// Safe inventory row for one Config document registered by `ksp-config-lib`. /// Safe inventory row for one Config document registered by `ksp-config-lib`.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
@@ -78,7 +78,7 @@ impl ConfigDocumentErrorDto {
} }
/// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state. /// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state.
pub(crate) fn inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocumentSummaryDto> { pub(crate) fn document_inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocumentSummaryDto> {
let management = state.config_management(); let management = state.config_management();
let engine = management.engine(); let engine = management.engine();
let mut documents = std::vec::Vec::new(); let mut documents = std::vec::Vec::new();
@@ -98,7 +98,7 @@ pub(crate) fn inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocument
} }
/// Loads one Config document detail, including raw source when Config can read it. /// Loads one Config document detail, including raw source when Config can read it.
pub(crate) fn detail(state: &crate::AppState, file_id_text: &str) -> std::result::Result<ConfigDocumentDetailDto, ConfigDocumentErrorDto> { pub(crate) fn document_detail(state: &crate::AppState, file_id_text: &str) -> std::result::Result<ConfigDocumentDetailDto, ConfigDocumentErrorDto> {
let file_id = parse_config_file_id(file_id_text); let file_id = parse_config_file_id(file_id_text);
let file_id = match file_id { let file_id = match file_id {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -133,7 +133,7 @@ pub(crate) fn detail(state: &crate::AppState, file_id_text: &str) -> std::result
} }
/// Validates and atomically persists one raw Config source candidate, then reloads the document detail. /// Validates and atomically persists one raw Config source candidate, then reloads the document detail.
pub(crate) fn save_source( pub(crate) fn save_document_source(
state: &crate::AppState, state: &crate::AppState,
file_id_text: &str, file_id_text: &str,
source: &str, source: &str,
@@ -148,7 +148,7 @@ pub(crate) fn save_source(
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)), std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)),
}; };
let document = detail(state, file_id.as_str()); let document = crate::document_detail(state, file_id.as_str());
let document = match document { let document = match document {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/dto_common.rs // file: crates/ksp-app-config-desk/src/dto_common.rs
// version: 1 // version: 2
//! Common Tauri DTOs shared by Config Desk commands. //! Common Tauri DTOs shared by Config Desk commands.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Safe command error projection that never serializes arbitrary KSP error context or source values. /// Safe command error projection that never serializes arbitrary KSP error context or source values.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/environment.rs // file: crates/ksp-app-config-desk/src/environment.rs
// version: 2 // version: 4
//! Safe Config environment reports and `.env` management projections for Config Desk. //! Safe Config environment reports and `.env` management projections for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Safe desired/effective environment view exposed to the ordinary Config Desk frontend. /// Safe desired/effective environment view exposed to the ordinary Config Desk frontend.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
@@ -63,17 +63,17 @@ impl EnvironmentMutationOperation {
} }
/// Returns the safe environment report owned by Config. /// Returns the safe environment report owned by Config.
pub(crate) fn report(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigEnvironmentReportDto>> { pub(crate) fn environment_report(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigEnvironmentReportDto>> {
return report_from_management(state.config_management()); return report_from_management(state.config_management());
} }
/// Creates or replaces one `.env` entry exclusively through ConfigManagement. /// Creates or replaces one `.env` entry exclusively through ConfigManagement.
pub(crate) fn set_value(state: &crate::AppState, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> { pub(crate) fn set_environment_value(state: &crate::AppState, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> {
return set_value_from_management(state.config_management(), variable_name, value); return set_value_from_management(state.config_management(), variable_name, value);
} }
/// Removes one `.env` entry exclusively through ConfigManagement. /// Removes one `.env` entry exclusively through ConfigManagement.
pub(crate) fn remove_value(state: &crate::AppState, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> { pub(crate) fn remove_environment_value(state: &crate::AppState, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> {
return remove_value_from_management(state.config_management(), variable_name); return remove_value_from_management(state.config_management(), variable_name);
} }

View File

@@ -1,44 +1,42 @@
// file: crates/ksp-app-config-desk/src/errors.rs // file: crates/ksp-app-config-desk/src/errors.rs
// version: 9 // version: 10
//! Application-local error codes for the configuration desktop shell. //! Application-local error codes for the configuration desktop shell.
/// Tauri runtime assembly or execution failed. /// Shared Config Desk application state is internally inconsistent.
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_runtime_failed"); pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_invalid");
/// Shared Config Desk runtime state cannot be locked safely.
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_lock_failed");
/// A Documents command requested a registered file that is not a Config-kind document.
pub(crate) const ERROR_CODE_DOCUMENT_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "document_kind_invalid");
/// Frontend logging requested an unsupported level.
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_level_invalid");
/// Frontend logging requested a target outside the application whitelist.
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_target_invalid");
/// Config Desk could not install the managed Logging runtime or its safe fallback. /// Config Desk could not install the managed Logging runtime or its safe fallback.
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_bootstrap_failed"); pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_bootstrap_failed");
/// Config Desk persisted a Logging candidate but could not restore the previous source after runtime application failed. /// Config Desk persisted a Logging candidate but could not restore the previous source after runtime application failed.
pub(crate) const ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED: ksp_core_lib::ErrorCode = pub(crate) const ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "logging_source_rollback_failed"); ksp_core_lib::ErrorCode::new("config_desk", "logging_source_rollback_failed");
/// Shared Config Desk application state is internally inconsistent.
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_invalid");
/// Shared Config Desk runtime state cannot be locked safely.
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_lock_failed");
/// Frontend logging requested an unsupported level.
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_level_invalid");
/// Frontend logging requested a target outside the application whitelist.
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_target_invalid");
/// Controlled Logging test request contains an unsupported or invalid selector. /// Controlled Logging test request contains an unsupported or invalid selector.
pub(crate) const ERROR_CODE_LOGGING_TEST_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_test_request_invalid"); pub(crate) const ERROR_CODE_LOGGING_TEST_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_test_request_invalid");
/// A validated Config document does not expose the standard profile contract expected by the Profiles panel. /// A validated Config document does not expose the standard profile contract expected by the Profiles panel.
pub(crate) const ERROR_CODE_PROFILE_CONTRACT_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_contract_missing"); pub(crate) const ERROR_CODE_PROFILE_CONTRACT_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_contract_missing");
/// Config profile data could not be projected safely for the frontend. /// Config profile data could not be projected safely for the frontend.
pub(crate) const ERROR_CODE_PROFILE_PROJECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_projection_failed"); pub(crate) const ERROR_CODE_PROFILE_PROJECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_projection_failed");
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_setting_invalid");
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_origin_invalid");
/// A required Tauri window is missing from the configured application runtime.
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_missing");
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_operation_failed");
/// A Documents command requested a registered file that is not a Config-kind document.
pub(crate) const ERROR_CODE_DOCUMENT_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "document_kind_invalid");
/// Privileged reveal was requested for a non-Secret KSP/KSPB namespace. /// Privileged reveal was requested for a non-Secret KSP/KSPB namespace.
pub(crate) const ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET: ksp_core_lib::ErrorCode = pub(crate) const ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_requires_secret"); ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_requires_secret");
/// Privileged reveal requested an unsupported source selector. /// Privileged reveal requested an unsupported source selector.
pub(crate) const ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_source_invalid"); pub(crate) const ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_source_invalid");
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_origin_invalid");
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_setting_invalid");
/// Tauri runtime assembly or execution failed.
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_runtime_failed");
/// A required Tauri window is missing from the configured application runtime.
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_missing");
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_operation_failed");

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/frontend_logging.rs // file: crates/ksp-app-config-desk/src/frontend_logging.rs
// version: 1 // version: 2
//! KSP-owned bridge for technical log events emitted by Config Desk frontend scripts. //! KSP-owned bridge for technical log events emitted by Config Desk frontend scripts.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Log payload sent by Config Desk frontend scripts. /// Log payload sent by Config Desk frontend scripts.
#[derive(Clone, Debug, serde::Deserialize, TS)] #[derive(Clone, Debug, serde::Deserialize, TS)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/lib.rs // file: crates/ksp-app-config-desk/src/lib.rs
// version: 14 // version: 16
//! Tauri desktop application for managing and validating KSP configuration. //! Tauri desktop application for managing and validating KSP configuration.
@@ -28,61 +28,173 @@ mod tw_splash;
/// Runs the KSP configuration desktop application. /// Runs the KSP configuration desktop application.
pub use self::tauri::run; pub use self::tauri::run;
/// Shared Config Desk application state managed by Tauri.
pub(crate) use self::app_state::AppState; pub(crate) use self::app_state::AppState;
/// Crate-internal `LoggingStartup` state shared across the owning crate.
pub(crate) use self::bootstrap::LoggingStartup;
/// Executes the crate-internal config management operation for the owning module.
pub(crate) use self::bootstrap::config_management; pub(crate) use self::bootstrap::config_management;
/// Executes the crate-internal initialize logging operation for the owning module.
pub(crate) use self::bootstrap::initialize_logging; pub(crate) use self::bootstrap::initialize_logging;
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP; pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
/// Structured domain for Config document inventory, diagnostics and repair.
pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS; pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS;
/// Structured domain for safe Config environment reports.
pub(crate) use self::constants::TRACING_DOMAIN_ENVIRONMENT; pub(crate) use self::constants::TRACING_DOMAIN_ENVIRONMENT;
/// Structured domain used by technical frontend events.
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND; pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
/// Structured domain for typed Logging editor inspection.
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_EDITOR; pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_EDITOR;
/// Structured domain for active Logging runtime metadata and explicit profile application.
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_RUNTIME; pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_RUNTIME;
/// Structured domain used by controlled Logging test events.
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_TEST; pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_TEST;
/// Structured domain for Config profile selection and provenance inspection.
pub(crate) use self::constants::TRACING_DOMAIN_PROFILES; pub(crate) use self::constants::TRACING_DOMAIN_PROFILES;
/// Structured domain for explicit privileged Secret reveal operations.
pub(crate) use self::constants::TRACING_DOMAIN_SECRETS; pub(crate) use self::constants::TRACING_DOMAIN_SECRETS;
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS; pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
/// Owning target for backend events emitted by Config Desk.
pub(crate) use self::constants::TRACING_TARGET; pub(crate) use self::constants::TRACING_TARGET;
/// Owning target for generic frontend events emitted through the KSP bridge.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND; pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
/// Owning target for main-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN; pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
/// Owning target for splash-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH; pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
/// Dedicated target used by the controlled Logging test panel.
pub(crate) use self::constants::TRACING_TARGET_LOGGING_TEST; pub(crate) use self::constants::TRACING_TARGET_LOGGING_TEST;
/// Detailed management view for one registered Config document.
pub(crate) use self::documents::ConfigDocumentDetailDto; pub(crate) use self::documents::ConfigDocumentDetailDto;
/// Safe document-specific command error with backend-owned diagnostic classification.
pub(crate) use self::documents::ConfigDocumentErrorDto; pub(crate) use self::documents::ConfigDocumentErrorDto;
/// Result of one validated raw-source repair attempt that reached persistence successfully.
pub(crate) use self::documents::ConfigDocumentSaveResultDto; pub(crate) use self::documents::ConfigDocumentSaveResultDto;
/// Safe inventory row for one Config document registered by `ksp-config-lib`.
pub(crate) use self::documents::ConfigDocumentSummaryDto; pub(crate) use self::documents::ConfigDocumentSummaryDto;
/// Loads one Config document detail, including raw source when Config can read it.
pub(crate) use self::documents::document_detail;
/// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state.
pub(crate) use self::documents::document_inventory;
/// Validates and atomically persists one raw Config source candidate, then reloads the document detail.
pub(crate) use self::documents::save_document_source;
/// Initial application/runtime snapshot exposed to the frontend.
pub(crate) use self::dto_common::AppSnapshotDto; pub(crate) use self::dto_common::AppSnapshotDto;
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
pub(crate) use self::dto_common::CommandErrorDto; pub(crate) use self::dto_common::CommandErrorDto;
/// Safe result metadata for one `.env` mutation.
pub(crate) use self::environment::ConfigEnvironmentChangeDto; pub(crate) use self::environment::ConfigEnvironmentChangeDto;
/// Safe desired/effective environment view exposed to the ordinary Config Desk frontend.
pub(crate) use self::environment::ConfigEnvironmentReportDto; pub(crate) use self::environment::ConfigEnvironmentReportDto;
/// Returns the safe environment report owned by Config.
pub(crate) use self::environment::environment_report;
/// Removes one `.env` entry exclusively through ConfigManagement.
pub(crate) use self::environment::remove_environment_value;
/// Creates or replaces one `.env` entry exclusively through ConfigManagement.
pub(crate) use self::environment::set_environment_value;
/// Shared Config Desk application state is internally inconsistent.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID; pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Config Desk runtime state cannot be locked safely.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED; pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
/// A Documents command requested a registered file that is not a Config-kind document.
pub(crate) use self::errors::ERROR_CODE_DOCUMENT_KIND_INVALID; pub(crate) use self::errors::ERROR_CODE_DOCUMENT_KIND_INVALID;
/// Frontend logging requested an unsupported level.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID; pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
/// Frontend logging requested a target outside the application whitelist.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID; pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
/// Config Desk could not install the managed Logging runtime or its safe fallback.
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED; pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
/// Config Desk persisted a Logging candidate but could not restore the previous source after runtime application failed.
pub(crate) use self::errors::ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED; pub(crate) use self::errors::ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED;
/// Controlled Logging test request contains an unsupported or invalid selector.
pub(crate) use self::errors::ERROR_CODE_LOGGING_TEST_REQUEST_INVALID; pub(crate) use self::errors::ERROR_CODE_LOGGING_TEST_REQUEST_INVALID;
/// A validated Config document does not expose the standard profile contract expected by the Profiles panel.
pub(crate) use self::errors::ERROR_CODE_PROFILE_CONTRACT_MISSING; pub(crate) use self::errors::ERROR_CODE_PROFILE_CONTRACT_MISSING;
/// Config profile data could not be projected safely for the frontend.
pub(crate) use self::errors::ERROR_CODE_PROFILE_PROJECTION_FAILED; pub(crate) use self::errors::ERROR_CODE_PROFILE_PROJECTION_FAILED;
/// Privileged reveal was requested for a non-Secret KSP/KSPB namespace.
pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET; pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET;
/// Privileged reveal requested an unsupported source selector.
pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID; pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID;
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID; pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID; pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
/// Tauri runtime assembly or execution failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED; pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
/// A required Tauri window is missing from the configured application runtime.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING; pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED; pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
/// Log payload sent by Config Desk frontend scripts.
pub(crate) use self::frontend_logging::FrontendLogPayloadDto; pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
/// Emits one validated frontend event through the KSP Logging facade.
pub(crate) use self::frontend_logging::emit_frontend_log_event; pub(crate) use self::frontend_logging::emit_frontend_log_event;
/// Console sink configuration exposed to the editor.
pub(crate) use self::logging_editor::LoggingConsoleDto;
/// Complete editable Logging candidate accepted from the frontend.
pub(crate) use self::logging_editor::LoggingDocumentCandidateDto; pub(crate) use self::logging_editor::LoggingDocumentCandidateDto;
/// Complete typed Logging document exposed to the editor.
pub(crate) use self::logging_editor::LoggingDocumentDto; pub(crate) use self::logging_editor::LoggingDocumentDto;
/// Result of one validated typed Logging persistence operation.
pub(crate) use self::logging_editor::LoggingDocumentSaveResultDto; pub(crate) use self::logging_editor::LoggingDocumentSaveResultDto;
pub(crate) use self::logging_runtime::{LoggingRuntimeFileDto, LoggingRuntimeStatusDto}; /// One persistent file sink exposed to the editor.
pub(crate) use self::logging_test::{LoggingTestRequestDto, LoggingTestResultDto}; pub(crate) use self::logging_editor::LoggingFileDto;
/// One Logging sink selector/filter exposed to the editor.
pub(crate) use self::logging_editor::LoggingOutputFilterDto;
/// One typed Logging profile exposed to the editor.
pub(crate) use self::logging_editor::LoggingProfileDto;
/// One global Logging target override exposed to the editor.
pub(crate) use self::logging_editor::LoggingTargetFilterDto;
/// Loads the typed Logging source through ConfigManagement and projects it for the frontend.
pub(crate) use self::logging_editor::logging_document;
/// Validates, atomically persists and hot-reloads one typed Logging candidate through ConfigManagement and `ksp-logging-lib`.
pub(crate) use self::logging_editor::save_logging_document;
/// Metadata for one currently active persistent Logging file output.
pub(crate) use self::logging_runtime::LoggingRuntimeFileDto;
/// Safe observable state of the currently active KSP Logging runtime.
pub(crate) use self::logging_runtime::LoggingRuntimeStatusDto;
/// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document.
pub(crate) use self::logging_runtime::apply_logging_profile;
/// Executes the crate-internal count to u64 operation for the owning module.
pub(crate) use self::logging_runtime::count_to_u64;
/// Creates the stable runtime identity for this Config Desk process launch.
pub(crate) use self::logging_runtime::launch_identity;
/// Returns safe metadata for the currently active Logging runtime.
pub(crate) use self::logging_runtime::logging_runtime_status;
/// Executes the crate-internal project logging file operation for the owning module.
pub(crate) use self::logging_runtime::project_logging_file;
/// Controlled request emitted through the backend KSP Logging facade.
pub(crate) use self::logging_test::LoggingTestRequestDto;
/// Safe result metadata for one controlled backend Logging test request.
pub(crate) use self::logging_test::LoggingTestResultDto;
/// Emits one controlled backend test request through `ksp-logging-lib` only.
pub(crate) use self::logging_test::emit_logging_test;
/// Safe detailed view of one resolved Config profile.
pub(crate) use self::profiles::ConfigProfileDetailDto; pub(crate) use self::profiles::ConfigProfileDetailDto;
/// One Config document that exposes the standard `default_profile` / `profiles` contract.
pub(crate) use self::profiles::ConfigProfileDocumentDto; pub(crate) use self::profiles::ConfigProfileDocumentDto;
/// Resolves one default or explicitly selected profile into a safe inspection DTO.
pub(crate) use self::profiles::profile_detail;
/// Lists validated Config documents that expose standard profiles.
pub(crate) use self::profiles::profile_inventory;
/// Privileged reveal request kept separate from ordinary environment-report DTOs.
pub(crate) use self::secrets::SecretRevealRequestDto; pub(crate) use self::secrets::SecretRevealRequestDto;
/// Privileged reveal response. This type deliberately does not derive `Clone` or `Debug`.
pub(crate) use self::secrets::SecretRevealResponseDto; pub(crate) use self::secrets::SecretRevealResponseDto;
/// Reveals one real Secret value only after the dedicated Tauri command has been explicitly invoked.
pub(crate) use self::secrets::reveal_secret;
/// Command emitted by Rust to the splash frontend.
pub(crate) use self::splash::SplashOrderDto; pub(crate) use self::splash::SplashOrderDto;
/// Runtime timings used by the common desk splash lifecycle.
pub(crate) use self::splash::SplashSettings; pub(crate) use self::splash::SplashSettings;
pub(crate) use self::tw_main::show_and_focus as show_main_window; /// Resolves the required main window or returns a typed error.
pub(crate) use self::tw_splash::frontend_ready as splash_frontend_ready_service; pub(crate) use self::tw_main::require_main_window;
/// Shows main window.
pub(crate) use self::tw_main::show_main_window;
/// Resolves the required splash window or returns a typed error.
pub(crate) use self::tw_splash::require_splash_window;
/// Executes the crate-internal splash frontend ready service operation for the owning module.
pub(crate) use self::tw_splash::splash_frontend_ready_service;

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/logging_editor.rs // file: crates/ksp-app-config-desk/src/logging_editor.rs
// version: 4 // version: 6
//! Typed Logging document projection and validated persistence for the Config Desk Logging editor. //! Typed Logging document projection and validated persistence for the Config Desk Logging editor.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// One Logging sink selector/filter exposed to the editor. /// One Logging sink selector/filter exposed to the editor.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
@@ -32,7 +32,7 @@ pub(crate) struct LoggingConsoleDto {
/// Console format source text. /// Console format source text.
pub(crate) format: String, pub(crate) format: String,
/// Sink-local selectors. /// Sink-local selectors.
pub(crate) filter: LoggingOutputFilterDto, pub(crate) filter: crate::LoggingOutputFilterDto,
} }
/// One persistent file sink exposed to the editor. /// One persistent file sink exposed to the editor.
@@ -53,7 +53,7 @@ pub(crate) struct LoggingFileDto {
/// Whether ANSI output is requested. /// Whether ANSI output is requested.
pub(crate) ansi: bool, pub(crate) ansi: bool,
/// Sink-local selectors. /// Sink-local selectors.
pub(crate) filter: LoggingOutputFilterDto, pub(crate) filter: crate::LoggingOutputFilterDto,
} }
/// One global Logging target override exposed to the editor. /// One global Logging target override exposed to the editor.
@@ -79,11 +79,11 @@ pub(crate) struct LoggingProfileDto {
/// Span lifecycle source text. /// Span lifecycle source text.
pub(crate) span_events: String, pub(crate) span_events: String,
/// Console sink configuration. /// Console sink configuration.
pub(crate) console: LoggingConsoleDto, pub(crate) console: crate::LoggingConsoleDto,
/// Persistent file sinks in source order. /// Persistent file sinks in source order.
pub(crate) files: std::vec::Vec<LoggingFileDto>, pub(crate) files: std::vec::Vec<crate::LoggingFileDto>,
/// Global target overrides in source order. /// Global target overrides in source order.
pub(crate) target_filters: std::vec::Vec<LoggingTargetFilterDto>, pub(crate) target_filters: std::vec::Vec<crate::LoggingTargetFilterDto>,
} }
/// Complete typed Logging document exposed to the editor. /// Complete typed Logging document exposed to the editor.
@@ -102,7 +102,7 @@ pub(crate) struct LoggingDocumentDto {
/// Autonomous default profile identifier. /// Autonomous default profile identifier.
pub(crate) default_profile: String, pub(crate) default_profile: String,
/// Typed profiles in source order. /// Typed profiles in source order.
pub(crate) profiles: std::vec::Vec<LoggingProfileDto>, pub(crate) profiles: std::vec::Vec<crate::LoggingProfileDto>,
} }
/// Complete editable Logging candidate accepted from the frontend. /// Complete editable Logging candidate accepted from the frontend.
@@ -115,7 +115,7 @@ pub(crate) struct LoggingDocumentCandidateDto {
/// Autonomous default profile identifier. /// Autonomous default profile identifier.
pub(crate) default_profile: String, pub(crate) default_profile: String,
/// Typed profiles in source order. /// Typed profiles in source order.
pub(crate) profiles: std::vec::Vec<LoggingProfileDto>, pub(crate) profiles: std::vec::Vec<crate::LoggingProfileDto>,
} }
/// Result of one validated typed Logging persistence operation. /// Result of one validated typed Logging persistence operation.
@@ -138,12 +138,12 @@ pub(crate) struct LoggingDocumentSaveResultDto {
} }
/// Loads the typed Logging source through ConfigManagement and projects it for the frontend. /// Loads the typed Logging source through ConfigManagement and projects it for the frontend.
pub(crate) fn document(state: &crate::AppState) -> ksp_core_lib::Result<LoggingDocumentDto> { pub(crate) fn logging_document(state: &crate::AppState) -> ksp_core_lib::Result<LoggingDocumentDto> {
return document_from_management(state.config_management()); return document_from_management(state.config_management());
} }
/// Validates, atomically persists and hot-reloads one typed Logging candidate through ConfigManagement and `ksp-logging-lib`. /// Validates, atomically persists and hot-reloads one typed Logging candidate through ConfigManagement and `ksp-logging-lib`.
pub(crate) fn save(state: &crate::AppState, candidate: LoggingDocumentCandidateDto) -> ksp_core_lib::Result<LoggingDocumentSaveResultDto> { pub(crate) fn save_logging_document(state: &crate::AppState, candidate: LoggingDocumentCandidateDto) -> ksp_core_lib::Result<LoggingDocumentSaveResultDto> {
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_STD_LOGGING); let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_STD_LOGGING);
let file_id = match file_id { let file_id = match file_id {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -255,7 +255,7 @@ fn document_from_management(management: &ksp_config_lib::ConfigManagement) -> ks
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let mut profiles = std::vec::Vec::<LoggingProfileDto>::with_capacity(source.profiles().len()); let mut profiles = std::vec::Vec::<crate::LoggingProfileDto>::with_capacity(source.profiles().len());
let mut file_count = 0usize; let mut file_count = 0usize;
let mut target_filter_count = 0usize; let mut target_filter_count = 0usize;
for profile in source.profiles() { for profile in source.profiles() {
@@ -291,7 +291,7 @@ fn config_candidate(candidate: LoggingDocumentCandidateDto) -> ksp_config_lib::L
return ksp_config_lib::LoggingConfigDocument::new(candidate.logs_directory, candidate.default_profile, profiles); return ksp_config_lib::LoggingConfigDocument::new(candidate.logs_directory, candidate.default_profile, profiles);
} }
fn config_profile(profile: LoggingProfileDto) -> ksp_config_lib::LoggingProfileConfig { fn config_profile(profile: crate::LoggingProfileDto) -> ksp_config_lib::LoggingProfileConfig {
let mut files = std::vec::Vec::<ksp_config_lib::LoggingFileConfig>::with_capacity(profile.files.len()); let mut files = std::vec::Vec::<ksp_config_lib::LoggingFileConfig>::with_capacity(profile.files.len());
for file in profile.files { for file in profile.files {
let mut config = let mut config =
@@ -319,14 +319,14 @@ fn config_profile(profile: LoggingProfileDto) -> ksp_config_lib::LoggingProfileC
); );
} }
fn config_output_filter(filter: LoggingOutputFilterDto) -> ksp_config_lib::LoggingOutputFilterConfig { fn config_output_filter(filter: crate::LoggingOutputFilterDto) -> ksp_config_lib::LoggingOutputFilterConfig {
return ksp_config_lib::LoggingOutputFilterConfig::new(filter.level, filter.targets, filter.domains); return ksp_config_lib::LoggingOutputFilterConfig::new(filter.level, filter.targets, filter.domains);
} }
fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingProfileDto { fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> crate::LoggingProfileDto {
let mut files = std::vec::Vec::<LoggingFileDto>::with_capacity(profile.files().len()); let mut files = std::vec::Vec::<crate::LoggingFileDto>::with_capacity(profile.files().len());
for file in profile.files() { for file in profile.files() {
files.push(LoggingFileDto { files.push(crate::LoggingFileDto {
output_id: file.output_id().to_owned(), output_id: file.output_id().to_owned(),
enabled: file.enabled(), enabled: file.enabled(),
path: file.path().to_owned(), path: file.path().to_owned(),
@@ -336,15 +336,15 @@ fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingPro
filter: project_output_filter(file.filter()), filter: project_output_filter(file.filter()),
}); });
} }
let mut target_filters = std::vec::Vec::<LoggingTargetFilterDto>::with_capacity(profile.target_filters().len()); let mut target_filters = std::vec::Vec::<crate::LoggingTargetFilterDto>::with_capacity(profile.target_filters().len());
for target_filter in profile.target_filters() { for target_filter in profile.target_filters() {
target_filters.push(LoggingTargetFilterDto { target_prefix: target_filter.target_prefix().to_owned(), level: target_filter.level().to_owned() }); target_filters.push(crate::LoggingTargetFilterDto { target_prefix: target_filter.target_prefix().to_owned(), level: target_filter.level().to_owned() });
} }
return LoggingProfileDto { return crate::LoggingProfileDto {
profile_id: profile.profile_id().to_owned(), profile_id: profile.profile_id().to_owned(),
default_filter: profile.default_filter().to_owned(), default_filter: profile.default_filter().to_owned(),
span_events: profile.span_events().to_owned(), span_events: profile.span_events().to_owned(),
console: LoggingConsoleDto { console: crate::LoggingConsoleDto {
enabled: profile.console().enabled(), enabled: profile.console().enabled(),
output: profile.console().output().to_owned(), output: profile.console().output().to_owned(),
ansi: profile.console().ansi(), ansi: profile.console().ansi(),
@@ -356,8 +356,8 @@ fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingPro
}; };
} }
fn project_output_filter(filter: &ksp_config_lib::LoggingOutputFilterConfig) -> LoggingOutputFilterDto { fn project_output_filter(filter: &ksp_config_lib::LoggingOutputFilterConfig) -> crate::LoggingOutputFilterDto {
return LoggingOutputFilterDto { return crate::LoggingOutputFilterDto {
level: filter.level().to_owned(), level: filter.level().to_owned(),
targets: filter.targets().to_vec(), targets: filter.targets().to_vec(),
domains: filter.domains().to_vec(), domains: filter.domains().to_vec(),

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/logging_runtime.rs // file: crates/ksp-app-config-desk/src/logging_runtime.rs
// version: 2 // version: 4
//! Runtime Logging metadata, launch identity and explicit profile application for Config Desk. //! Runtime Logging metadata, launch identity and explicit profile application for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Metadata for one currently active persistent Logging file output. /// Metadata for one currently active persistent Logging file output.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
@@ -56,12 +56,12 @@ pub(crate) fn launch_identity() -> ksp_core_lib::Result<ksp_logging_lib::Logging
} }
/// Returns safe metadata for the currently active Logging runtime. /// Returns safe metadata for the currently active Logging runtime.
pub(crate) fn status(state: &crate::AppState) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> { pub(crate) fn logging_runtime_status(state: &crate::AppState) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> {
return state.logging_runtime_status(); return state.logging_runtime_status();
} }
/// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document. /// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document.
pub(crate) fn apply_profile(state: &crate::AppState, profile_id: &str) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> { pub(crate) fn apply_logging_profile(state: &crate::AppState, profile_id: &str) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> {
let environment = ksp_config_lib::ConfigEnvironment::load(); let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment { let environment = match environment {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -88,7 +88,8 @@ pub(crate) fn apply_profile(state: &crate::AppState, profile_id: &str) -> ksp_co
return state.logging_runtime_status(); return state.logging_runtime_status();
} }
pub(crate) fn project_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> LoggingRuntimeFileDto { /// Executes the crate-internal project logging file operation for the owning module.
pub(crate) fn project_logging_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> LoggingRuntimeFileDto {
return LoggingRuntimeFileDto { return LoggingRuntimeFileDto {
output_id: metadata.output_id().to_owned(), output_id: metadata.output_id().to_owned(),
directory: metadata.directory().display().to_string(), directory: metadata.directory().display().to_string(),
@@ -97,7 +98,7 @@ pub(crate) fn project_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> L
}; };
} }
pub(crate) const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str { const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str {
return match rotation { return match rotation {
ksp_logging_lib::FileRotation::Never => "never", ksp_logging_lib::FileRotation::Never => "never",
ksp_logging_lib::FileRotation::Hourly => "hourly", ksp_logging_lib::FileRotation::Hourly => "hourly",
@@ -105,6 +106,7 @@ pub(crate) const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &
}; };
} }
/// Executes the crate-internal count to u64 operation for the owning module.
pub(crate) fn count_to_u64(value: usize) -> u64 { pub(crate) fn count_to_u64(value: usize) -> u64 {
let converted = u64::try_from(value); let converted = u64::try_from(value);
return match converted { return match converted {

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/logging_test.rs // file: crates/ksp-app-config-desk/src/logging_test.rs
// version: 1 // version: 2
//! Controlled Logging test events for validating KSP runtime routing from Config Desk. //! Controlled Logging test events for validating KSP runtime routing from Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Controlled request emitted through the backend KSP Logging facade. /// Controlled request emitted through the backend KSP Logging facade.
#[derive(Clone, Debug, serde::Deserialize, TS)] #[derive(Clone, Debug, serde::Deserialize, TS)]
@@ -56,7 +56,7 @@ enum LoggingTestTarget {
} }
/// Emits one controlled backend test request through `ksp-logging-lib` only. /// Emits one controlled backend test request through `ksp-logging-lib` only.
pub(crate) fn emit(state: &crate::AppState, request: LoggingTestRequestDto) -> ksp_core_lib::Result<LoggingTestResultDto> { pub(crate) fn emit_logging_test(state: &crate::AppState, request: LoggingTestRequestDto) -> ksp_core_lib::Result<LoggingTestResultDto> {
let level = parse_level(request.level.as_str()); let level = parse_level(request.level.as_str());
let level = match level { let level = match level {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/profiles.rs // file: crates/ksp-app-config-desk/src/profiles.rs
// version: 1 // version: 4
//! Safe Config profile inspection and provenance projections for Config Desk. //! Safe Config profile inspection and provenance projections for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// One Config document that exposes the standard `default_profile` / `profiles` contract. /// One Config document that exposes the standard `default_profile` / `profiles` contract.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
@@ -77,13 +77,22 @@ pub(crate) struct ConfigProfileDetailDto {
pub(crate) environment_provenance: std::vec::Vec<ConfigProfileEnvironmentProvenanceDto>, pub(crate) environment_provenance: std::vec::Vec<ConfigProfileEnvironmentProvenanceDto>,
} }
struct ProfileContract {
default_profile: String,
profile_ids: std::vec::Vec<String>,
}
/// Lists validated Config documents that expose standard profiles. /// Lists validated Config documents that expose standard profiles.
pub(crate) fn inventory(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigProfileDocumentDto>> { pub(crate) fn profile_inventory(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigProfileDocumentDto>> {
return inventory_from_management(state.config_management()); return inventory_from_management(state.config_management());
} }
/// Resolves one default or explicitly selected profile into a safe inspection DTO. /// Resolves one default or explicitly selected profile into a safe inspection DTO.
pub(crate) fn detail(state: &crate::AppState, file_id: &str, requested_profile: std::option::Option<&str>) -> ksp_core_lib::Result<ConfigProfileDetailDto> { pub(crate) fn profile_detail(
state: &crate::AppState,
file_id: &str,
requested_profile: std::option::Option<&str>,
) -> ksp_core_lib::Result<ConfigProfileDetailDto> {
return detail_from_management(state.config_management(), file_id, requested_profile); return detail_from_management(state.config_management(), file_id, requested_profile);
} }
@@ -213,11 +222,6 @@ fn detail_from_management(
}); });
} }
struct ProfileContract {
default_profile: String,
profile_ids: std::vec::Vec<String>,
}
fn profile_contract(document: &ksp_config_lib::ConfigJsonDocument) -> ksp_core_lib::Result<std::option::Option<ProfileContract>> { fn profile_contract(document: &ksp_config_lib::ConfigJsonDocument) -> ksp_core_lib::Result<std::option::Option<ProfileContract>> {
let root = match document.value().as_object() { let root = match document.value().as_object() {
std::option::Option::Some(value) => value, std::option::Option::Some(value) => value,

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/secrets.rs // file: crates/ksp-app-config-desk/src/secrets.rs
// version: 1 // version: 2
//! Privileged, explicitly requested Config Secret reveal boundary for Config Desk. //! Privileged, explicitly requested Config Secret reveal boundary for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Privileged reveal request kept separate from ordinary environment-report DTOs. /// Privileged reveal request kept separate from ordinary environment-report DTOs.
#[derive(serde::Deserialize, TS)] #[derive(serde::Deserialize, TS)]
@@ -56,7 +56,7 @@ impl SecretRevealSource {
} }
/// Reveals one real Secret value only after the dedicated Tauri command has been explicitly invoked. /// Reveals one real Secret value only after the dedicated Tauri command has been explicitly invoked.
pub(crate) fn reveal(state: &crate::AppState, request: SecretRevealRequestDto) -> ksp_core_lib::Result<SecretRevealResponseDto> { pub(crate) fn reveal_secret(state: &crate::AppState, request: SecretRevealRequestDto) -> ksp_core_lib::Result<SecretRevealResponseDto> {
return reveal_from_management(state.config_management(), request); return reveal_from_management(state.config_management(), request);
} }

View File

@@ -1,16 +1,16 @@
// file: crates/ksp-app-config-desk/src/splash.rs // file: crates/ksp-app-config-desk/src/splash.rs
// version: 3 // version: 5
//! Common splash settings and frontend event contracts for Config Desk. //! Common splash settings and frontend event contracts for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_MS";
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
const DEFAULT_SPLASH_FADE_MS: u32 = 300; const DEFAULT_SPLASH_FADE_MS: u32 = 300;
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000; const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_MS";
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
const MAX_SPLASH_FADE_MS: u32 = 10_000; const MAX_SPLASH_FADE_MS: u32 = 10_000;
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
/// Runtime timings used by the common desk splash lifecycle. /// Runtime timings used by the common desk splash lifecycle.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -94,14 +94,6 @@ impl SplashSettings {
} }
} }
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
return match source {
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
};
}
/// Command emitted by Rust to the splash frontend. /// Command emitted by Rust to the splash frontend.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -116,12 +108,21 @@ pub(crate) struct SplashOrderDto {
} }
impl SplashOrderDto { impl SplashOrderDto {
/// Creates a new `SplashOrderDto` value.
#[must_use] #[must_use]
pub(crate) fn new(action: &str, message: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self { pub(crate) fn new(action: &str, message: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self {
return Self { action: action.to_owned(), message: message.map(std::string::ToString::to_string), duration_ms }; return Self { action: action.to_owned(), message: message.map(std::string::ToString::to_string), duration_ms };
} }
} }
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
return match source {
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
};
}
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> { fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
let parsed = value.parse::<u64>(); let parsed = value.parse::<u64>();
let parsed = match parsed { let parsed = match parsed {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/tauri.rs // file: crates/ksp-app-config-desk/src/tauri.rs
// version: 15 // version: 16
//! Tauri runtime assembly for the KSP configuration desktop application. //! Tauri runtime assembly for the KSP configuration desktop application.
@@ -60,11 +60,11 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> { fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.setup(|app| { return builder.setup(|app| {
let splash = crate::tw_splash::require_window(app); let splash = crate::require_splash_window(app);
if let std::result::Result::Err(error) = splash { if let std::result::Result::Err(error) = splash {
return std::result::Result::Err(std::boxed::Box::new(error)); return std::result::Result::Err(std::boxed::Box::new(error));
} }
let main = crate::tw_main::require_window(app); let main = crate::require_main_window(app);
if let std::result::Result::Err(error) = main { if let std::result::Result::Err(error) = main {
return std::result::Result::Err(std::boxed::Box::new(error)); return std::result::Result::Err(std::boxed::Box::new(error));
} }
@@ -83,7 +83,7 @@ fn get_app_snapshot(state: tauri::State<'_, crate::AppState>) -> std::result::Re
#[tauri::command] #[tauri::command]
fn get_config_documents(state: tauri::State<'_, crate::AppState>) -> std::vec::Vec<crate::ConfigDocumentSummaryDto> { fn get_config_documents(state: tauri::State<'_, crate::AppState>) -> std::vec::Vec<crate::ConfigDocumentSummaryDto> {
return crate::documents::inventory(&state); return crate::document_inventory(&state);
} }
#[tauri::command] #[tauri::command]
@@ -91,7 +91,7 @@ fn get_config_document_detail(
file_id: String, file_id: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigDocumentDetailDto, crate::ConfigDocumentErrorDto> { ) -> std::result::Result<crate::ConfigDocumentDetailDto, crate::ConfigDocumentErrorDto> {
return crate::documents::detail(&state, file_id.as_str()); return crate::document_detail(&state, file_id.as_str());
} }
#[tauri::command] #[tauri::command]
@@ -100,14 +100,14 @@ fn save_config_document_source(
source: String, source: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigDocumentSaveResultDto, crate::ConfigDocumentErrorDto> { ) -> std::result::Result<crate::ConfigDocumentSaveResultDto, crate::ConfigDocumentErrorDto> {
return crate::documents::save_source(&state, file_id.as_str(), source.as_str()); return crate::save_document_source(&state, file_id.as_str(), source.as_str());
} }
#[tauri::command] #[tauri::command]
fn get_config_profile_documents( fn get_config_profile_documents(
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::ConfigProfileDocumentDto>, crate::CommandErrorDto> { ) -> std::result::Result<std::vec::Vec<crate::ConfigProfileDocumentDto>, crate::CommandErrorDto> {
let result = crate::profiles::inventory(&state); let result = crate::profile_inventory(&state);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -120,7 +120,7 @@ fn get_config_profile_detail(
profile_id: std::option::Option<String>, profile_id: std::option::Option<String>,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigProfileDetailDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::ConfigProfileDetailDto, crate::CommandErrorDto> {
let result = crate::profiles::detail(&state, file_id.as_str(), profile_id.as_deref()); let result = crate::profile_detail(&state, file_id.as_str(), profile_id.as_deref());
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -131,7 +131,7 @@ fn get_config_profile_detail(
fn get_environment_report( fn get_environment_report(
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::ConfigEnvironmentReportDto>, crate::CommandErrorDto> { ) -> std::result::Result<std::vec::Vec<crate::ConfigEnvironmentReportDto>, crate::CommandErrorDto> {
let result = crate::environment::report(&state); let result = crate::environment_report(&state);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -144,7 +144,7 @@ fn set_environment_value(
value: String, value: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> {
let result = crate::environment::set_value(&state, variable_name.as_str(), value.as_str()); let result = crate::set_environment_value(&state, variable_name.as_str(), value.as_str());
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -156,7 +156,7 @@ fn remove_environment_value(
variable_name: String, variable_name: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> {
let result = crate::environment::remove_value(&state, variable_name.as_str()); let result = crate::remove_environment_value(&state, variable_name.as_str());
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -168,7 +168,7 @@ fn reveal_environment_value(
request: crate::SecretRevealRequestDto, request: crate::SecretRevealRequestDto,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::SecretRevealResponseDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::SecretRevealResponseDto, crate::CommandErrorDto> {
let result = crate::secrets::reveal(&state, request); let result = crate::reveal_secret(&state, request);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -177,7 +177,7 @@ fn reveal_environment_value(
#[tauri::command] #[tauri::command]
fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingDocumentDto, crate::CommandErrorDto> { fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingDocumentDto, crate::CommandErrorDto> {
let result = crate::logging_editor::document(&state); let result = crate::logging_document(&state);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -186,7 +186,7 @@ fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result
#[tauri::command] #[tauri::command]
fn get_logging_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> { fn get_logging_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> {
let result = crate::logging_runtime::status(&state); let result = crate::logging_runtime_status(&state);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -198,7 +198,7 @@ fn apply_logging_profile(
profile_id: String, profile_id: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> {
let result = crate::logging_runtime::apply_profile(&state, profile_id.as_str()); let result = crate::apply_logging_profile(&state, profile_id.as_str());
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -210,7 +210,7 @@ fn save_logging_document(
candidate: crate::LoggingDocumentCandidateDto, candidate: crate::LoggingDocumentCandidateDto,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LoggingDocumentSaveResultDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::LoggingDocumentSaveResultDto, crate::CommandErrorDto> {
let result = crate::logging_editor::save(&state, candidate); let result = crate::save_logging_document(&state, candidate);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -222,7 +222,7 @@ fn emit_logging_test(
request: crate::LoggingTestRequestDto, request: crate::LoggingTestRequestDto,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LoggingTestResultDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::LoggingTestResultDto, crate::CommandErrorDto> {
let result = crate::logging_test::emit(&state, request); let result = crate::emit_logging_test(&state, request);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),

View File

@@ -1,13 +1,15 @@
// file: crates/ksp-app-config-desk/src/tw_main.rs // file: crates/ksp-app-config-desk/src/tw_main.rs
// version: 1 // version: 4
//! Tauri-window helpers for the Config Desk main window. //! Tauri-window helpers for the Config Desk main window.
use tauri::Manager; // rust-rules: trait-import use tauri::Manager; // rust-rules: trait-import
/// Crate-internal `WINDOW_LABEL_MAIN` constant.
pub(crate) const WINDOW_LABEL_MAIN: &str = "main"; pub(crate) const WINDOW_LABEL_MAIN: &str = "main";
pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> { /// Resolves the required main window or returns a typed error.
pub(crate) fn require_main_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
let window = manager.get_webview_window(WINDOW_LABEL_MAIN); let window = manager.get_webview_window(WINDOW_LABEL_MAIN);
return match window { return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value), std::option::Option::Some(value) => std::result::Result::Ok(value),
@@ -18,8 +20,9 @@ pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib
}; };
} }
pub(crate) fn show_and_focus(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> { /// Shows main window.
let window = require_window(app); pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
let window = crate::require_main_window(app);
let window = match window { let window = match window {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),

View File

@@ -1,15 +1,18 @@
// file: crates/ksp-app-config-desk/src/tw_splash.rs // file: crates/ksp-app-config-desk/src/tw_splash.rs
// version: 2 // version: 5
//! Tauri-window lifecycle for the Config Desk splash window. //! Tauri-window lifecycle for the Config Desk splash window.
use tauri::Emitter; // rust-rules: trait-import use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import use tauri::Manager; // rust-rules: trait-import
/// Crate-internal `WINDOW_LABEL_SPLASH` constant.
pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash"; pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
const SPLASH_EVENT_NAME: &str = "ksp-splash-order"; const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> { /// Resolves the required splash window or returns a typed error.
pub(crate) fn require_splash_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
let window = manager.get_webview_window(WINDOW_LABEL_SPLASH); let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
return match window { return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value), std::option::Option::Some(value) => std::result::Result::Ok(value),
@@ -20,7 +23,12 @@ pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib
}; };
} }
pub(crate) async fn frontend_ready(app: tauri::AppHandle, invoking_window: tauri::WebviewWindow, state: &crate::AppState) -> ksp_core_lib::Result<()> { /// Executes the crate-internal splash frontend ready service operation for the owning module.
pub(crate) async fn splash_frontend_ready_service(
app: tauri::AppHandle,
invoking_window: tauri::WebviewWindow,
state: &crate::AppState,
) -> ksp_core_lib::Result<()> {
if invoking_window.label() != WINDOW_LABEL_SPLASH { if invoking_window.label() != WINDOW_LABEL_SPLASH {
return std::result::Result::Err( return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window") ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")

View File

@@ -1,31 +1,29 @@
// file: crates/ksp-app-config-desk/unit_tests/documents.rs // file: crates/ksp-app-config-desk/unit_tests/documents.rs
// version: 1 // version: 2
use super::*;
#[test] #[test]
fn diagnostic_classifier_distinguishes_json_schema_semantic_and_effective_errors() { fn diagnostic_classifier_distinguishes_json_schema_semantic_and_effective_errors() {
let json = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "json"); let json = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "json");
assert_eq!(classify_error(&json), STAGE_JSON); assert_eq!(super::classify_error(&json), super::STAGE_JSON);
let schema = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_SCHEMA_VALIDATION_FAILED, "schema"); let schema = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_SCHEMA_VALIDATION_FAILED, "schema");
assert_eq!(classify_error(&schema), STAGE_SCHEMA); assert_eq!(super::classify_error(&schema), super::STAGE_SCHEMA);
let semantic = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "semantic"); let semantic = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "semantic");
assert_eq!(classify_error(&semantic), STAGE_SEMANTIC); assert_eq!(super::classify_error(&semantic), super::STAGE_SEMANTIC);
let effective = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective"); let effective = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective");
assert_eq!(classify_error(&effective), STAGE_EFFECTIVE); assert_eq!(super::classify_error(&effective), super::STAGE_EFFECTIVE);
} }
#[test] #[test]
fn schema_source_read_errors_are_classified_as_schema_failures() { fn schema_source_read_errors_are_classified_as_schema_failures() {
let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_FILE_READ_FAILED, "missing schema").with_context("file_id", "schema.std.logging"); let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_FILE_READ_FAILED, "missing schema").with_context("file_id", "schema.std.logging");
assert_eq!(classify_error(&error), STAGE_SCHEMA); assert_eq!(super::classify_error(&error), super::STAGE_SCHEMA);
} }
#[test] #[test]
fn document_error_projection_does_not_expose_arbitrary_error_context() { fn document_error_projection_does_not_expose_arbitrary_error_context() {
let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "invalid source").with_context("detail", "sensitive-canary"); let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "invalid source").with_context("detail", "sensitive-canary");
let projection = ConfigDocumentErrorDto::from_error(&error); let projection = crate::ConfigDocumentErrorDto::from_error(&error);
assert_eq!(projection.diagnostic_stage, STAGE_JSON); assert_eq!(projection.diagnostic_stage, super::STAGE_JSON);
assert_eq!(projection.error.domain, "config"); assert_eq!(projection.error.domain, "config");
assert_eq!(projection.error.code, "json_syntax_invalid"); assert_eq!(projection.error.code, "json_syntax_invalid");
assert_eq!(projection.error.message, "invalid source"); assert_eq!(projection.error.message, "invalid source");

View File

@@ -1,10 +1,10 @@
// file: crates/ksp-app-config-desk/unit_tests/dto_common.rs // file: crates/ksp-app-config-desk/unit_tests/dto_common.rs
// version: 1 // version: 2
#[test] #[test]
fn command_error_projection_excludes_context_values() { fn command_error_projection_excludes_context_values() {
let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failure"), "safe message").with_context("secret_canary", "must-not-be-exported"); let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failure"), "safe message").with_context("secret_canary", "must-not-be-exported");
let dto = super::CommandErrorDto::from_error(&error); let dto = crate::CommandErrorDto::from_error(&error);
assert_eq!(dto.domain, "test"); assert_eq!(dto.domain, "test");
assert_eq!(dto.code, "failure"); assert_eq!(dto.code, "failure");
assert_eq!(dto.message, "safe message"); assert_eq!(dto.message, "safe message");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/environment.rs // file: crates/ksp-app-config-desk/unit_tests/environment.rs
// version: 2 // version: 4
#[test] #[test]
fn namespace_projection_distinguishes_ksp_and_kspb_sensitivity_families() { fn namespace_projection_distinguishes_ksp_and_kspb_sensitivity_families() {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/logging_editor.rs // file: crates/ksp-app-config-desk/unit_tests/logging_editor.rs
// version: 4 // version: 7
#[test] #[test]
fn committed_logging_document_maps_complete_read_only_editor_contract() { fn committed_logging_document_maps_complete_read_only_editor_contract() {
@@ -66,7 +66,7 @@ fn editor_candidate_round_trips_through_typed_config_contract() {
let document = super::document_from_management(&management); let document = super::document_from_management(&management);
assert!(document.is_ok(), "Logging editor document should load: {document:?}"); assert!(document.is_ok(), "Logging editor document should load: {document:?}");
if let std::result::Result::Ok(document) = document { if let std::result::Result::Ok(document) = document {
let candidate = super::LoggingDocumentCandidateDto { let candidate = crate::LoggingDocumentCandidateDto {
logs_directory: document.logs_directory.clone(), logs_directory: document.logs_directory.clone(),
default_profile: document.default_profile.clone(), default_profile: document.default_profile.clone(),
profiles: document.profiles.clone(), profiles: document.profiles.clone(),
@@ -89,42 +89,42 @@ fn editor_candidate_round_trips_through_typed_config_contract() {
#[test] #[test]
fn profile_candidate_preserves_multi_file_and_target_filter_shape() { fn profile_candidate_preserves_multi_file_and_target_filter_shape() {
let profile = super::LoggingProfileDto { let profile = crate::LoggingProfileDto {
profile_id: "clone_test".to_owned(), profile_id: "clone_test".to_owned(),
default_filter: "info".to_owned(), default_filter: "info".to_owned(),
span_events: "full".to_owned(), span_events: "full".to_owned(),
console: super::LoggingConsoleDto { console: crate::LoggingConsoleDto {
enabled: true, enabled: true,
output: "stdout".to_owned(), output: "stdout".to_owned(),
ansi: false, ansi: false,
format: "human".to_owned(), format: "human".to_owned(),
filter: super::LoggingOutputFilterDto { level: "info".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] }, filter: crate::LoggingOutputFilterDto { level: "info".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] },
}, },
files: std::vec![ files: std::vec![
super::LoggingFileDto { crate::LoggingFileDto {
output_id: "file.one".to_owned(), output_id: "file.one".to_owned(),
enabled: true, enabled: true,
path: "one.log".to_owned(), path: "one.log".to_owned(),
rotation: "daily".to_owned(), rotation: "daily".to_owned(),
format: "human".to_owned(), format: "human".to_owned(),
ansi: false, ansi: false,
filter: super::LoggingOutputFilterDto { level: "debug".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] }, filter: crate::LoggingOutputFilterDto { level: "debug".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] },
}, },
super::LoggingFileDto { crate::LoggingFileDto {
output_id: "file.two".to_owned(), output_id: "file.two".to_owned(),
enabled: false, enabled: false,
path: "two.jsonl".to_owned(), path: "two.jsonl".to_owned(),
rotation: "hourly".to_owned(), rotation: "hourly".to_owned(),
format: "json".to_owned(), format: "json".to_owned(),
ansi: false, ansi: false,
filter: super::LoggingOutputFilterDto { filter: crate::LoggingOutputFilterDto {
level: "error".to_owned(), level: "error".to_owned(),
targets: std::vec!["ksp-config-lib".to_owned()], targets: std::vec!["ksp-config-lib".to_owned()],
domains: std::vec!["config".to_owned()], domains: std::vec!["config".to_owned()],
}, },
}, },
], ],
target_filters: std::vec![super::LoggingTargetFilterDto { target_prefix: "ksp-app-config-desk".to_owned(), level: "trace".to_owned() }], target_filters: std::vec![crate::LoggingTargetFilterDto { target_prefix: "ksp-app-config-desk".to_owned(), level: "trace".to_owned() }],
}; };
let config = super::config_profile(profile); let config = super::config_profile(profile);
assert_eq!(config.profile_id(), "clone_test"); assert_eq!(config.profile_id(), "clone_test");

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/unit_tests/logging_runtime.rs // file: crates/ksp-app-config-desk/unit_tests/logging_runtime.rs
// version: 1 // version: 2
#[test] #[test]
fn launch_identity_is_safe_and_contains_application_and_process_identity() { fn launch_identity_is_safe_and_contains_application_and_process_identity() {
let identity = super::launch_identity(); let identity = crate::launch_identity();
assert!(identity.is_ok()); assert!(identity.is_ok());
if let std::result::Result::Ok(identity) = identity { if let std::result::Result::Ok(identity) = identity {
assert_eq!(identity.application_id(), crate::TRACING_TARGET); assert_eq!(identity.application_id(), crate::TRACING_TARGET);
@@ -21,6 +21,6 @@ fn runtime_rotation_labels_are_stable() {
#[test] #[test]
fn runtime_count_projection_is_non_lossy_for_normal_values() { fn runtime_count_projection_is_non_lossy_for_normal_values() {
assert_eq!(super::count_to_u64(0), 0); assert_eq!(crate::count_to_u64(0), 0);
assert_eq!(super::count_to_u64(42), 42); assert_eq!(crate::count_to_u64(42), 42);
} }

View File

@@ -1,25 +1,23 @@
// file: crates/ksp-app-config-desk/unit_tests/logging_test.rs // file: crates/ksp-app-config-desk/unit_tests/logging_test.rs
// version: 1 // version: 2
use super::{LoggingTestLevel, LoggingTestTarget};
#[test] #[test]
fn controlled_logging_test_levels_include_all_five_levels_and_batch_mode() { fn controlled_logging_test_levels_include_all_five_levels_and_batch_mode() {
assert!(matches!(super::parse_level("trace"), std::result::Result::Ok(LoggingTestLevel::Trace))); assert!(matches!(super::parse_level("trace"), std::result::Result::Ok(super::LoggingTestLevel::Trace)));
assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(LoggingTestLevel::Debug))); assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(super::LoggingTestLevel::Debug)));
assert!(matches!(super::parse_level("info"), std::result::Result::Ok(LoggingTestLevel::Info))); assert!(matches!(super::parse_level("info"), std::result::Result::Ok(super::LoggingTestLevel::Info)));
assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(LoggingTestLevel::Warn))); assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(super::LoggingTestLevel::Warn)));
assert!(matches!(super::parse_level("error"), std::result::Result::Ok(LoggingTestLevel::Error))); assert!(matches!(super::parse_level("error"), std::result::Result::Ok(super::LoggingTestLevel::Error)));
assert!(matches!(super::parse_level("all"), std::result::Result::Ok(LoggingTestLevel::All))); assert!(matches!(super::parse_level("all"), std::result::Result::Ok(super::LoggingTestLevel::All)));
assert!(super::parse_level("fatal").is_err()); assert!(super::parse_level("fatal").is_err());
} }
#[test] #[test]
fn controlled_logging_test_targets_are_static_ksp_targets() { fn controlled_logging_test_targets_are_static_ksp_targets() {
assert!(matches!(super::parse_target("app"), std::result::Result::Ok(LoggingTestTarget::App))); assert!(matches!(super::parse_target("app"), std::result::Result::Ok(super::LoggingTestTarget::App)));
assert!(matches!(super::parse_target("logging_test"), std::result::Result::Ok(LoggingTestTarget::Dedicated))); assert!(matches!(super::parse_target("logging_test"), std::result::Result::Ok(super::LoggingTestTarget::Dedicated)));
assert_eq!(super::target_value(LoggingTestTarget::App), crate::TRACING_TARGET); assert_eq!(super::target_value(super::LoggingTestTarget::App), crate::TRACING_TARGET);
assert_eq!(super::target_value(LoggingTestTarget::Dedicated), crate::TRACING_TARGET_LOGGING_TEST); assert_eq!(super::target_value(super::LoggingTestTarget::Dedicated), crate::TRACING_TARGET_LOGGING_TEST);
assert!(super::parse_target("external").is_err()); assert!(super::parse_target("external").is_err());
} }

View File

@@ -1,20 +1,26 @@
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs // file: crates/ksp-app-config-desk/unit_tests/profiles.rs
// version: 2 // version: 5
#[test] #[test]
fn profile_inventory_exposes_logging_default_and_available_profiles() { fn profile_inventory_exposes_registered_standard_profile_documents() {
let management = fixture_management(); let management = fixture_management();
assert!(management.is_ok(), "fixture management should construct: {management:?}"); assert!(management.is_ok(), "fixture management should construct: {management:?}");
if let std::result::Result::Ok(management) = management { if let std::result::Result::Ok(management) = management {
let inventory = super::inventory_from_management(&management); let inventory = super::inventory_from_management(&management);
assert!(inventory.is_ok(), "profile inventory should resolve: {inventory:?}"); assert!(inventory.is_ok(), "profile inventory should resolve: {inventory:?}");
if let std::result::Result::Ok(inventory) = inventory { if let std::result::Result::Ok(inventory) = inventory {
assert_eq!(inventory.len(), 1); assert!(inventory.iter().any(|document| -> bool {
assert_eq!(inventory[0].file_id, ksp_config_lib::FILE_ID_STD_LOGGING); return document.file_id == ksp_config_lib::FILE_ID_STD_LOGGING;
assert!(!inventory[0].default_profile.is_empty());
assert!(inventory[0].profile_ids.iter().any(|profile_id| {
return profile_id == &inventory[0].default_profile;
})); }));
assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_STD_TRANSPORT;
}));
for document in inventory {
assert!(!document.default_profile.is_empty());
assert!(document.profile_ids.iter().any(|profile_id| -> bool {
return profile_id == &document.default_profile;
}));
}
} }
} }
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/secrets.rs // file: crates/ksp-app-config-desk/unit_tests/secrets.rs
// version: 1 // version: 2
#[test] #[test]
fn reveal_policy_accepts_only_secret_names_and_supported_sources() { fn reveal_policy_accepts_only_secret_names_and_supported_sources() {
@@ -21,7 +21,7 @@ fn reveal_policy_accepts_only_secret_names_and_supported_sources() {
#[test] #[test]
fn reveal_response_type_does_not_require_debug_or_clone_contracts() { fn reveal_response_type_does_not_require_debug_or_clone_contracts() {
let response = super::SecretRevealResponseDto { let response = crate::SecretRevealResponseDto {
variable_name: "KSP_SECRET_TEST_VALUE".to_owned(), variable_name: "KSP_SECRET_TEST_VALUE".to_owned(),
source: "dotenv".to_owned(), source: "dotenv".to_owned(),
value: std::option::Option::Some("secret-canary".to_owned()), value: std::option::Option::Some("secret-canary".to_owned()),

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-config-lib/Cargo.toml # file: crates/ksp-config-lib/Cargo.toml
# version: 3 # version: 6
[package] [package]
name = "ksp-config-lib" name = "ksp-config-lib"
@@ -10,9 +10,13 @@ repository.workspace = true
[dependencies] [dependencies]
ksp-core-lib = { path = "../ksp-core-lib" } ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" } ksp-logging-lib = { path = "../ksp-logging-lib" }
serde.workspace = true ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
jsonschema.workspace = true jsonschema.workspace = true
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt"] }
[lints] [lints]
workspace = true workspace = true

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/README.md --> <!-- file: crates/ksp-config-lib/README.md -->
<!-- version: 3 --> <!-- version: 5 -->
# ksp-config-lib # ksp-config-lib
@@ -23,6 +23,7 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
- la classification `Public`, `Internal`, `Secret` ; - la classification `Public`, `Internal`, `Secret` ;
- les représentations réelle et sûre/redacted ainsi que la provenance des valeurs résolues ; - les représentations réelle et sûre/redacted ainsi que la provenance des valeurs résolues ;
- l'adapter du document Logging effectif vers `ksp_logging_lib::LoggingSettings` ; - l'adapter du document Logging effectif vers `ksp_logging_lib::LoggingSettings` ;
- l'adapter du document HTTP Transport effectif vers `ksp_onchain_transport_lib::HttpTransportSettings`, y compris redaction/provenance des URLs `KSP_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` ; - 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 é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`. - les audits workspace empêchant les bypass d'ownership Config et les oublis dans `.env.example`.
@@ -32,9 +33,11 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
Le registre par défaut connaît : Le registre par défaut connaît :
```text ```text
cfg.std.logging -> config/std.logging.json cfg.std.logging -> config/std.logging.json
schema.std.logging -> config/schemas/std.logging.schema.json cfg.std.transport -> config/std.transport.json
schema.composite -> config/schemas/composite.schema.json schema.std.logging -> config/schemas/std.logging.schema.json
schema.std.transport -> config/schemas/std.transport.schema.json
schema.composite -> config/schemas/composite.schema.json
``` ```
`ConfigFileRegistry::descriptors()` expose ces descripteurs en lecture seule et dans un ordre déterministe par `file_id`. Une application de management peut ainsi découvrir les fichiers connus sans maintenir une liste parallèle ni dépendre de leurs filenames physiques. `ConfigFileRegistry::descriptors()` expose ces descripteurs en lecture seule et dans un ordre déterministe par `file_id`. Une application de management peut ainsi découvrir les fichiers connus sans maintenir une liste parallèle ni dépendre de leurs filenames physiques.
@@ -59,15 +62,15 @@ Les autres crates et applications KSP ne doivent pas :
- parser ou écrire directement `.env` ; - parser ou écrire directement `.env` ;
- ouvrir directement les documents Config connus par leur filename physique ; - ouvrir directement les documents Config connus par leur filename physique ;
- réimplémenter la sélection de profils, les compositions ou les placeholders ; - réimplémenter la sélection de profils, les compositions ou les placeholders ;
- reconstruire elles-mêmes la configuration Logging depuis le JSON. - reconstruire elles-mêmes la configuration Logging ou Transport depuis le JSON.
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result` et de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`. `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`, et de `ksp-onchain-transport-lib` pour construire le contrat runtime Transport dans la direction Config -> Transport.
La dépendance inverse est interdite : `ksp-core-lib` et `ksp-logging-lib` ne dépendent pas de Config. La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib` et `ksp-onchain-transport-lib` 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`. 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`.
Tauri et les DTO TS-RS restent hors de cette crate. La future `ksp-app-config-desk` doit rester une frontière applicative mince au-dessus des APIs Config. Tauri et les DTO TS-RS restent hors de cette crate. `ksp-app-config-desk` reste une frontière applicative mince au-dessus des APIs Config et découvre les documents standards via le registre Config sans déplacer leur logique métier dans l'application.
## Secrets ## Secrets
@@ -75,12 +78,13 @@ 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. 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 Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs endpoint : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted.
## Documentation ## Documentation
- [`USAGE.md`](USAGE.md) — construction du moteur, résolution runtime et management ; - [`USAGE.md`](USAGE.md) — construction du moteur, résolution runtime et management ;
- [`TODO.md`](TODO.md) — points explicitement différés après `0.1.3` ; - [`TODO.md`](TODO.md) — points explicitement différés après `0.1.3` ;
- [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ; - [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ;
- [`../../config/std.logging.json`](../../config/std.logging.json) — premier document standard concret ; - [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard HTTP Transport ;
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime. - [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/TODO.md --> <!-- file: crates/ksp-config-lib/TODO.md -->
<!-- version: 3 --> <!-- version: 4 -->
# TODO ksp-config-lib # TODO ksp-config-lib
@@ -31,11 +31,15 @@ La validation applicative desktop appartient à `ksp-app-config-desk` :
Ces points ne nécessitent pas de duplication de logique dans `ksp-config-lib`; toute lacune réelle révélée par l'application ouvrira un delta Config explicite. Ces points ne nécessitent pas de duplication de logique dans `ksp-config-lib`; toute lacune réelle révélée par l'application ouvrira un delta Config explicite.
## Extension `0.2.1` — Transport HTTP
`0.2.1-pre.006` introduit le premier nouveau domaine standard depuis Logging : `std.transport.json`, son schema, son exemple, son enregistrement et l'adapter Config -> `HttpTransportSettings`. Cette extension confirme que les nouveaux domaines restent ajoutés à la demande d'un consumer réel, sans transformer Config en propriétaire du runtime Transport.
## Futur, uniquement au besoin ## Futur, uniquement au besoin
Les capacités suivantes sont différées jusqu'à l'apparition de composants réels : Les capacités suivantes sont différées jusqu'à l'apparition de composants réels :
- nouveaux documents `std.<domain>.json` et schemas associés ; - documents `std.<domain>.json` supplémentaires et schemas associés ;
- descriptors `cfg.composite.<consumer>` pour de vrais consumers ; - descriptors `cfg.composite.<consumer>` pour de vrais consumers ;
- contrats typés de management supplémentaires pour les nouveaux documents ; - contrats typés de management supplémentaires pour les nouveaux documents ;
- watcher filesystem/reload automatique si une application ou un service démontre le besoin ; - watcher filesystem/reload automatique si une application ou un service démontre le besoin ;

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/USAGE.md --> <!-- file: crates/ksp-config-lib/USAGE.md -->
<!-- version: 4 --> <!-- version: 5 -->
# Utilisation de ksp-config-lib # Utilisation de ksp-config-lib
@@ -29,6 +29,7 @@ Les arguments compris par Config sont :
--cfgpath=/path/to/config --cfgpath=/path/to/config
--schemapath=/path/to/schemas --schemapath=/path/to/schemas
--filemap=cfg.std.logging=my-logging.json --filemap=cfg.std.logging=my-logging.json
--filemap=cfg.std.transport=my-transport.json
``` ```
`cfgpath` et `schemapath` ne sont jamais lus depuis JSON, `.env` ou une variable KSP : cette règle évite un bootstrap récursif. `cfgpath` et `schemapath` ne sont jamais lus depuis JSON, `.env` ou une variable KSP : cette règle évite un bootstrap récursif.
@@ -122,6 +123,23 @@ Un `logs_directory` relatif est ancré sur le current working directory du proce
Les `files[].path` restent relatifs sous le root Logging, y compris après interpolation. Les `files[].path` restent relatifs sous le root Logging, y compris après interpolation.
### 4.1 Construire le Transport HTTP depuis Config
Config possède également l'adapter du document `std.transport` vers le contrat runtime de `ksp-onchain-transport-lib` :
```rust
let transport = match engine.load_resolved_transport_config(std::option::Option::None, &environment) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transport_settings = transport.into_settings();
```
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. Les URLs peuvent provenir de `KSP_PUBLIC_*` ou de `KSP_SECRET_*`; dans ce dernier cas la valeur réelle reste disponible au runtime Transport, mais `ResolvedTransportConfig::effective().safe_value()` et les représentations `Debug` sont redacted.
La dépendance reste unidirectionnelle : Config connaît le contrat Transport pour le construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
## 5. Profils et composites ## 5. Profils et composites
Pour un document standard profilé : Pour un document standard profilé :
@@ -213,6 +231,7 @@ Ce guide reste volontairement indépendant des numéros de release. Les contrats
| Environnement | `ConfigEnvironment`, `ConfigEnvironmentSource`, `ConfigEnvironmentValue`, `DEFAULT_DOTENV_PATH`, `DEFAULT_DOTENV_EXAMPLE_PATH` | §3, §78 | | Environnement | `ConfigEnvironment`, `ConfigEnvironmentSource`, `ConfigEnvironmentValue`, `DEFAULT_DOTENV_PATH`, `DEFAULT_DOTENV_EXAMPLE_PATH` | §3, §78 |
| Sensibilité/provenance | `ConfigSensitivity`, `ConfigValueProvenance`, `ResolvedConfigText`, `ResolvedConfigJson`, `REDACTED_CONFIG_VALUE` | §3 | | Sensibilité/provenance | `ConfigSensitivity`, `ConfigValueProvenance`, `ResolvedConfigText`, `ResolvedConfigJson`, `REDACTED_CONFIG_VALUE` | §3 |
| Logging effectif | `ResolvedLoggingConfig` | §4 | | Logging effectif | `ResolvedLoggingConfig` | §4 |
| Transport effectif | `ResolvedTransportConfig` | §4.1 |
| Management | `ConfigManagement`, `ConfigManagedSource`, `ConfigDocumentChangeReport`, `ConfigEnvironmentReport`, `ConfigEnvironmentChangeReport` | §67 | | Management | `ConfigManagement`, `ConfigManagedSource`, `ConfigDocumentChangeReport`, `ConfigEnvironmentReport`, `ConfigEnvironmentChangeReport` | §67 |
| Source Logging typée | `LoggingConfigDocument`, `LoggingProfileConfig`, `LoggingConsoleConfig`, `LoggingFileConfig`, `LoggingOutputFilterConfig`, `LoggingTargetFilterConfig` | §6 et exemple ci-dessous | | Source Logging typée | `LoggingConfigDocument`, `LoggingProfileConfig`, `LoggingConsoleConfig`, `LoggingFileConfig`, `LoggingOutputFilterConfig`, `LoggingTargetFilterConfig` | §6 et exemple ci-dessous |
| Erreurs Config | constantes `ERROR_CODE_*` réexportées par la crate | exemple ci-dessous | | Erreurs Config | constantes `ERROR_CODE_*` réexportées par la crate | exemple ci-dessous |
@@ -324,4 +343,3 @@ if let std::result::Result::Err(error) = loaded {
``` ```
Le message/context d'erreur reste destiné au diagnostic ; l'identité machine-readable passe par `ErrorCode`. Le message/context d'erreur reste destiné au diagnostic ; l'identité machine-readable passe par `ErrorCode`.

View File

@@ -1,14 +1,14 @@
// file: crates/ksp-config-lib/src/bootstrap.rs // file: crates/ksp-config-lib/src/bootstrap.rs
// version: 1 // version: 2
/// Default root containing KSP runtime configuration documents.
pub const DEFAULT_CFG_PATH: &str = "config";
/// Default root containing KSP JSON schemas.
pub const DEFAULT_SCHEMA_PATH: &str = "config/schemas";
/// Bootstrap argument used to replace the configuration document root. /// Bootstrap argument used to replace the configuration document root.
pub const ARG_CFG_PATH: &str = "--cfgpath"; pub const ARG_CFG_PATH: &str = "--cfgpath";
/// Bootstrap argument used to replace the schema root. /// Bootstrap argument used to replace the schema root.
pub const ARG_SCHEMA_PATH: &str = "--schemapath"; pub const ARG_SCHEMA_PATH: &str = "--schemapath";
/// Default root containing KSP runtime configuration documents.
pub const DEFAULT_CFG_PATH: &str = "config";
/// Default root containing KSP JSON schemas.
pub const DEFAULT_SCHEMA_PATH: &str = "config/schemas";
/// Non-recursive bootstrap options required before Config can resolve any managed document. /// Non-recursive bootstrap options required before Config can resolve any managed document.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/composite.rs // file: crates/ksp-config-lib/src/composite.rs
// version: 1 // version: 2
/// One resolved document component selected by a composite profile. /// One resolved document component selected by a composite profile.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@@ -118,6 +118,7 @@ struct CompositeDocumentReferenceSource {
profile_id: std::option::Option<String>, profile_id: std::option::Option<String>,
} }
/// Validates composite document contract.
pub(crate) fn validate_composite_document_contract(engine: &crate::ConfigDocumentEngine, document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> { pub(crate) fn validate_composite_document_contract(engine: &crate::ConfigDocumentEngine, document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
if !document.file_id().as_str().starts_with("cfg.composite.") { if !document.file_id().as_str().starts_with("cfg.composite.") {
return std::result::Result::Ok(()); return std::result::Result::Ok(());
@@ -198,12 +199,8 @@ fn resolve_composite_document(
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let resolved = crate::profile::load_resolved_profile_with_source( let resolved =
engine, crate::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
&file_id,
reference.profile_id.as_deref(),
crate::ConfigProfileSelectionSource::Composite,
);
let resolved = match resolved { let resolved = match resolved {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -262,8 +259,7 @@ fn validate_reference(
"referenced file_id is not a Config document", "referenced file_id is not a Config document",
)); ));
} }
let resolved = let resolved = crate::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
crate::profile::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
if let std::result::Result::Err(error) = resolved { if let std::result::Result::Err(error) = resolved {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }

View File

@@ -0,0 +1,7 @@
// file: crates/ksp-config-lib/src/constants.rs
// version: 1
//! Config-owned tracing constants.
/// Owning tracing target for events emitted by the Config crate.
pub(crate) const TRACING_TARGET: &str = "ksp-config-lib";

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/document.rs // file: crates/ksp-config-lib/src/document.rs
// version: 5 // version: 6
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation. /// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@@ -85,6 +85,7 @@ impl ConfigDocumentEngine {
return self.validate_document(document, &schema_file_id); return self.validate_document(document, &schema_file_id);
} }
/// Validates source candidate.
pub(crate) fn validate_source_candidate(&self, file_id: &crate::ConfigFileId, source: &str) -> ksp_core_lib::Result<ConfigJsonDocument> { pub(crate) fn validate_source_candidate(&self, file_id: &crate::ConfigFileId, source: &str) -> ksp_core_lib::Result<ConfigJsonDocument> {
let path = self.registry.resolve_path(&self.bootstrap, file_id); let path = self.registry.resolve_path(&self.bootstrap, file_id);
let path = match path { let path = match path {
@@ -99,6 +100,7 @@ impl ConfigDocumentEngine {
return self.validate_candidate(file_id, value); return self.validate_candidate(file_id, value);
} }
/// Validates candidate.
pub(crate) fn validate_candidate(&self, file_id: &crate::ConfigFileId, value: serde_json::Value) -> ksp_core_lib::Result<ConfigJsonDocument> { pub(crate) fn validate_candidate(&self, file_id: &crate::ConfigFileId, value: serde_json::Value) -> ksp_core_lib::Result<ConfigJsonDocument> {
let descriptor = self.registry.descriptor(file_id); let descriptor = self.registry.descriptor(file_id);
let descriptor = match descriptor { let descriptor = match descriptor {
@@ -256,11 +258,11 @@ fn validate_instance(document: &ConfigJsonDocument, schema: &ConfigJsonDocument)
} }
fn validate_document_semantics(engine: &ConfigDocumentEngine, document: &ConfigJsonDocument) -> ksp_core_lib::Result<()> { fn validate_document_semantics(engine: &ConfigDocumentEngine, document: &ConfigJsonDocument) -> ksp_core_lib::Result<()> {
let profile_validation = crate::profile::validate_document_profile_contract(document); let profile_validation = crate::validate_document_profile_contract(document);
if let std::result::Result::Err(error) = profile_validation { if let std::result::Result::Err(error) = profile_validation {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
let composite_validation = crate::composite::validate_composite_document_contract(engine, document); let composite_validation = crate::validate_composite_document_contract(engine, document);
if let std::result::Result::Err(error) = composite_validation { if let std::result::Result::Err(error) = composite_validation {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }

View File

@@ -1,13 +1,11 @@
// file: crates/ksp-config-lib/src/environment.rs // file: crates/ksp-config-lib/src/environment.rs
// version: 4 // version: 8
/// Default local environment file read by Config from the process launch directory.
pub const DEFAULT_DOTENV_PATH: &str = ".env";
/// Versioned environment contract template expected at the repository/runtime root. /// Versioned environment contract template expected at the repository/runtime root.
pub const DEFAULT_DOTENV_EXAMPLE_PATH: &str = ".env.example"; pub const DEFAULT_DOTENV_EXAMPLE_PATH: &str = ".env.example";
/// Default local environment file read by Config from the process launch directory.
pub const DEFAULT_DOTENV_PATH: &str = ".env";
const LOGGING_TARGET: &str = "ksp-config-lib";
const LOGGING_DOMAIN: &str = "config.environment"; const LOGGING_DOMAIN: &str = "config.environment";
/// Source that supplied one resolved Config environment variable. /// Source that supplied one resolved Config environment variable.
@@ -231,6 +229,7 @@ impl ConfigEnvironment {
}; };
} }
/// Loads from dotenv path.
pub(crate) fn load_from_dotenv_path(dotenv_path: &std::path::Path) -> ksp_core_lib::Result<Self> { pub(crate) fn load_from_dotenv_path(dotenv_path: &std::path::Path) -> ksp_core_lib::Result<Self> {
let process = collect_process_environment(std::env::vars_os()); let process = collect_process_environment(std::env::vars_os());
let process = match process { let process = match process {
@@ -245,57 +244,24 @@ impl ConfigEnvironment {
return std::result::Result::Ok(Self { process, dotenv, dotenv_path: dotenv_path.to_path_buf() }); return std::result::Result::Ok(Self { process, dotenv, dotenv_path: dotenv_path.to_path_buf() });
} }
/// Returns the current process values.
pub(crate) const fn process_values(&self) -> &std::collections::BTreeMap<String, String> { pub(crate) const fn process_values(&self) -> &std::collections::BTreeMap<String, String> {
return &self.process; return &self.process;
} }
/// Returns the current dotenv values.
pub(crate) const fn dotenv_values(&self) -> &std::collections::BTreeMap<String, String> { pub(crate) const fn dotenv_values(&self) -> &std::collections::BTreeMap<String, String> {
return &self.dotenv; return &self.dotenv;
} }
/// Builds `ConfigEnvironment` from maps.
#[cfg(test)] #[cfg(test)]
pub(crate) fn from_maps(process: std::collections::BTreeMap<String, String>, dotenv: std::collections::BTreeMap<String, String>) -> Self { pub(crate) fn from_maps(process: std::collections::BTreeMap<String, String>, dotenv: std::collections::BTreeMap<String, String>) -> Self {
return Self { process, dotenv, dotenv_path: std::path::PathBuf::from(DEFAULT_DOTENV_PATH) }; return Self { process, dotenv, dotenv_path: std::path::PathBuf::from(DEFAULT_DOTENV_PATH) };
} }
} }
fn collect_process_environment<I>(values: I) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> /// Executes the crate-internal parse dotenv content operation for the owning module.
where
I: std::iter::IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
{
let mut output = std::collections::BTreeMap::<String, String>::new();
for (name, value) in values {
let name = match name.to_str() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
if !has_supported_namespace(name) {
continue;
}
let validation = validate_supported_variable_name(name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
let value = value.into_string();
let value = match value {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(invalid_environment_value_error(name)),
};
output.insert(name.to_owned(), value);
}
return std::result::Result::Ok(output);
}
fn load_dotenv_file(path: &std::path::Path) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
let content = std::fs::read_to_string(path);
let content = match content {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => return std::result::Result::Ok(std::collections::BTreeMap::new()),
std::result::Result::Err(error) => return std::result::Result::Err(dotenv_read_error(path, error)),
};
return parse_dotenv_content(path, content.as_str());
}
pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> { pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
let mut output = std::collections::BTreeMap::<String, String>::new(); let mut output = std::collections::BTreeMap::<String, String>::new();
for (line_index, raw_line) in content.lines().enumerate() { for (line_index, raw_line) in content.lines().enumerate() {
@@ -343,6 +309,61 @@ pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp
return std::result::Result::Ok(output); return std::result::Result::Ok(output);
} }
/// Validates supported variable name.
pub(crate) fn validate_supported_variable_name(variable_name: &str) -> ksp_core_lib::Result<()> {
if !has_supported_namespace(variable_name) {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable must use the KSP_ or KSPB_ namespace"));
}
let prefix_length = if variable_name.starts_with("KSPB_") { 5 } else { 4 };
if variable_name.len() <= prefix_length {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable namespace must be followed by a name"));
}
for byte in variable_name.bytes() {
let valid = byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_';
if !valid {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable names use uppercase ASCII letters, digits and underscores"));
}
}
return std::result::Result::Ok(());
}
fn collect_process_environment<I>(values: I) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>>
where
I: std::iter::IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
{
let mut output = std::collections::BTreeMap::<String, String>::new();
for (name, value) in values {
let name = match name.to_str() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
if !has_supported_namespace(name) {
continue;
}
let validation = validate_supported_variable_name(name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
let value = value.into_string();
let value = match value {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(invalid_environment_value_error(name)),
};
output.insert(name.to_owned(), value);
}
return std::result::Result::Ok(output);
}
fn load_dotenv_file(path: &std::path::Path) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
let content = std::fs::read_to_string(path);
let content = match content {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => return std::result::Result::Ok(std::collections::BTreeMap::new()),
std::result::Result::Err(error) => return std::result::Result::Err(dotenv_read_error(path, error)),
};
return crate::parse_dotenv_content(path, content.as_str());
}
fn parse_dotenv_value(path: &std::path::Path, line_number: usize, raw_value: &str) -> ksp_core_lib::Result<String> { fn parse_dotenv_value(path: &std::path::Path, line_number: usize, raw_value: &str) -> ksp_core_lib::Result<String> {
if raw_value.starts_with('\'') && (raw_value.len() < 2 || !raw_value.ends_with('\'')) { if raw_value.starts_with('\'') && (raw_value.len() < 2 || !raw_value.ends_with('\'')) {
return std::result::Result::Err(dotenv_syntax_error(path, line_number, "single-quoted value is not terminated")); return std::result::Result::Err(dotenv_syntax_error(path, line_number, "single-quoted value is not terminated"));
@@ -517,23 +538,6 @@ fn escape_json_pointer_token(value: &str) -> String {
return value.replace('~', "~0").replace('/', "~1"); return value.replace('~', "~0").replace('/', "~1");
} }
pub(crate) fn validate_supported_variable_name(variable_name: &str) -> ksp_core_lib::Result<()> {
if !has_supported_namespace(variable_name) {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable must use the KSP_ or KSPB_ namespace"));
}
let prefix_length = if variable_name.starts_with("KSPB_") { 5 } else { 4 };
if variable_name.len() <= prefix_length {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable namespace must be followed by a name"));
}
for byte in variable_name.bytes() {
let valid = byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_';
if !valid {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable names use uppercase ASCII letters, digits and underscores"));
}
}
return std::result::Result::Ok(());
}
fn has_supported_namespace(variable_name: &str) -> bool { fn has_supported_namespace(variable_name: &str) -> bool {
return variable_name.starts_with("KSP_") || variable_name.starts_with("KSPB_"); return variable_name.starts_with("KSP_") || variable_name.starts_with("KSPB_");
} }
@@ -556,7 +560,7 @@ fn is_generic_dotenv_name(variable_name: &str) -> bool {
} }
fn emit_missing_variable_warning(variable_name: &str) { fn emit_missing_variable_warning(variable_name: &str) {
ksp_logging_lib::warn!(target: LOGGING_TARGET, domain = LOGGING_DOMAIN, variable_name = variable_name, "Config environment variable is missing"); ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = LOGGING_DOMAIN, variable_name = variable_name, "Config environment variable is missing");
} }
fn missing_variable_error(variable_name: &str) -> ksp_core_lib::Error { fn missing_variable_error(variable_name: &str) -> ksp_core_lib::Error {

View File

@@ -1,68 +1,47 @@
// file: crates/ksp-config-lib/src/error.rs // file: crates/ksp-config-lib/src/error.rs
// version: 8 // version: 9
/// Error code used when a Config bootstrap argument is missing its value. /// Error code used when a Config bootstrap argument is missing its value.
pub const ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_argument_missing_value"); pub const ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_argument_missing_value");
/// Error code used when a Config bootstrap path is empty, inaccessible, or resolves to an existing non-directory path. /// Error code used when a Config bootstrap path is empty, inaccessible, or resolves to an existing non-directory path.
pub const ERROR_CODE_BOOTSTRAP_INVALID_PATH: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_invalid_path"); pub const ERROR_CODE_BOOTSTRAP_INVALID_PATH: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_invalid_path");
/// Error code used when a logical Config file identifier is malformed.
pub const ERROR_CODE_FILE_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_invalid");
/// Error code used when a requested logical Config file identifier is not registered.
pub const ERROR_CODE_FILE_ID_UNKNOWN: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_unknown");
/// Error code used when the same logical Config file identifier is registered more than once.
pub const ERROR_CODE_FILE_ID_DUPLICATE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_duplicate");
/// Error code used when a Config filename mapping or descriptor relation is invalid.
pub const ERROR_CODE_FILE_MAPPING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_mapping_invalid");
/// Error code used when a Config-managed JSON document or schema cannot be read from its resolved path.
pub const ERROR_CODE_JSON_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_file_read_failed");
/// Error code used when a Config-managed file contains invalid JSON syntax.
pub const ERROR_CODE_JSON_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_syntax_invalid");
/// Error code used when a JSON Schema document is itself invalid for the selected JSON Schema draft.
pub const ERROR_CODE_SCHEMA_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_invalid");
/// Error code used when a Config document does not satisfy its registered JSON Schema.
pub const ERROR_CODE_SCHEMA_VALIDATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_validation_failed");
/// Error code used when a schema-valid Config document violates KSP semantic invariants for its document type.
pub const ERROR_CODE_DOCUMENT_SEMANTIC_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "document_semantic_invalid");
/// Error code used when an explicitly requested Config profile does not exist in a validated document.
pub const ERROR_CODE_PROFILE_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "profile_not_found");
/// Error code used when a composite document references an invalid, unknown, or unsupported Config document. /// Error code used when a composite document references an invalid, unknown, or unsupported Config document.
pub const ERROR_CODE_COMPOSITE_REFERENCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "composite_reference_invalid"); pub const ERROR_CODE_COMPOSITE_REFERENCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "composite_reference_invalid");
/// Error code used when a schema-valid Config document violates KSP semantic invariants for its document type.
pub const ERROR_CODE_DOCUMENT_SEMANTIC_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "document_semantic_invalid");
/// Error code used when the local `.env` file cannot be read for a reason other than absence. /// Error code used when the local `.env` file cannot be read for a reason other than absence.
pub const ERROR_CODE_DOTENV_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "dotenv_file_read_failed"); pub const ERROR_CODE_DOTENV_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "dotenv_file_read_failed");
/// Error code used when the local `.env` file contains syntax Config cannot interpret safely. /// Error code used when the local `.env` file contains syntax Config cannot interpret safely.
pub const ERROR_CODE_DOTENV_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "dotenv_syntax_invalid"); pub const ERROR_CODE_DOTENV_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "dotenv_syntax_invalid");
/// Error code used when a Config environment variable name is malformed or outside the KSP/KSPB namespaces.
pub const ERROR_CODE_ENVIRONMENT_VARIABLE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_variable_invalid");
/// Error code used when a referenced Config environment variable is absent and has no fallback.
pub const ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_variable_missing");
/// Error code used when a supported process environment variable has a value that cannot become a JSON UTF-8 string.
pub const ERROR_CODE_ENVIRONMENT_VALUE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_value_invalid");
/// Error code used when a `${NAME}` / `${NAME:-fallback}` expression is malformed.
pub const ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_placeholder_invalid");
/// Error code used when an environment-resolved Config cannot be mapped safely to a runtime consumer contract. /// Error code used when an environment-resolved Config cannot be mapped safely to a runtime consumer contract.
pub const ERROR_CODE_EFFECTIVE_CONFIG_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "effective_config_invalid"); pub const ERROR_CODE_EFFECTIVE_CONFIG_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "effective_config_invalid");
/// Error code used when a `${NAME}` / `${NAME:-fallback}` expression is malformed.
pub const ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_placeholder_invalid");
/// Error code used when a supported process environment variable has a value that cannot become a JSON UTF-8 string.
pub const ERROR_CODE_ENVIRONMENT_VALUE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_value_invalid");
/// Error code used when a Config environment variable name is malformed or outside the KSP/KSPB namespaces.
pub const ERROR_CODE_ENVIRONMENT_VARIABLE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_variable_invalid");
/// Error code used when a referenced Config environment variable is absent and has no fallback.
pub const ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_variable_missing");
/// Error code used when the same logical Config file identifier is registered more than once.
pub const ERROR_CODE_FILE_ID_DUPLICATE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_duplicate");
/// Error code used when a logical Config file identifier is malformed.
pub const ERROR_CODE_FILE_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_invalid");
/// Error code used when a requested logical Config file identifier is not registered.
pub const ERROR_CODE_FILE_ID_UNKNOWN: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_unknown");
/// Error code used when a Config filename mapping or descriptor relation is invalid.
pub const ERROR_CODE_FILE_MAPPING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_mapping_invalid");
/// Error code used when a Config-managed JSON document or schema cannot be read from its resolved path.
pub const ERROR_CODE_JSON_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_file_read_failed");
/// Error code used when a Config-managed file contains invalid JSON syntax.
pub const ERROR_CODE_JSON_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_syntax_invalid");
/// Error code used when an explicit Config management operation is unsupported or targets the wrong managed resource kind. /// Error code used when an explicit Config management operation is unsupported or targets the wrong managed resource kind.
pub const ERROR_CODE_MANAGEMENT_OPERATION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "management_operation_invalid"); pub const ERROR_CODE_MANAGEMENT_OPERATION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "management_operation_invalid");
/// Error code used when an atomic managed Config or `.env` persistence operation fails before commit. /// Error code used when an atomic managed Config or `.env` persistence operation fails before commit.
pub const ERROR_CODE_PERSISTENCE_WRITE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "persistence_write_failed"); pub const ERROR_CODE_PERSISTENCE_WRITE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "persistence_write_failed");
/// Error code used when an explicitly requested Config profile does not exist in a validated document.
pub const ERROR_CODE_PROFILE_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "profile_not_found");
/// Error code used when a JSON Schema document is itself invalid for the selected JSON Schema draft.
pub const ERROR_CODE_SCHEMA_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_invalid");
/// Error code used when a Config document does not satisfy its registered JSON Schema.
pub const ERROR_CODE_SCHEMA_VALIDATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_validation_failed");

View File

@@ -1,5 +1,6 @@
// file: crates/ksp-config-lib/src/lib.rs // file: crates/ksp-config-lib/src/lib.rs
// version: 10 // version: 14
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -8,11 +9,12 @@
//! //!
//! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and //! 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 //! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and
//! provenance. The standard Logging document maps explicitly to `ksp_logging_lib::LoggingSettings`, while the management surface provides typed Logging //! provenance. Standard Logging and HTTP Transport documents map explicitly to their runtime settings contracts, while the management surface provides
//! mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence. //! typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
mod bootstrap; mod bootstrap;
mod composite; mod composite;
mod constants;
mod document; mod document;
mod environment; mod environment;
mod error; mod error;
@@ -22,6 +24,7 @@ mod persistence;
mod profile; mod profile;
mod registry; mod registry;
mod sensitivity; mod sensitivity;
mod transport;
/// Bootstrap argument used to replace the configuration document root. /// Bootstrap argument used to replace the configuration document root.
pub use self::bootstrap::ARG_CFG_PATH; pub use self::bootstrap::ARG_CFG_PATH;
@@ -141,12 +144,20 @@ pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME;
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME; pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
/// Default physical filename for the standard Logging JSON Schema document. /// Default physical filename for the standard Logging JSON Schema document.
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME; pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
/// Default physical filename for the standard HTTP Transport configuration document.
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
/// Default physical filename for the standard HTTP Transport JSON Schema document.
pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME;
/// Logical file identifier for the generic composite JSON Schema document. /// Logical file identifier for the generic composite JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE; pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
/// Logical file identifier for the standard Logging JSON Schema document. /// Logical file identifier for the standard Logging JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING; pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
/// Logical file identifier for the standard HTTP Transport JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
/// Logical file identifier for the standard Logging configuration document. /// Logical file identifier for the standard Logging configuration document.
pub use self::registry::FILE_ID_STD_LOGGING; pub use self::registry::FILE_ID_STD_LOGGING;
/// Logical file identifier for the standard HTTP Transport configuration document.
pub use self::registry::FILE_ID_STD_TRANSPORT;
/// Sensitivity assigned to one Config value after environment resolution. /// Sensitivity assigned to one Config value after environment resolution.
pub use self::sensitivity::ConfigSensitivity; pub use self::sensitivity::ConfigSensitivity;
/// Provenance segment participating in one resolved Config value. /// Provenance segment participating in one resolved Config value.
@@ -157,3 +168,24 @@ pub use self::sensitivity::REDACTED_CONFIG_VALUE;
pub use self::sensitivity::ResolvedConfigJson; pub use self::sensitivity::ResolvedConfigJson;
/// One resolved Config string preserving real/safe representations and provenance. /// One resolved Config string preserving real/safe representations and provenance.
pub use self::sensitivity::ResolvedConfigText; pub use self::sensitivity::ResolvedConfigText;
/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`.
pub use self::transport::ResolvedTransportConfig;
/// Validates composite document contract.
pub(crate) use self::composite::validate_composite_document_contract;
/// Owning tracing target for events emitted by the Config crate.
pub(crate) use self::constants::TRACING_TARGET;
/// Executes the crate-internal parse dotenv content operation for the owning module.
pub(crate) use self::environment::parse_dotenv_content;
/// Validates supported variable name.
pub(crate) use self::environment::validate_supported_variable_name;
/// Executes the crate-internal atomic write operation for the owning module.
pub(crate) use self::persistence::atomic_write;
/// Executes the crate-internal atomic write private operation for the owning module.
pub(crate) use self::persistence::atomic_write_private;
/// Loads resolved profile with source.
pub(crate) use self::profile::load_resolved_profile_with_source;
/// Validates document profile contract.
pub(crate) use self::profile::validate_document_profile_contract;
/// Executes the crate-internal build registry operation for the owning module.
pub(crate) use self::registry::build_registry;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/management.rs // file: crates/ksp-config-lib/src/management.rs
// version: 4 // version: 6
/// Raw source of one registered Config document read for explicit management/correction. /// Raw source of one registered Config document read for explicit management/correction.
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
@@ -727,7 +727,7 @@ impl ConfigManagement {
/// Authentication/authorization of the human user belongs to the calling application. Calling this method is the explicit Config boundary that opts into /// Authentication/authorization of the human user belongs to the calling application. Calling this method is the explicit Config boundary that opts into
/// real-value access; the returned value must never be logged. /// real-value access; the returned value must never be logged.
pub fn reveal_effective_environment_value(&self, variable_name: &str) -> ksp_core_lib::Result<std::option::Option<String>> { pub fn reveal_effective_environment_value(&self, variable_name: &str) -> ksp_core_lib::Result<std::option::Option<String>> {
let validation = crate::environment::validate_supported_variable_name(variable_name); let validation = crate::validate_supported_variable_name(variable_name);
if let std::result::Result::Err(error) = validation { if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
@@ -744,7 +744,7 @@ impl ConfigManagement {
/// Explicitly reveals the real persisted `.env` value for management display/editing. /// Explicitly reveals the real persisted `.env` value for management display/editing.
pub fn reveal_dotenv_value(&self, variable_name: &str) -> ksp_core_lib::Result<std::option::Option<String>> { pub fn reveal_dotenv_value(&self, variable_name: &str) -> ksp_core_lib::Result<std::option::Option<String>> {
let validation = crate::environment::validate_supported_variable_name(variable_name); let validation = crate::validate_supported_variable_name(variable_name);
if let std::result::Result::Err(error) = validation { if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
@@ -758,7 +758,7 @@ impl ConfigManagement {
/// Creates or updates one supported KSP/KSPB `.env` entry atomically without mutating the inherited process environment. /// Creates or updates one supported KSP/KSPB `.env` entry atomically without mutating the inherited process environment.
pub fn set_dotenv_value(&self, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeReport> { pub fn set_dotenv_value(&self, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeReport> {
let validation = crate::environment::validate_supported_variable_name(variable_name); let validation = crate::validate_supported_variable_name(variable_name);
if let std::result::Result::Err(error) = validation { if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
@@ -781,7 +781,7 @@ impl ConfigManagement {
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
if candidate != content { if candidate != content {
let write = crate::persistence::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes()); let write = crate::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes());
match write { match write {
std::result::Result::Ok(()) => {}, std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -797,7 +797,7 @@ impl ConfigManagement {
/// Removes one supported KSP/KSPB `.env` entry atomically without mutating the inherited process environment. /// Removes one supported KSP/KSPB `.env` entry atomically without mutating the inherited process environment.
pub fn remove_dotenv_value(&self, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeReport> { pub fn remove_dotenv_value(&self, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeReport> {
let validation = crate::environment::validate_supported_variable_name(variable_name); let validation = crate::validate_supported_variable_name(variable_name);
if let std::result::Result::Err(error) = validation { if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
@@ -820,7 +820,7 @@ impl ConfigManagement {
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
if candidate != content { if candidate != content {
let write = crate::persistence::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes()); let write = crate::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes());
match write { match write {
std::result::Result::Ok(()) => {}, std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -838,6 +838,7 @@ impl ConfigManagement {
return crate::ConfigEnvironment::load_from_dotenv_path(self.dotenv_path.as_path()); return crate::ConfigEnvironment::load_from_dotenv_path(self.dotenv_path.as_path());
} }
/// Executes the crate-internal with dotenv path operation for `ConfigManagement`.
#[cfg(test)] #[cfg(test)]
pub(crate) fn with_dotenv_path(engine: crate::ConfigDocumentEngine, dotenv_path: std::path::PathBuf) -> Self { pub(crate) fn with_dotenv_path(engine: crate::ConfigDocumentEngine, dotenv_path: std::path::PathBuf) -> Self {
return Self { engine, dotenv_path }; return Self { engine, dotenv_path };
@@ -921,7 +922,7 @@ fn read_dotenv_source(path: &std::path::Path) -> ksp_core_lib::Result<String> {
let content = std::fs::read_to_string(path); let content = std::fs::read_to_string(path);
return match content { return match content {
std::result::Result::Ok(value) => { std::result::Result::Ok(value) => {
let validation = crate::environment::parse_dotenv_content(path, value.as_str()); let validation = crate::parse_dotenv_content(path, value.as_str());
match validation { match validation {
std::result::Result::Ok(_) => std::result::Result::Ok(value), std::result::Result::Ok(_) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error), std::result::Result::Err(error) => std::result::Result::Err(error),
@@ -937,7 +938,7 @@ fn read_dotenv_source(path: &std::path::Path) -> ksp_core_lib::Result<String> {
} }
fn update_dotenv_source(path: &std::path::Path, source: &str, variable_name: &str, replacement: std::option::Option<&str>) -> ksp_core_lib::Result<String> { fn update_dotenv_source(path: &std::path::Path, source: &str, variable_name: &str, replacement: std::option::Option<&str>) -> ksp_core_lib::Result<String> {
let validation = crate::environment::parse_dotenv_content(path, source); let validation = crate::parse_dotenv_content(path, source);
if let std::result::Result::Err(error) = validation { if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
@@ -967,7 +968,7 @@ fn update_dotenv_source(path: &std::path::Path, source: &str, variable_name: &st
if !candidate.is_empty() { if !candidate.is_empty() {
candidate.push('\n'); candidate.push('\n');
} }
let candidate_validation = crate::environment::parse_dotenv_content(path, candidate.as_str()); let candidate_validation = crate::parse_dotenv_content(path, candidate.as_str());
return match candidate_validation { return match candidate_validation {
std::result::Result::Ok(_) => std::result::Result::Ok(candidate), std::result::Result::Ok(_) => std::result::Result::Ok(candidate),
std::result::Result::Err(error) => std::result::Result::Err(error), std::result::Result::Err(error) => std::result::Result::Err(error),
@@ -1030,7 +1031,7 @@ fn persist_document_source(file_id: &crate::ConfigFileId, path: &std::path::Path
if existing.as_slice() == source { if existing.as_slice() == source {
return std::result::Result::Ok(ConfigDocumentChangeReport { source_changed: false, reload_required: false }); return std::result::Result::Ok(ConfigDocumentChangeReport { source_changed: false, reload_required: false });
} }
let write = crate::persistence::atomic_write(path, source); let write = crate::atomic_write(path, source);
if let std::result::Result::Err(error) = write { if let std::result::Result::Err(error) = write {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }

View File

@@ -1,12 +1,14 @@
// file: crates/ksp-config-lib/src/persistence.rs // file: crates/ksp-config-lib/src/persistence.rs
// version: 2 // version: 4
static NEXT_TEMPORARY_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); static NEXT_TEMPORARY_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
/// Executes the crate-internal atomic write operation for the owning module.
pub(crate) fn atomic_write(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> { pub(crate) fn atomic_write(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return atomic_write_with_policy(path, content, false); return atomic_write_with_policy(path, content, false);
} }
/// Executes the crate-internal atomic write private operation for the owning module.
pub(crate) fn atomic_write_private(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> { pub(crate) fn atomic_write_private(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return atomic_write_with_policy(path, content, true); return atomic_write_with_policy(path, content, true);
} }
@@ -99,7 +101,7 @@ fn cleanup_temporary_file(path: &std::path::Path) {
&& error.kind() != std::io::ErrorKind::NotFound && error.kind() != std::io::ErrorKind::NotFound
{ {
ksp_logging_lib::warn!( ksp_logging_lib::warn!(
target: "ksp-config-lib", target: crate::TRACING_TARGET,
domain = "config.persistence", domain = "config.persistence",
path = %path.to_string_lossy(), path = %path.to_string_lossy(),
error = %error, error = %error,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/profile.rs // file: crates/ksp-config-lib/src/profile.rs
// version: 4 // version: 5
/// Origin of one top-level value in a resolved standard Config profile. /// Origin of one top-level value in a resolved standard Config profile.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -135,6 +135,7 @@ impl crate::ConfigDocumentEngine {
} }
} }
/// Loads resolved profile with source.
pub(crate) fn load_resolved_profile_with_source( pub(crate) fn load_resolved_profile_with_source(
engine: &crate::ConfigDocumentEngine, engine: &crate::ConfigDocumentEngine,
file_id: &crate::ConfigFileId, file_id: &crate::ConfigFileId,
@@ -153,6 +154,7 @@ pub(crate) fn load_resolved_profile_with_source(
return resolve_document_profile(&document, requested_profile, source); return resolve_document_profile(&document, requested_profile, source);
} }
/// Validates document profile contract.
pub(crate) fn validate_document_profile_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> { pub(crate) fn validate_document_profile_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
let root = match document.value().as_object() { let root = match document.value().as_object() {
std::option::Option::Some(value) => value, std::option::Option::Some(value) => value,

View File

@@ -1,20 +1,28 @@
// file: crates/ksp-config-lib/src/registry.rs // file: crates/ksp-config-lib/src/registry.rs
// version: 4 // version: 7
/// Bootstrap argument used to replace a known Config filename mapping. /// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap"; pub const ARG_FILE_MAP: &str = "--filemap";
/// Logical file identifier for the standard Logging configuration document. /// Default physical filename for the generic composite JSON Schema document.
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging"; pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
/// Logical file identifier for the standard Logging JSON Schema document.
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
/// Logical file identifier for the generic composite JSON Schema document.
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
/// Default physical filename for the standard Logging configuration document. /// Default physical filename for the standard Logging configuration document.
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json"; pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
/// Default physical filename for the standard Logging JSON Schema document. /// Default physical filename for the standard Logging JSON Schema document.
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json"; pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
/// Default physical filename for the generic composite JSON Schema document. /// Default physical filename for the standard HTTP Transport configuration document.
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json"; pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
/// Default physical filename for the standard HTTP Transport JSON Schema document.
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
/// Logical file identifier for the generic composite JSON Schema document.
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
/// Logical file identifier for the standard Logging JSON Schema document.
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
/// Logical file identifier for the standard HTTP Transport JSON Schema document.
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
/// Logical file identifier for the standard Logging configuration document.
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
/// Logical file identifier for the standard HTTP Transport configuration document.
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
/// Stable logical identifier for a Config-managed file. /// Stable logical identifier for a Config-managed file.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
@@ -81,6 +89,7 @@ impl ConfigFileDescriptor {
return self.schema_file_id.as_ref(); return self.schema_file_id.as_ref();
} }
/// Creates a new `ConfigFileDescriptor` value.
pub(crate) fn new( pub(crate) fn new(
file_id: &'static str, file_id: &'static str,
kind: ConfigFileKind, kind: ConfigFileKind,
@@ -135,13 +144,29 @@ impl ConfigFileRegistry {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let transport = ConfigFileDescriptor::new(
FILE_ID_STD_TRANSPORT,
ConfigFileKind::Config,
DEFAULT_STD_TRANSPORT_FILENAME,
std::option::Option::Some(FILE_ID_SCHEMA_STD_TRANSPORT),
);
let transport = match transport {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transport_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_TRANSPORT, ConfigFileKind::Schema, DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME, std::option::Option::None);
let transport_schema = match transport_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite_schema = let composite_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None); ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None);
let composite_schema = match composite_schema { let composite_schema = match composite_schema {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
return build_registry([logging, logging_schema, composite_schema]); return crate::build_registry([logging, logging_schema, transport, transport_schema, composite_schema]);
} }
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments. /// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.
@@ -225,6 +250,7 @@ impl ConfigFileRegistry {
} }
} }
/// Executes the crate-internal build registry operation for the owning module.
pub(crate) fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result<ConfigFileRegistry> { pub(crate) fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result<ConfigFileRegistry> {
let mut registry = ConfigFileRegistry { descriptors: std::collections::BTreeMap::new() }; let mut registry = ConfigFileRegistry { descriptors: std::collections::BTreeMap::new() };
for descriptor in descriptors { for descriptor in descriptors {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/sensitivity.rs // file: crates/ksp-config-lib/src/sensitivity.rs
// version: 1 // version: 2
/// Replacement used for secret environment fragments in safe diagnostic representations. /// Replacement used for secret environment fragments in safe diagnostic representations.
pub const REDACTED_CONFIG_VALUE: &str = "********"; pub const REDACTED_CONFIG_VALUE: &str = "********";
@@ -18,7 +18,7 @@ pub enum ConfigSensitivity {
impl ConfigSensitivity { impl ConfigSensitivity {
/// Classifies one supported KSP/KSPB environment variable by its namespace. /// Classifies one supported KSP/KSPB environment variable by its namespace.
pub fn from_variable_name(variable_name: &str) -> ksp_core_lib::Result<Self> { pub fn from_variable_name(variable_name: &str) -> ksp_core_lib::Result<Self> {
let validation = crate::environment::validate_supported_variable_name(variable_name); let validation = crate::validate_supported_variable_name(variable_name);
if let std::result::Result::Err(error) = validation { if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
@@ -103,6 +103,7 @@ pub struct ResolvedConfigText {
} }
impl ResolvedConfigText { impl ResolvedConfigText {
/// Creates a new `ResolvedConfigText` value.
pub(crate) fn new(value: String, safe_value: String, sensitivity: ConfigSensitivity, provenance: std::vec::Vec<ConfigValueProvenance>) -> Self { pub(crate) fn new(value: String, safe_value: String, sensitivity: ConfigSensitivity, provenance: std::vec::Vec<ConfigValueProvenance>) -> Self {
return Self { value, safe_value, sensitivity, provenance }; return Self { value, safe_value, sensitivity, provenance };
} }
@@ -153,6 +154,7 @@ pub struct ResolvedConfigJson {
} }
impl ResolvedConfigJson { impl ResolvedConfigJson {
/// Creates a new `ResolvedConfigJson` value.
pub(crate) fn new( pub(crate) fn new(
value: serde_json::Value, value: serde_json::Value,
safe_value: serde_json::Value, safe_value: serde_json::Value,

View File

@@ -0,0 +1,316 @@
// file: crates/ksp-config-lib/src/transport.rs
// version: 1
/// Effective standard HTTP Transport configuration resolved from Config and mapped to the Transport runtime contract.
#[derive(Clone, Eq, PartialEq)]
pub struct ResolvedTransportConfig {
file_id: crate::ConfigFileId,
source_path: std::path::PathBuf,
profile_id: String,
selection_source: crate::ConfigProfileSelectionSource,
effective: crate::ResolvedConfigJson,
settings: ksp_onchain_transport_lib::HttpTransportSettings,
}
impl ResolvedTransportConfig {
/// Returns the logical Config document identifier used by this runtime configuration.
#[must_use]
pub const fn file_id(&self) -> &crate::ConfigFileId {
return &self.file_id;
}
/// Returns the physical source Config document path.
#[must_use]
pub fn source_path(&self) -> &std::path::Path {
return self.source_path.as_path();
}
/// Returns the selected standard Transport profile identifier.
#[must_use]
pub fn profile_id(&self) -> &str {
return self.profile_id.as_str();
}
/// Returns the source that selected the standard Transport profile.
#[must_use]
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
return self.selection_source;
}
/// Returns the detailed environment-resolved effective Config view.
///
/// The real tree is available to legitimate runtime consumers. The safe tree redacts values originating from `KSP_SECRET_*` or `KSPB_SECRET_*`
/// placeholders and is the only representation used by this type's [`std::fmt::Debug`] implementation.
#[must_use]
pub const fn effective(&self) -> &crate::ResolvedConfigJson {
return &self.effective;
}
/// Returns the validated runtime HTTP Transport settings.
#[must_use]
pub const fn settings(&self) -> &ksp_onchain_transport_lib::HttpTransportSettings {
return &self.settings;
}
/// Consumes this resolved Config and returns the mapped runtime HTTP Transport settings.
#[must_use]
pub fn into_settings(self) -> ksp_onchain_transport_lib::HttpTransportSettings {
return self.settings;
}
}
impl std::fmt::Debug for ResolvedTransportConfig {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("ResolvedTransportConfig")
.field("file_id", &self.file_id)
.field("source_path", &self.source_path)
.field("profile_id", &self.profile_id)
.field("selection_source", &self.selection_source)
.field("effective", &self.effective)
.finish_non_exhaustive();
}
}
impl crate::ConfigDocumentEngine {
/// Loads the standard HTTP Transport document, selects a profile, resolves environment placeholders and maps it to Transport runtime settings.
///
/// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Secret endpoint URLs are allowed
/// because the Transport URL wrapper owns runtime redaction. Invalid environment-resolved values are reported as
/// [`crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID`] without copying endpoint URL values into ordinary error context.
pub fn load_resolved_transport_config(
&self,
requested_profile: std::option::Option<&str>,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedTransportConfig> {
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_TRANSPORT);
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_transport_profile(&profile, environment);
}
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveTransportSource {
format_version: u32,
profile_id: String,
retry: EffectiveRetrySource,
endpoints: std::vec::Vec<EffectiveEndpointSource>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveRetrySource {
max_retries: u32,
initial_backoff_ms: u64,
max_backoff_ms: u64,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveEndpointSource {
name: String,
enabled: bool,
provider: String,
cluster: String,
url: String,
connect_timeout_ms: u64,
request_timeout_ms: u64,
max_idle_connections_per_host: std::option::Option<usize>,
roles: std::vec::Vec<EffectiveRoleSource>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveRoleSource {
role: String,
enabled: bool,
request_kinds: std::vec::Vec<String>,
priority: u32,
limits: EffectiveLimitsSource,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveLimitsSource {
requests_per_second: std::option::Option<u32>,
burst_capacity: std::option::Option<u32>,
max_concurrent_requests: std::option::Option<u32>,
pause_after_rate_limit_ms: std::option::Option<u64>,
}
fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedTransportConfig> {
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 source = serde_json::from_value::<EffectiveTransportSource>(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 Transport 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 Transport format_version is unsupported"));
}
if source.profile_id != profile.profile_id() {
return std::result::Result::Err(effective_error(profile, "effective Transport profile_id does not match the selected profile"));
}
let retry = ksp_onchain_transport_lib::HttpRetrySettings::new(
source.retry.max_retries,
std::time::Duration::from_millis(source.retry.initial_backoff_ms),
std::time::Duration::from_millis(source.retry.max_backoff_ms),
);
let endpoints = map_endpoints(source.endpoints, profile);
let endpoints = match endpoints {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(endpoints, retry);
let validation = settings.validate();
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(transport_contract_error(profile, "effective Transport settings fail the Transport runtime contract", &error));
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
profile_id = profile.profile_id(),
endpoint_count = settings.endpoints().len(),
"mapped standard Transport Config to runtime settings"
);
return std::result::Result::Ok(ResolvedTransportConfig {
file_id: profile.file_id().clone(),
source_path: profile.path().to_path_buf(),
profile_id: profile.profile_id().to_owned(),
selection_source: profile.selection_source(),
effective,
settings,
});
}
fn map_endpoints(
sources: std::vec::Vec<EffectiveEndpointSource>,
profile: &crate::ResolvedConfigProfile,
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::HttpEndpointSettings>> {
let mut endpoints = std::vec::Vec::<ksp_onchain_transport_lib::HttpEndpointSettings>::with_capacity(sources.len());
for source in sources {
let endpoint_name = source.name.clone();
let url = ksp_onchain_transport_lib::HttpEndpointUrl::parse(source.url);
let url = match url {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
transport_contract_error(profile, "effective endpoint URL is invalid", &error).with_context("endpoint_name", endpoint_name),
);
},
};
let roles = map_roles(source.roles, profile, endpoint_name.as_str());
let roles = match roles {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
endpoints.push(ksp_onchain_transport_lib::HttpEndpointSettings::new(
source.name,
source.enabled,
ksp_onchain_transport_lib::HttpProviderName::new(source.provider),
ksp_onchain_transport_lib::HttpClusterName::new(source.cluster),
url,
std::time::Duration::from_millis(source.connect_timeout_ms),
std::time::Duration::from_millis(source.request_timeout_ms),
source.max_idle_connections_per_host,
roles,
));
}
return std::result::Result::Ok(endpoints);
}
fn map_roles(
sources: std::vec::Vec<EffectiveRoleSource>,
profile: &crate::ResolvedConfigProfile,
endpoint_name: &str,
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::HttpEndpointRoleSettings>> {
let mut roles = std::vec::Vec::<ksp_onchain_transport_lib::HttpEndpointRoleSettings>::with_capacity(sources.len());
for source in sources {
let requests_per_second = map_non_zero(source.limits.requests_per_second, profile, "limits.requests_per_second", endpoint_name);
let requests_per_second = match requests_per_second {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let burst_capacity = map_non_zero(source.limits.burst_capacity, profile, "limits.burst_capacity", endpoint_name);
let burst_capacity = match burst_capacity {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let max_concurrent_requests = map_non_zero(source.limits.max_concurrent_requests, profile, "limits.max_concurrent_requests", endpoint_name);
let max_concurrent_requests = match max_concurrent_requests {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pause_after_rate_limit = match source.limits.pause_after_rate_limit_ms {
std::option::Option::Some(value) => std::option::Option::Some(std::time::Duration::from_millis(value)),
std::option::Option::None => std::option::Option::None,
};
let limits = ksp_onchain_transport_lib::HttpRoleLimits::new(requests_per_second, burst_capacity, max_concurrent_requests, pause_after_rate_limit);
let mut request_kinds = std::vec::Vec::<ksp_onchain_transport_lib::HttpRequestKind>::with_capacity(source.request_kinds.len());
for request_kind in source.request_kinds {
request_kinds.push(ksp_onchain_transport_lib::HttpRequestKind::new(request_kind));
}
roles.push(ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
ksp_onchain_transport_lib::HttpRoleName::new(source.role),
source.enabled,
request_kinds,
source.priority,
limits,
));
}
return std::result::Result::Ok(roles);
}
fn map_non_zero(
value: std::option::Option<u32>,
profile: &crate::ResolvedConfigProfile,
field: &'static str,
endpoint_name: &str,
) -> ksp_core_lib::Result<std::option::Option<std::num::NonZeroU32>> {
let value = match value {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let non_zero = std::num::NonZeroU32::new(value);
return match non_zero {
std::option::Option::Some(value) => std::result::Result::Ok(std::option::Option::Some(value)),
std::option::Option::None => std::result::Result::Err(
effective_error(profile, "effective Transport role limit must be greater than zero")
.with_context("field", field)
.with_context("endpoint_name", endpoint_name),
),
};
}
fn transport_contract_error(profile: &crate::ResolvedConfigProfile, reason: &'static str, transport_error: &ksp_core_lib::Error) -> ksp_core_lib::Error {
return effective_error(profile, reason)
.with_context("transport_error_domain", transport_error.code().domain())
.with_context("transport_error_code", transport_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/transport.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/ownership.rs // file: crates/ksp-config-lib/tests/ownership.rs
// version: 2 // version: 3
//! Workspace ownership audits for KSP application configuration boundaries. //! Workspace ownership audits for KSP application configuration boundaries.
@@ -239,7 +239,14 @@ fn workspace_crates_do_not_hardcode_config_managed_physical_files() {
std::result::Result::Ok(value) => non_comment_source(value.as_str()), std::result::Result::Ok(value) => non_comment_source(value.as_str()),
std::result::Result::Err(_) => continue, std::result::Result::Err(_) => continue,
}; };
for token in ["\".env\"", "\"std.logging.json\"", "\"std.logging.schema.json\"", "\"composite.schema.json\""] { for token in [
"\".env\"",
"\"std.logging.json\"",
"\"std.logging.schema.json\"",
"\"std.transport.json\"",
"\"std.transport.schema.json\"",
"\"composite.schema.json\"",
] {
assert!(!source.contains(token), "{} hardcodes Config-managed physical resource {token}; use ksp-config-lib contracts", rust_file.display()); assert!(!source.contains(token), "{} hardcodes Config-managed physical resource {token}; use ksp-config-lib contracts", rust_file.display());
} }
} }

View File

@@ -1,8 +1,8 @@
// file: crates/ksp-config-lib/tests/public_api.rs // file: crates/ksp-config-lib/tests/public_api.rs
// version: 16 // version: 17
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity, Logging-adapter and //! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
//! management contracts. //! Logging/Transport adapters and management contracts.
#[test] #[test]
fn bootstrap_contract_is_available_from_crate_root() { fn bootstrap_contract_is_available_from_crate_root() {
@@ -80,15 +80,18 @@ fn registry_descriptor_inventory_is_available_from_crate_root() {
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}"); assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect(); let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 3); assert_eq!(descriptors.len(), 5);
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING); assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[0].kind(), ksp_config_lib::ConfigFileKind::Config); assert_eq!(descriptors[0].kind(), ksp_config_lib::ConfigFileKind::Config);
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE); assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING); assert_eq!(descriptors[1].kind(), ksp_config_lib::ConfigFileKind::Config);
let schema_file_id = descriptors[0].schema_file_id(); assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert!(schema_file_id.is_some(), "public descriptor inventory should preserve schema association"); assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
let schema_file_id = descriptors[1].schema_file_id();
assert!(schema_file_id.is_some(), "public Transport descriptor should preserve schema association");
if let std::option::Option::Some(schema_file_id) = schema_file_id { 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_LOGGING); assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
} }
} }
} }
@@ -241,3 +244,13 @@ fn management_contracts_are_available_from_crate_root() {
let _ = (save_source_candidate, reveal_effective, reveal_dotenv); let _ = (save_source_candidate, reveal_effective, reveal_dotenv);
assert_ne!(ksp_config_lib::ERROR_CODE_MANAGEMENT_OPERATION_INVALID, ksp_config_lib::ERROR_CODE_PERSISTENCE_WRITE_FAILED); assert_ne!(ksp_config_lib::ERROR_CODE_MANAGEMENT_OPERATION_INVALID, ksp_config_lib::ERROR_CODE_PERSISTENCE_WRITE_FAILED);
} }
#[test]
fn transport_adapter_contract_is_available_from_crate_root() {
let _loader = ksp_config_lib::ConfigDocumentEngine::load_resolved_transport_config;
assert!(std::mem::size_of::<ksp_config_lib::ResolvedTransportConfig>() > 0);
assert_eq!(ksp_config_lib::FILE_ID_STD_TRANSPORT, "cfg.std.transport");
assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT, "schema.std.transport");
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_FILENAME, "std.transport.json");
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME, "std.transport.schema.json");
}

View File

@@ -0,0 +1,32 @@
// file: crates/ksp-config-lib/tests/transport_devnet_smoke.rs
// version: 1
//! Opt-in live Devnet smoke for the Config -> Transport foundation path.
#[tokio::test(flavor = "current_thread")]
#[ignore = "opt-in live Solana Devnet smoke; performs external network requests"]
async fn committed_devnet_transport_profile_reaches_all_foundation_canaries() {
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"))
.expect("committed Config roots must be valid");
let registry = ksp_config_lib::ConfigFileRegistry::defaults().expect("default Config registry must be valid");
let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry);
let environment = ksp_config_lib::ConfigEnvironment::load().expect("Config must capture the opt-in smoke environment");
let resolved = engine
.load_resolved_transport_config(std::option::Option::Some("devnet_public"), &environment)
.expect("committed devnet_public Transport profile must resolve");
assert_eq!(resolved.profile_id(), "devnet_public");
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(resolved.into_settings()).expect("resolved Transport settings must construct the HTTP pool");
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
let health = pool.get_health(&role).await.expect("Devnet getHealth smoke must succeed");
assert_eq!(health, ksp_onchain_transport_lib::SolanaNodeHealth::Healthy);
let genesis_hash = pool.get_genesis_hash(&role).await.expect("Devnet getGenesisHash smoke must succeed");
assert!(!genesis_hash.as_str().is_empty());
let version = pool.get_version(&role).await.expect("Devnet getVersion smoke must succeed");
assert!(!version.solana_core().is_empty());
let balance = pool
.get_balance(&role, &ksp_core_lib::PRGIDPK_SOLANA_SYSTEM, std::option::Option::None)
.await
.expect("Devnet getBalance smoke must succeed");
assert!(balance.context().slot() > 0);
}

View File

@@ -1,39 +1,39 @@
// file: crates/ksp-config-lib/unit_tests/bootstrap.rs // file: crates/ksp-config-lib/unit_tests/bootstrap.rs
// version: 1 // version: 2
#[test] #[test]
fn defaults_use_hardcoded_ksp_roots() { fn defaults_use_hardcoded_ksp_roots() {
let result = super::ConfigBootstrapOptions::defaults(); let result = crate::ConfigBootstrapOptions::defaults();
assert!(result.is_ok(), "default bootstrap paths should be valid: {result:?}"); assert!(result.is_ok(), "default bootstrap paths should be valid: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH)); assert_eq!(options.cfg_path(), std::path::Path::new(crate::DEFAULT_CFG_PATH));
assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH)); assert_eq!(options.schema_path(), std::path::Path::new(crate::DEFAULT_SCHEMA_PATH));
} }
} }
#[test] #[test]
fn cfg_path_override_keeps_schema_default() { fn cfg_path_override_keeps_schema_default() {
let defaults = super::ConfigBootstrapOptions::defaults(); let defaults = crate::ConfigBootstrapOptions::defaults();
assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}"); assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}");
if let std::result::Result::Ok(options) = defaults { if let std::result::Result::Ok(options) = defaults {
let result = options.with_cfg_path("custom-config"); let result = options.with_cfg_path("custom-config");
assert!(result.is_ok(), "cfg path override should be valid: {result:?}"); assert!(result.is_ok(), "cfg path override should be valid: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new("custom-config")); assert_eq!(options.cfg_path(), std::path::Path::new("custom-config"));
assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH)); assert_eq!(options.schema_path(), std::path::Path::new(crate::DEFAULT_SCHEMA_PATH));
} }
} }
} }
#[test] #[test]
fn schema_path_override_keeps_cfg_default() { fn schema_path_override_keeps_cfg_default() {
let defaults = super::ConfigBootstrapOptions::defaults(); let defaults = crate::ConfigBootstrapOptions::defaults();
assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}"); assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}");
if let std::result::Result::Ok(options) = defaults { if let std::result::Result::Ok(options) = defaults {
let result = options.with_schema_path("custom-schemas"); let result = options.with_schema_path("custom-schemas");
assert!(result.is_ok(), "schema path override should be valid: {result:?}"); assert!(result.is_ok(), "schema path override should be valid: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH)); assert_eq!(options.cfg_path(), std::path::Path::new(crate::DEFAULT_CFG_PATH));
assert_eq!(options.schema_path(), std::path::Path::new("custom-schemas")); assert_eq!(options.schema_path(), std::path::Path::new("custom-schemas"));
} }
} }
@@ -42,21 +42,21 @@ fn schema_path_override_keeps_cfg_default() {
#[test] #[test]
fn cfg_cli_override_keeps_schema_default() { fn cfg_cli_override_keeps_schema_default() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath=cli-config")]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath=cli-config")];
let result = super::ConfigBootstrapOptions::from_args(&args); let result = crate::ConfigBootstrapOptions::from_args(&args);
assert!(result.is_ok(), "cfg CLI override should parse: {result:?}"); assert!(result.is_ok(), "cfg CLI override should parse: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new("cli-config")); assert_eq!(options.cfg_path(), std::path::Path::new("cli-config"));
assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH)); assert_eq!(options.schema_path(), std::path::Path::new(crate::DEFAULT_SCHEMA_PATH));
} }
} }
#[test] #[test]
fn schema_cli_override_keeps_cfg_default() { fn schema_cli_override_keeps_cfg_default() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=cli-schemas")]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=cli-schemas")];
let result = super::ConfigBootstrapOptions::from_args(&args); let result = crate::ConfigBootstrapOptions::from_args(&args);
assert!(result.is_ok(), "schema CLI override should parse: {result:?}"); assert!(result.is_ok(), "schema CLI override should parse: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH)); assert_eq!(options.cfg_path(), std::path::Path::new(crate::DEFAULT_CFG_PATH));
assert_eq!(options.schema_path(), std::path::Path::new("cli-schemas")); assert_eq!(options.schema_path(), std::path::Path::new("cli-schemas"));
} }
} }
@@ -73,7 +73,7 @@ fn parser_accepts_inline_and_separate_forms_and_last_value_wins() {
std::ffi::OsString::from("--schemapath"), std::ffi::OsString::from("--schemapath"),
std::ffi::OsString::from("second-schemas"), std::ffi::OsString::from("second-schemas"),
]; ];
let result = super::ConfigBootstrapOptions::from_args(&args); let result = crate::ConfigBootstrapOptions::from_args(&args);
assert!(result.is_ok(), "bootstrap arguments should parse: {result:?}"); assert!(result.is_ok(), "bootstrap arguments should parse: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new("second-config")); assert_eq!(options.cfg_path(), std::path::Path::new("second-config"));
@@ -84,7 +84,7 @@ fn parser_accepts_inline_and_separate_forms_and_last_value_wins() {
#[test] #[test]
fn parser_reports_missing_separate_value() { fn parser_reports_missing_separate_value() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath")]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath")];
let result = super::ConfigBootstrapOptions::from_args(&args); let result = crate::ConfigBootstrapOptions::from_args(&args);
assert!(result.is_err(), "missing value must be rejected"); assert!(result.is_err(), "missing value must be rejected");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE); assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE);
@@ -94,7 +94,7 @@ fn parser_reports_missing_separate_value() {
#[test] #[test]
fn parser_reports_another_option_as_missing_separate_value() { fn parser_reports_another_option_as_missing_separate_value() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath"), std::ffi::OsString::from("--other-option")]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath"), std::ffi::OsString::from("--other-option")];
let result = super::ConfigBootstrapOptions::from_args(&args); let result = crate::ConfigBootstrapOptions::from_args(&args);
assert!(result.is_err(), "another option must not become a bootstrap path value"); assert!(result.is_err(), "another option must not become a bootstrap path value");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE); assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE);
@@ -104,7 +104,7 @@ fn parser_reports_another_option_as_missing_separate_value() {
#[test] #[test]
fn empty_inline_path_is_rejected() { fn empty_inline_path_is_rejected() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=")]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=")];
let result = super::ConfigBootstrapOptions::from_args(&args); let result = crate::ConfigBootstrapOptions::from_args(&args);
assert!(result.is_err(), "empty path must be rejected"); assert!(result.is_err(), "empty path must be rejected");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH); assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH);
@@ -113,7 +113,7 @@ fn empty_inline_path_is_rejected() {
#[test] #[test]
fn explicit_programmatic_paths_do_not_depend_on_default_roots() { fn explicit_programmatic_paths_do_not_depend_on_default_roots() {
let result = super::ConfigBootstrapOptions::from_paths("programmatic-config", "programmatic-schemas"); let result = crate::ConfigBootstrapOptions::from_paths("programmatic-config", "programmatic-schemas");
assert!(result.is_ok(), "explicit programmatic paths should be valid: {result:?}"); assert!(result.is_ok(), "explicit programmatic paths should be valid: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new("programmatic-config")); assert_eq!(options.cfg_path(), std::path::Path::new("programmatic-config"));
@@ -126,7 +126,7 @@ fn existing_non_directory_path_is_rejected() {
let fixture = unique_fixture_path("existing-file"); let fixture = unique_fixture_path("existing-file");
let create = std::fs::write(fixture.as_path(), b"fixture"); let create = std::fs::write(fixture.as_path(), b"fixture");
assert!(create.is_ok(), "fixture file should be creatable: {create:?}"); assert!(create.is_ok(), "fixture file should be creatable: {create:?}");
let result = super::ConfigBootstrapOptions::from_paths(fixture.as_path(), "programmatic-schemas"); let result = crate::ConfigBootstrapOptions::from_paths(fixture.as_path(), "programmatic-schemas");
let remove = std::fs::remove_file(fixture.as_path()); let remove = std::fs::remove_file(fixture.as_path());
assert!(remove.is_ok(), "fixture file should be removable: {remove:?}"); assert!(remove.is_ok(), "fixture file should be removable: {remove:?}");
assert!(result.is_err(), "existing file must not be accepted as a bootstrap directory"); assert!(result.is_err(), "existing file must not be accepted as a bootstrap directory");

View File

@@ -1,8 +1,8 @@
// file: crates/ksp-config-lib/unit_tests/composite.rs // file: crates/ksp-config-lib/unit_tests/composite.rs
// version: 3 // version: 4
const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test";
const TEST_COMPOSITE_FILENAME: &str = "examples/composite.example.json"; const TEST_COMPOSITE_FILENAME: &str = "examples/composite.example.json";
const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test";
#[test] #[test]
fn fixture_composite_example_resolves_default_document_profile() { fn fixture_composite_example_resolves_default_document_profile() {
@@ -197,7 +197,7 @@ fn test_registry(composite_filename: &'static str) -> ksp_core_lib::Result<crate
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
return crate::registry::build_registry([logging, logging_schema, composite_schema, composite]); return crate::build_registry([logging, logging_schema, composite_schema, composite]);
} }
fn prepare_fixture(fixture: &FixtureRoots, composite: &str) -> std::io::Result<()> { fn prepare_fixture(fixture: &FixtureRoots, composite: &str) -> std::io::Result<()> {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/document.rs // file: crates/ksp-config-lib/unit_tests/document.rs
// version: 3 // version: 4
#[test] #[test]
fn committed_logging_document_passes_registered_schema_and_semantic_validation() { fn committed_logging_document_passes_registered_schema_and_semantic_validation() {
@@ -11,7 +11,7 @@ fn committed_logging_document_passes_registered_schema_and_semantic_validation()
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}"); assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(bootstrap), std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (bootstrap, registry, file_id) { if let (std::result::Result::Ok(bootstrap), std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (bootstrap, registry, file_id) {
let engine = super::ConfigDocumentEngine::new(bootstrap, registry); let engine = crate::ConfigDocumentEngine::new(bootstrap, registry);
let document = engine.load_validated_document(&file_id); let document = engine.load_validated_document(&file_id);
assert!(document.is_ok(), "committed std.logging.json should validate: {document:?}"); assert!(document.is_ok(), "committed std.logging.json should validate: {document:?}");
if let std::result::Result::Ok(document) = document { if let std::result::Result::Ok(document) = document {
@@ -228,7 +228,7 @@ fn valid_logging_profile(profile_id: &str, output_id: &str) -> String {
); );
} }
fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result<super::ConfigJsonDocument> { fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result<crate::ConfigJsonDocument> {
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.as_path()); let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.as_path());
let bootstrap = match bootstrap { let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -244,7 +244,7 @@ fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result<super::ConfigJso
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let engine = super::ConfigDocumentEngine::new(bootstrap, registry); let engine = crate::ConfigDocumentEngine::new(bootstrap, registry);
return engine.load_validated_document(&file_id); return engine.load_validated_document(&file_id);
} }
@@ -278,7 +278,7 @@ fn valid_minimal_logging_schema() -> &'static str {
}"#; }"#;
} }
fn assert_error_code(result: ksp_core_lib::Result<super::ConfigJsonDocument>, expected: ksp_core_lib::ErrorCode) { fn assert_error_code(result: ksp_core_lib::Result<crate::ConfigJsonDocument>, expected: ksp_core_lib::ErrorCode) {
assert!(result.is_err(), "fixture should fail with {expected:?}: {result:?}"); assert!(result.is_err(), "fixture should fail with {expected:?}: {result:?}");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), expected); assert_eq!(error.code(), expected);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/environment.rs // file: crates/ksp-config-lib/unit_tests/environment.rs
// version: 5 // version: 6
#[test] #[test]
fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() { fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() {
@@ -7,7 +7,7 @@ fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() {
process.insert("KSP_LOGS_DIRECTORY".to_owned(), String::new()); process.insert("KSP_LOGS_DIRECTORY".to_owned(), String::new());
let mut dotenv = std::collections::BTreeMap::<String, String>::new(); let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "dotenv-logs".to_owned()); dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "dotenv-logs".to_owned());
let environment = super::ConfigEnvironment::from_maps(process, dotenv); let environment = crate::ConfigEnvironment::from_maps(process, dotenv);
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs")); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs"));
assert!(resolved.is_ok(), "process value should resolve"); assert!(resolved.is_ok(), "process value should resolve");
let resolved = match resolved { let resolved = match resolved {
@@ -15,7 +15,7 @@ fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() {
std::result::Result::Err(_) => return, std::result::Result::Err(_) => return,
}; };
assert_eq!(resolved.value(), ""); assert_eq!(resolved.value(), "");
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Process); assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::Process);
} }
#[test] #[test]
@@ -23,7 +23,7 @@ fn dotenv_wins_over_fallback_when_process_value_is_absent() {
let process = std::collections::BTreeMap::<String, String>::new(); let process = std::collections::BTreeMap::<String, String>::new();
let mut dotenv = std::collections::BTreeMap::<String, String>::new(); let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "dotenv-logs".to_owned()); dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "dotenv-logs".to_owned());
let environment = super::ConfigEnvironment::from_maps(process, dotenv); let environment = crate::ConfigEnvironment::from_maps(process, dotenv);
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs")); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs"));
assert!(resolved.is_ok(), "dotenv value should resolve"); assert!(resolved.is_ok(), "dotenv value should resolve");
let resolved = match resolved { let resolved = match resolved {
@@ -31,25 +31,25 @@ fn dotenv_wins_over_fallback_when_process_value_is_absent() {
std::result::Result::Err(_) => return, std::result::Result::Err(_) => return,
}; };
assert_eq!(resolved.value(), "dotenv-logs"); assert_eq!(resolved.value(), "dotenv-logs");
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::DotEnv); assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::DotEnv);
} }
#[test] #[test]
fn empty_dotenv_value_is_defined_and_beats_fallback() { fn empty_dotenv_value_is_defined_and_beats_fallback() {
let mut dotenv = std::collections::BTreeMap::<String, String>::new(); let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), String::new()); dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), String::new());
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs")); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs"));
assert!(resolved.is_ok(), "empty dotenv value should resolve"); assert!(resolved.is_ok(), "empty dotenv value should resolve");
if let std::result::Result::Ok(resolved) = resolved { if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.value(), ""); assert_eq!(resolved.value(), "");
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::DotEnv); assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::DotEnv);
} }
} }
#[test] #[test]
fn fallback_is_used_only_when_external_sources_are_absent() { fn fallback_is_used_only_when_external_sources_are_absent() {
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs")); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs"));
assert!(resolved.is_ok(), "fallback should resolve"); assert!(resolved.is_ok(), "fallback should resolve");
let resolved = match resolved { let resolved = match resolved {
@@ -57,12 +57,12 @@ fn fallback_is_used_only_when_external_sources_are_absent() {
std::result::Result::Err(_) => return, std::result::Result::Err(_) => return,
}; };
assert_eq!(resolved.value(), "fallback-logs"); assert_eq!(resolved.value(), "fallback-logs");
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Fallback); assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::Fallback);
} }
#[test] #[test]
fn missing_variable_without_fallback_is_a_distinct_error() { fn missing_variable_without_fallback_is_a_distinct_error() {
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::None); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::None);
let error = match resolved { let error = match resolved {
std::result::Result::Ok(_) => return, std::result::Result::Ok(_) => return,
@@ -78,7 +78,7 @@ fn ksp_and_kspb_namespaces_are_supported_but_external_names_are_rejected() {
process.insert("KSP_LOGS_DIRECTORY".to_owned(), "logs".to_owned()); process.insert("KSP_LOGS_DIRECTORY".to_owned(), "logs".to_owned());
let bot_variable = ["KSPB_", "TEST_KEY"].concat(); let bot_variable = ["KSPB_", "TEST_KEY"].concat();
process.insert(bot_variable.clone(), "hidden".to_owned()); process.insert(bot_variable.clone(), "hidden".to_owned());
let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
assert!(environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::None).is_ok()); assert!(environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::None).is_ok());
assert!(environment.resolve_variable(bot_variable.as_str(), std::option::Option::None).is_ok()); assert!(environment.resolve_variable(bot_variable.as_str(), std::option::Option::None).is_ok());
let external = environment.resolve_variable("OTHER_NETWORK", std::option::Option::None); let external = environment.resolve_variable("OTHER_NETWORK", std::option::Option::None);
@@ -91,7 +91,7 @@ fn ksp_and_kspb_namespaces_are_supported_but_external_names_are_rejected() {
#[test] #[test]
fn text_resolver_supports_multiple_placeholders_and_literal_fallbacks() { fn text_resolver_supports_multiple_placeholders_and_literal_fallbacks() {
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = environment.resolve_text("logs=${KSP_LOGS_DIRECTORY:-logs};second=${KSP_LOGS_DIRECTORY:-other}"); let resolved = environment.resolve_text("logs=${KSP_LOGS_DIRECTORY:-logs};second=${KSP_LOGS_DIRECTORY:-other}");
assert!(resolved.is_ok(), "multiple placeholders should resolve"); assert!(resolved.is_ok(), "multiple placeholders should resolve");
if let std::result::Result::Ok(resolved) = resolved { if let std::result::Result::Ok(resolved) = resolved {
@@ -101,7 +101,7 @@ fn text_resolver_supports_multiple_placeholders_and_literal_fallbacks() {
#[test] #[test]
fn malformed_or_nested_placeholders_are_rejected() { fn malformed_or_nested_placeholders_are_rejected() {
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let unclosed = environment.resolve_text("${KSP_LOGS_DIRECTORY"); let unclosed = environment.resolve_text("${KSP_LOGS_DIRECTORY");
let unclosed = match unclosed { let unclosed = match unclosed {
std::result::Result::Ok(_) => return, std::result::Result::Ok(_) => return,
@@ -120,7 +120,7 @@ fn malformed_or_nested_placeholders_are_rejected() {
fn json_resolver_walks_objects_and_arrays_without_changing_keys() { fn json_resolver_walks_objects_and_arrays_without_changing_keys() {
let mut dotenv = std::collections::BTreeMap::<String, String>::new(); let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "runtime-logs".to_owned()); dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "runtime-logs".to_owned());
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
let source = serde_json::json!({"path": "${KSP_LOGS_DIRECTORY}", "items": [1, "${KSP_LOGS_DIRECTORY}"], "enabled": true}); let source = serde_json::json!({"path": "${KSP_LOGS_DIRECTORY}", "items": [1, "${KSP_LOGS_DIRECTORY}"], "enabled": true});
let resolved = environment.resolve_json(&source); let resolved = environment.resolve_json(&source);
assert!(resolved.is_ok(), "recursive JSON resolution should succeed"); assert!(resolved.is_ok(), "recursive JSON resolution should succeed");
@@ -137,7 +137,7 @@ fn json_resolver_walks_objects_and_arrays_without_changing_keys() {
fn dotenv_parser_supports_comments_export_quotes_empty_values_and_ignores_external_keys() { fn dotenv_parser_supports_comments_export_quotes_empty_values_and_ignores_external_keys() {
let path = std::path::Path::new("fixture.env"); let path = std::path::Path::new("fixture.env");
let content = "# comment\nexport KSP_LOGS_DIRECTORY = 'quoted logs'\nOTHER_TOOL=value\n"; let content = "# comment\nexport KSP_LOGS_DIRECTORY = 'quoted logs'\nOTHER_TOOL=value\n";
let parsed = super::parse_dotenv_content(path, content); let parsed = crate::parse_dotenv_content(path, content);
assert!(parsed.is_ok(), "dotenv fixture should parse"); assert!(parsed.is_ok(), "dotenv fixture should parse");
let parsed = match parsed { let parsed = match parsed {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -149,7 +149,7 @@ fn dotenv_parser_supports_comments_export_quotes_empty_values_and_ignores_extern
#[test] #[test]
fn dotenv_duplicate_ksp_key_is_rejected() { fn dotenv_duplicate_ksp_key_is_rejected() {
let parsed = super::parse_dotenv_content(std::path::Path::new("fixture.env"), "KSP_LOGS_DIRECTORY=one\nKSP_LOGS_DIRECTORY=two\n"); let parsed = crate::parse_dotenv_content(std::path::Path::new("fixture.env"), "KSP_LOGS_DIRECTORY=one\nKSP_LOGS_DIRECTORY=two\n");
let error = match parsed { let error = match parsed {
std::result::Result::Ok(_) => return, std::result::Result::Ok(_) => return,
std::result::Result::Err(error) => error, std::result::Result::Err(error) => error,
@@ -199,7 +199,7 @@ fn logging_fixture_profile_resolves_environment_fallback_without_changing_source
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return, std::result::Result::Err(_) => return,
}; };
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let effective = profile.resolve_effective_environment(&environment); let effective = profile.resolve_effective_environment(&environment);
assert!(effective.is_ok(), "committed Logging environment fallback should resolve"); assert!(effective.is_ok(), "committed Logging environment fallback should resolve");
let effective = match effective { let effective = match effective {
@@ -245,7 +245,7 @@ fn secret_environment_value_keeps_real_value_but_redacts_safe_and_debug_views()
let canary = "KSP_SECRET_CANARY_91b7c6"; let canary = "KSP_SECRET_CANARY_91b7c6";
let mut process = std::collections::BTreeMap::<String, String>::new(); let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_TEST_TOKEN".to_owned(), canary.to_owned()); process.insert("KSP_SECRET_TEST_TOKEN".to_owned(), canary.to_owned());
let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let resolved = environment.resolve_variable("KSP_SECRET_TEST_TOKEN", std::option::Option::None); let resolved = environment.resolve_variable("KSP_SECRET_TEST_TOKEN", std::option::Option::None);
assert!(resolved.is_ok(), "secret process value should resolve"); assert!(resolved.is_ok(), "secret process value should resolve");
let resolved = match resolved { let resolved = match resolved {
@@ -255,7 +255,7 @@ fn secret_environment_value_keeps_real_value_but_redacts_safe_and_debug_views()
assert_eq!(resolved.value(), canary); assert_eq!(resolved.value(), canary);
assert_eq!(resolved.safe_value(), crate::REDACTED_CONFIG_VALUE); assert_eq!(resolved.safe_value(), crate::REDACTED_CONFIG_VALUE);
assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret); assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret);
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Process); assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::Process);
let debug = format!("{resolved:?}"); let debug = format!("{resolved:?}");
assert!(!debug.contains(canary), "Debug must not reveal the secret canary"); assert!(!debug.contains(canary), "Debug must not reveal the secret canary");
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE)); assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
@@ -267,7 +267,7 @@ fn detailed_text_redacts_only_secret_segments_and_keeps_ordered_provenance() {
let mut process = std::collections::BTreeMap::<String, String>::new(); let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_PUBLIC_HOST".to_owned(), "rpc.example.test".to_owned()); process.insert("KSP_PUBLIC_HOST".to_owned(), "rpc.example.test".to_owned());
process.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned()); process.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned());
let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let resolved = environment.resolve_text_detailed("https://${KSP_PUBLIC_HOST}/?token=${KSP_SECRET_TOKEN}"); let resolved = environment.resolve_text_detailed("https://${KSP_PUBLIC_HOST}/?token=${KSP_SECRET_TOKEN}");
assert!(resolved.is_ok(), "composed secret URL should resolve"); assert!(resolved.is_ok(), "composed secret URL should resolve");
let resolved = match resolved { let resolved = match resolved {
@@ -288,7 +288,7 @@ fn detailed_text_redacts_only_secret_segments_and_keeps_ordered_provenance() {
#[test] #[test]
fn secret_fallback_inherits_secret_sensitivity_and_is_redacted() { fn secret_fallback_inherits_secret_sensitivity_and_is_redacted() {
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = environment.resolve_text_detailed("token=${KSP_SECRET_TOKEN:-false-secret}"); let resolved = environment.resolve_text_detailed("token=${KSP_SECRET_TOKEN:-false-secret}");
assert!(resolved.is_ok(), "secret fallback should resolve"); assert!(resolved.is_ok(), "secret fallback should resolve");
let resolved = match resolved { let resolved = match resolved {
@@ -298,7 +298,7 @@ fn secret_fallback_inherits_secret_sensitivity_and_is_redacted() {
assert_eq!(resolved.value(), "token=false-secret"); assert_eq!(resolved.value(), "token=false-secret");
assert_eq!(resolved.safe_value(), "token=********"); assert_eq!(resolved.safe_value(), "token=********");
assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret); assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret);
assert_eq!(resolved.provenance()[1].environment_source(), std::option::Option::Some(super::ConfigEnvironmentSource::Fallback)); assert_eq!(resolved.provenance()[1].environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Fallback));
} }
#[test] #[test]
@@ -306,7 +306,7 @@ fn detailed_json_preserves_safe_tree_sensitivity_and_pointer_provenance() {
let secret = "nested-secret-canary-2d11"; let secret = "nested-secret-canary-2d11";
let mut dotenv = std::collections::BTreeMap::<String, String>::new(); let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned()); dotenv.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned());
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
let source = serde_json::json!({"transport": {"url": "https://host/?token=${KSP_SECRET_TOKEN}"}, "items": ["plain", 7]}); let source = serde_json::json!({"transport": {"url": "https://host/?token=${KSP_SECRET_TOKEN}"}, "items": ["plain", 7]});
let resolved = environment.resolve_json_detailed(&source); let resolved = environment.resolve_json_detailed(&source);
assert!(resolved.is_ok(), "detailed JSON should resolve"); assert!(resolved.is_ok(), "detailed JSON should resolve");
@@ -349,7 +349,7 @@ fn detailed_fixture_profile_environment_keeps_global_origin_and_adds_environment
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return, std::result::Result::Err(_) => return,
}; };
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let effective = profile.resolve_effective_environment_detailed(&environment); let effective = profile.resolve_effective_environment_detailed(&environment);
assert!(effective.is_ok(), "detailed committed Logging profile should resolve"); assert!(effective.is_ok(), "detailed committed Logging profile should resolve");
let effective = match effective { let effective = match effective {

View File

@@ -0,0 +1,40 @@
{
"format_version": 1,
"retry": {
"max_retries": 4,
"initial_backoff_ms": 125,
"max_backoff_ms": 2500
},
"default_profile": "secret_test",
"profiles": [
{
"profile_id": "secret_test",
"endpoints": [
{
"name": "fixture_private",
"enabled": true,
"provider": "fixture-provider",
"cluster": "fixture-cluster",
"url": "${KSP_SECRET_TRANSPORT_TEST_URL:-https://fallback.invalid}",
"connect_timeout_ms": 750,
"request_timeout_ms": 2500,
"max_idle_connections_per_host": 3,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"priority": 7,
"limits": {
"requests_per_second": 9,
"burst_capacity": 12,
"max_concurrent_requests": 4,
"pause_after_rate_limit_ms": 650
}
}
]
}
]
}
]
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/profile.rs // file: crates/ksp-config-lib/unit_tests/profile.rs
// version: 2 // version: 3
#[test] #[test]
fn fixture_default_profile_resolves_globals_profile_and_provenance() { fn fixture_default_profile_resolves_globals_profile_and_provenance() {
@@ -15,9 +15,9 @@ fn fixture_default_profile_resolves_globals_profile_and_provenance() {
if let (std::result::Result::Ok(document), std::result::Result::Ok(resolved)) = (document, resolved) { if let (std::result::Result::Ok(document), std::result::Result::Ok(resolved)) = (document, resolved) {
let default_profile = document.value().get("default_profile").and_then(serde_json::Value::as_str); let default_profile = document.value().get("default_profile").and_then(serde_json::Value::as_str);
assert_eq!(default_profile, std::option::Option::Some(resolved.profile_id())); assert_eq!(default_profile, std::option::Option::Some(resolved.profile_id()));
assert_eq!(resolved.selection_source(), super::ConfigProfileSelectionSource::DefaultProfile); assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
assert_eq!(resolved.origin("logs_directory"), std::option::Option::Some(super::ConfigValueOrigin::Global)); assert_eq!(resolved.origin("logs_directory"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
assert_eq!(resolved.origin("default_filter"), std::option::Option::Some(super::ConfigValueOrigin::Profile)); assert_eq!(resolved.origin("default_filter"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
assert_eq!( assert_eq!(
resolved.profile().get("default_filter").and_then(serde_json::Value::as_str), resolved.profile().get("default_filter").and_then(serde_json::Value::as_str),
resolved.effective().get("default_filter").and_then(serde_json::Value::as_str), resolved.effective().get("default_filter").and_then(serde_json::Value::as_str),
@@ -45,7 +45,7 @@ fn explicit_profile_selection_is_distinct_from_default_selection() {
assert!(resolved.is_ok(), "explicit committed profile should resolve: {resolved:?}"); assert!(resolved.is_ok(), "explicit committed profile should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved { if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.profile_id(), profile_id); assert_eq!(resolved.profile_id(), profile_id);
assert_eq!(resolved.selection_source(), super::ConfigProfileSelectionSource::Explicit); assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
} }
} }
} }

View File

@@ -1,39 +1,49 @@
// file: crates/ksp-config-lib/unit_tests/registry.rs // file: crates/ksp-config-lib/unit_tests/registry.rs
// version: 4 // version: 6
#[test] #[test]
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() { fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&super::ConfigFileDescriptor> = registry.descriptors().collect(); let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 3); assert_eq!(descriptors.len(), 5);
assert_eq!(descriptors[0].file_id().as_str(), super::FILE_ID_STD_LOGGING); assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[0].kind(), super::ConfigFileKind::Config); assert_eq!(descriptors[0].kind(), crate::ConfigFileKind::Config);
assert_eq!(descriptors[0].filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_FILENAME)); assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_STD_LOGGING_FILENAME));
let schema_file_id = descriptors[0].schema_file_id(); let logging_schema_file_id = descriptors[0].schema_file_id();
assert!(schema_file_id.is_some(), "logging descriptor should expose its validation schema"); assert!(logging_schema_file_id.is_some(), "logging descriptor should expose its validation schema");
if let std::option::Option::Some(schema_file_id) = schema_file_id { if let std::option::Option::Some(schema_file_id) = logging_schema_file_id {
assert_eq!(schema_file_id.as_str(), super::FILE_ID_SCHEMA_STD_LOGGING); assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
} }
assert_eq!(descriptors[1].file_id().as_str(), super::FILE_ID_SCHEMA_COMPOSITE); assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[1].kind(), super::ConfigFileKind::Schema); assert_eq!(descriptors[1].kind(), crate::ConfigFileKind::Config);
assert_eq!(descriptors[2].file_id().as_str(), super::FILE_ID_SCHEMA_STD_LOGGING); assert_eq!(descriptors[1].filename(), std::path::Path::new(crate::DEFAULT_STD_TRANSPORT_FILENAME));
assert_eq!(descriptors[2].kind(), super::ConfigFileKind::Schema); let transport_schema_file_id = descriptors[1].schema_file_id();
assert!(transport_schema_file_id.is_some(), "transport descriptor should expose its validation schema");
if let std::option::Option::Some(schema_file_id) = transport_schema_file_id {
assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
}
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[2].kind(), crate::ConfigFileKind::Schema);
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[3].kind(), crate::ConfigFileKind::Schema);
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[4].kind(), crate::ConfigFileKind::Schema);
} }
} }
#[test] #[test]
fn descriptors_reflect_filename_overrides_without_changing_logical_metadata() { fn descriptors_reflect_filename_overrides_without_changing_logical_metadata() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
let file_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING); let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}"); assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) { if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) {
let overridden = registry.with_filename_override(&file_id, "profiles/desktop.logging.json"); let overridden = registry.with_filename_override(&file_id, "profiles/desktop.logging.json");
assert!(overridden.is_ok(), "filename override should remain valid: {overridden:?}"); assert!(overridden.is_ok(), "filename override should remain valid: {overridden:?}");
if let std::result::Result::Ok(overridden) = overridden { if let std::result::Result::Ok(overridden) = overridden {
let mut found: std::option::Option<&super::ConfigFileDescriptor> = std::option::Option::None; let mut found: std::option::Option<&crate::ConfigFileDescriptor> = std::option::Option::None;
for descriptor in overridden.descriptors() { for descriptor in overridden.descriptors() {
if descriptor.file_id() == &file_id { if descriptor.file_id() == &file_id {
found = std::option::Option::Some(descriptor); found = std::option::Option::Some(descriptor);
@@ -42,12 +52,12 @@ fn descriptors_reflect_filename_overrides_without_changing_logical_metadata() {
assert!(found.is_some(), "public descriptor inventory should retain the overridden logging descriptor"); assert!(found.is_some(), "public descriptor inventory should retain the overridden logging descriptor");
if let std::option::Option::Some(descriptor) = found { if let std::option::Option::Some(descriptor) = found {
assert_eq!(descriptor.file_id(), &file_id); assert_eq!(descriptor.file_id(), &file_id);
assert_eq!(descriptor.kind(), super::ConfigFileKind::Config); assert_eq!(descriptor.kind(), crate::ConfigFileKind::Config);
assert_eq!(descriptor.filename(), std::path::Path::new("profiles/desktop.logging.json")); assert_eq!(descriptor.filename(), std::path::Path::new("profiles/desktop.logging.json"));
let schema_file_id = descriptor.schema_file_id(); let schema_file_id = descriptor.schema_file_id();
assert!(schema_file_id.is_some(), "filename override should preserve schema association"); assert!(schema_file_id.is_some(), "filename override should preserve schema association");
if let std::option::Option::Some(schema_file_id) = schema_file_id { if let std::option::Option::Some(schema_file_id) = schema_file_id {
assert_eq!(schema_file_id.as_str(), super::FILE_ID_SCHEMA_STD_LOGGING); assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
} }
} }
} }
@@ -56,11 +66,11 @@ fn descriptors_reflect_filename_overrides_without_changing_logical_metadata() {
#[test] #[test]
fn defaults_register_logging_document_and_schema_with_distinct_roots() { fn defaults_register_logging_document_and_schema_with_distinct_roots() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let logging_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING); let logging_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_LOGGING); let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_LOGGING);
assert!(logging_id.is_ok(), "logging file_id should be valid: {logging_id:?}"); assert!(logging_id.is_ok(), "logging file_id should be valid: {logging_id:?}");
assert!(schema_id.is_ok(), "logging schema file_id should be valid: {schema_id:?}"); assert!(schema_id.is_ok(), "logging schema file_id should be valid: {schema_id:?}");
if let (std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) = (logging_id, schema_id) { if let (std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) = (logging_id, schema_id) {
@@ -69,15 +79,40 @@ fn defaults_register_logging_document_and_schema_with_distinct_roots() {
assert!(logging.is_ok(), "logging descriptor should exist: {logging:?}"); assert!(logging.is_ok(), "logging descriptor should exist: {logging:?}");
assert!(schema.is_ok(), "logging schema descriptor should exist: {schema:?}"); assert!(schema.is_ok(), "logging schema descriptor should exist: {schema:?}");
if let (std::result::Result::Ok(logging), std::result::Result::Ok(schema)) = (logging, schema) { if let (std::result::Result::Ok(logging), std::result::Result::Ok(schema)) = (logging, schema) {
assert_eq!(logging.kind(), super::ConfigFileKind::Config); assert_eq!(logging.kind(), crate::ConfigFileKind::Config);
assert_eq!(logging.filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_FILENAME)); assert_eq!(logging.filename(), std::path::Path::new(crate::DEFAULT_STD_LOGGING_FILENAME));
let logging_schema = logging.schema_file_id(); let logging_schema = logging.schema_file_id();
assert!(logging_schema.is_some(), "logging document should declare its validation schema"); assert!(logging_schema.is_some(), "logging document should declare its validation schema");
if let std::option::Option::Some(logging_schema) = logging_schema { if let std::option::Option::Some(logging_schema) = logging_schema {
assert_eq!(logging_schema, &schema_id); assert_eq!(logging_schema, &schema_id);
} }
assert_eq!(schema.kind(), super::ConfigFileKind::Schema); assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_SCHEMA_FILENAME)); assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME));
}
}
}
}
#[test]
fn defaults_register_transport_document_and_schema_with_distinct_roots() {
let registry = crate::ConfigFileRegistry::defaults();
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let transport_id = crate::ConfigFileId::new(crate::FILE_ID_STD_TRANSPORT);
let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_TRANSPORT);
assert!(transport_id.is_ok(), "transport file_id should be valid: {transport_id:?}");
assert!(schema_id.is_ok(), "transport schema file_id should be valid: {schema_id:?}");
if let (std::result::Result::Ok(transport_id), std::result::Result::Ok(schema_id)) = (transport_id, schema_id) {
let transport = registry.descriptor(&transport_id);
let schema = registry.descriptor(&schema_id);
assert!(transport.is_ok(), "transport descriptor should exist: {transport:?}");
assert!(schema.is_ok(), "transport schema descriptor should exist: {schema:?}");
if let (std::result::Result::Ok(transport), std::result::Result::Ok(schema)) = (transport, schema) {
assert_eq!(transport.kind(), crate::ConfigFileKind::Config);
assert_eq!(transport.filename(), std::path::Path::new(crate::DEFAULT_STD_TRANSPORT_FILENAME));
assert_eq!(transport.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_TRANSPORT_SCHEMA_FILENAME));
} }
} }
} }
@@ -85,13 +120,13 @@ fn defaults_register_logging_document_and_schema_with_distinct_roots() {
#[test] #[test]
fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() { fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
let bootstrap = crate::ConfigBootstrapOptions::from_paths("runtime-config", "runtime-schemas"); let bootstrap = crate::ConfigBootstrapOptions::from_paths("runtime-config", "runtime-schemas");
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(bootstrap.is_ok(), "bootstrap paths should be valid: {bootstrap:?}"); assert!(bootstrap.is_ok(), "bootstrap paths should be valid: {bootstrap:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(bootstrap)) = (registry, bootstrap) { if let (std::result::Result::Ok(registry), std::result::Result::Ok(bootstrap)) = (registry, bootstrap) {
let logging_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING); let logging_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_LOGGING); let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_LOGGING);
if let (std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) = (logging_id, schema_id) { if let (std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) = (logging_id, schema_id) {
let logging = registry.resolve_path(&bootstrap, &logging_id); let logging = registry.resolve_path(&bootstrap, &logging_id);
let schema = registry.resolve_path(&bootstrap, &schema_id); let schema = registry.resolve_path(&bootstrap, &schema_id);
@@ -115,16 +150,16 @@ fn cli_filemap_override_replaces_filename_and_last_value_wins() {
std::ffi::OsString::from("--other-option"), std::ffi::OsString::from("--other-option"),
std::ffi::OsString::from("--filemap=cfg.std.logging=profiles/custom.logging.json"), std::ffi::OsString::from("--filemap=cfg.std.logging=profiles/custom.logging.json"),
]; ];
let registry = super::ConfigFileRegistry::from_args(&args); let registry = crate::ConfigFileRegistry::from_args(&args);
assert!(registry.is_ok(), "filemap overrides should parse: {registry:?}"); assert!(registry.is_ok(), "filemap overrides should parse: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let file_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING); let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
if let std::result::Result::Ok(file_id) = file_id { if let std::result::Result::Ok(file_id) = file_id {
let descriptor = registry.descriptor(&file_id); let descriptor = registry.descriptor(&file_id);
assert!(descriptor.is_ok(), "logging descriptor should remain registered: {descriptor:?}"); assert!(descriptor.is_ok(), "logging descriptor should remain registered: {descriptor:?}");
if let std::result::Result::Ok(descriptor) = descriptor { if let std::result::Result::Ok(descriptor) = descriptor {
assert_eq!(descriptor.filename(), std::path::Path::new("profiles/custom.logging.json")); assert_eq!(descriptor.filename(), std::path::Path::new("profiles/custom.logging.json"));
assert_eq!(descriptor.kind(), super::ConfigFileKind::Config); assert_eq!(descriptor.kind(), crate::ConfigFileKind::Config);
} }
} }
} }
@@ -132,8 +167,8 @@ fn cli_filemap_override_replaces_filename_and_last_value_wins() {
#[test] #[test]
fn programmatic_override_preserves_file_id_and_kind() { fn programmatic_override_preserves_file_id_and_kind() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
let file_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_LOGGING); let file_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_LOGGING);
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(file_id.is_ok(), "schema file_id should be valid: {file_id:?}"); assert!(file_id.is_ok(), "schema file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) { if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) {
@@ -143,7 +178,7 @@ fn programmatic_override_preserves_file_id_and_kind() {
let descriptor = overridden.descriptor(&file_id); let descriptor = overridden.descriptor(&file_id);
if let std::result::Result::Ok(descriptor) = descriptor { if let std::result::Result::Ok(descriptor) = descriptor {
assert_eq!(descriptor.file_id(), &file_id); assert_eq!(descriptor.file_id(), &file_id);
assert_eq!(descriptor.kind(), super::ConfigFileKind::Schema); assert_eq!(descriptor.kind(), crate::ConfigFileKind::Schema);
assert_eq!(descriptor.filename(), std::path::Path::new("alternate/logging.schema.json")); assert_eq!(descriptor.filename(), std::path::Path::new("alternate/logging.schema.json"));
} }
} }
@@ -153,7 +188,7 @@ fn programmatic_override_preserves_file_id_and_kind() {
#[test] #[test]
fn unknown_file_id_override_is_rejected() { fn unknown_file_id_override_is_rejected() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--filemap=cfg.unknown=unknown.json")]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--filemap=cfg.unknown=unknown.json")];
let result = super::ConfigFileRegistry::from_args(&args); let result = crate::ConfigFileRegistry::from_args(&args);
assert!(result.is_err(), "unknown logical files must not be introduced by CLI override"); assert!(result.is_err(), "unknown logical files must not be introduced by CLI override");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN); assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN);
@@ -163,7 +198,7 @@ fn unknown_file_id_override_is_rejected() {
#[test] #[test]
fn invalid_file_ids_are_rejected() { fn invalid_file_ids_are_rejected() {
for value in ["", ".cfg", "cfg.", "cfg..logging", "CFG.logging", "cfg/logging"] { for value in ["", ".cfg", "cfg.", "cfg..logging", "CFG.logging", "cfg/logging"] {
let result = super::ConfigFileId::new(value); let result = crate::ConfigFileId::new(value);
assert!(result.is_err(), "invalid file_id must be rejected: {value}"); assert!(result.is_err(), "invalid file_id must be rejected: {value}");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_INVALID); assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_INVALID);
@@ -173,8 +208,8 @@ fn invalid_file_ids_are_rejected() {
#[test] #[test]
fn absolute_and_traversing_filenames_are_rejected() { fn absolute_and_traversing_filenames_are_rejected() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
let file_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING); let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}"); assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) { if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) {
@@ -191,7 +226,7 @@ fn absolute_and_traversing_filenames_are_rejected() {
fn malformed_filemap_arguments_are_rejected() { fn malformed_filemap_arguments_are_rejected() {
for argument in ["--filemap", "--filemap=cfg.std.logging", "--filemap==logging.json", "--filemap=cfg.std.logging="] { for argument in ["--filemap", "--filemap=cfg.std.logging", "--filemap==logging.json", "--filemap=cfg.std.logging="] {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from(argument)]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from(argument)];
let result = super::ConfigFileRegistry::from_args(&args); let result = crate::ConfigFileRegistry::from_args(&args);
assert!(result.is_err(), "malformed filemap argument must be rejected: {argument}"); assert!(result.is_err(), "malformed filemap argument must be rejected: {argument}");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID); assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
@@ -201,12 +236,12 @@ fn malformed_filemap_arguments_are_rejected() {
#[test] #[test]
fn duplicate_registry_ids_are_rejected() { fn duplicate_registry_ids_are_rejected() {
let first = super::ConfigFileDescriptor::new("cfg.duplicate", super::ConfigFileKind::Config, "first.json", std::option::Option::None); let first = crate::ConfigFileDescriptor::new("cfg.duplicate", crate::ConfigFileKind::Config, "first.json", std::option::Option::None);
let second = super::ConfigFileDescriptor::new("cfg.duplicate", super::ConfigFileKind::Config, "second.json", std::option::Option::None); let second = crate::ConfigFileDescriptor::new("cfg.duplicate", crate::ConfigFileKind::Config, "second.json", std::option::Option::None);
assert!(first.is_ok(), "first descriptor should be valid: {first:?}"); assert!(first.is_ok(), "first descriptor should be valid: {first:?}");
assert!(second.is_ok(), "second descriptor should be valid: {second:?}"); assert!(second.is_ok(), "second descriptor should be valid: {second:?}");
if let (std::result::Result::Ok(first), std::result::Result::Ok(second)) = (first, second) { if let (std::result::Result::Ok(first), std::result::Result::Ok(second)) = (first, second) {
let result = super::build_registry([first, second]); let result = crate::build_registry([first, second]);
assert!(result.is_err(), "duplicate file_ids must be rejected"); assert!(result.is_err(), "duplicate file_ids must be rejected");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_DUPLICATE); assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_DUPLICATE);
@@ -216,7 +251,7 @@ fn duplicate_registry_ids_are_rejected() {
#[test] #[test]
fn descriptor_kind_must_match_file_id_namespace() { fn descriptor_kind_must_match_file_id_namespace() {
let result = super::ConfigFileDescriptor::new("schema.invalid-kind", super::ConfigFileKind::Config, "invalid.json", std::option::Option::None); let result = crate::ConfigFileDescriptor::new("schema.invalid-kind", crate::ConfigFileKind::Config, "invalid.json", std::option::Option::None);
assert!(result.is_err(), "descriptor kind mismatch must be rejected"); assert!(result.is_err(), "descriptor kind mismatch must be rejected");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID); assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
@@ -225,10 +260,10 @@ fn descriptor_kind_must_match_file_id_namespace() {
#[test] #[test]
fn config_schema_association_must_reference_registered_schema_descriptor() { fn config_schema_association_must_reference_registered_schema_descriptor() {
let config = super::ConfigFileDescriptor::new("cfg.test", super::ConfigFileKind::Config, "test.json", std::option::Option::Some("schema.test")); let config = crate::ConfigFileDescriptor::new("cfg.test", crate::ConfigFileKind::Config, "test.json", std::option::Option::Some("schema.test"));
assert!(config.is_ok(), "config descriptor should be valid before registry association validation: {config:?}"); assert!(config.is_ok(), "config descriptor should be valid before registry association validation: {config:?}");
if let std::result::Result::Ok(config) = config { if let std::result::Result::Ok(config) = config {
let result = super::build_registry([config]); let result = crate::build_registry([config]);
assert!(result.is_err(), "registry must reject a missing schema association"); assert!(result.is_err(), "registry must reject a missing schema association");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID); assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
@@ -251,9 +286,9 @@ fn absolute_fixture_path() -> std::path::PathBuf {
#[test] #[test]
fn defaults_register_generic_composite_schema_without_runtime_composite() { fn defaults_register_generic_composite_schema_without_runtime_composite() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_COMPOSITE); let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_COMPOSITE);
let runtime_id = super::ConfigFileId::new("cfg.composite.ksp-app-wallet-desk"); let runtime_id = crate::ConfigFileId::new("cfg.composite.ksp-app-wallet-desk");
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}"); assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}");
assert!(runtime_id.is_ok(), "future composite runtime file_id syntax should be valid: {runtime_id:?}"); assert!(runtime_id.is_ok(), "future composite runtime file_id syntax should be valid: {runtime_id:?}");
@@ -263,8 +298,8 @@ fn defaults_register_generic_composite_schema_without_runtime_composite() {
assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}"); assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}");
assert!(runtime.is_err(), "no fictitious runtime composite should be registered"); assert!(runtime.is_err(), "no fictitious runtime composite should be registered");
if let std::result::Result::Ok(schema) = schema { if let std::result::Result::Ok(schema) = schema {
assert_eq!(schema.kind(), super::ConfigFileKind::Schema); assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_COMPOSITE_SCHEMA_FILENAME)); assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
} }
if let std::result::Result::Err(error) = runtime { if let std::result::Result::Err(error) = runtime {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN); assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN);

View File

@@ -1,31 +1,31 @@
// file: crates/ksp-config-lib/unit_tests/sensitivity.rs // file: crates/ksp-config-lib/unit_tests/sensitivity.rs
// version: 1 // version: 2
#[test] #[test]
fn environment_names_map_to_expected_sensitivity() { fn environment_names_map_to_expected_sensitivity() {
assert_eq!(super::ConfigSensitivity::from_variable_name("KSP_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(super::ConfigSensitivity::Public)); assert_eq!(crate::ConfigSensitivity::from_variable_name("KSP_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(crate::ConfigSensitivity::Public));
assert_eq!(super::ConfigSensitivity::from_variable_name("KSP_MODE").ok(), std::option::Option::Some(super::ConfigSensitivity::Internal)); assert_eq!(crate::ConfigSensitivity::from_variable_name("KSP_MODE").ok(), std::option::Option::Some(crate::ConfigSensitivity::Internal));
assert_eq!(super::ConfigSensitivity::from_variable_name("KSP_SECRET_PASSWORD").ok(), std::option::Option::Some(super::ConfigSensitivity::Secret)); assert_eq!(crate::ConfigSensitivity::from_variable_name("KSP_SECRET_PASSWORD").ok(), std::option::Option::Some(crate::ConfigSensitivity::Secret));
assert_eq!(super::ConfigSensitivity::from_variable_name("KSPB_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(super::ConfigSensitivity::Public)); assert_eq!(crate::ConfigSensitivity::from_variable_name("KSPB_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(crate::ConfigSensitivity::Public));
assert_eq!(super::ConfigSensitivity::from_variable_name("KSPB_MODE").ok(), std::option::Option::Some(super::ConfigSensitivity::Internal)); assert_eq!(crate::ConfigSensitivity::from_variable_name("KSPB_MODE").ok(), std::option::Option::Some(crate::ConfigSensitivity::Internal));
assert_eq!(super::ConfigSensitivity::from_variable_name("KSPB_SECRET_PASSWORD").ok(), std::option::Option::Some(super::ConfigSensitivity::Secret)); assert_eq!(crate::ConfigSensitivity::from_variable_name("KSPB_SECRET_PASSWORD").ok(), std::option::Option::Some(crate::ConfigSensitivity::Secret));
} }
#[test] #[test]
fn strongest_sensitivity_follows_secret_internal_public_order() { fn strongest_sensitivity_follows_secret_internal_public_order() {
assert_eq!(super::ConfigSensitivity::Public.strongest(super::ConfigSensitivity::Internal), super::ConfigSensitivity::Internal); assert_eq!(crate::ConfigSensitivity::Public.strongest(crate::ConfigSensitivity::Internal), crate::ConfigSensitivity::Internal);
assert_eq!(super::ConfigSensitivity::Internal.strongest(super::ConfigSensitivity::Secret), super::ConfigSensitivity::Secret); assert_eq!(crate::ConfigSensitivity::Internal.strongest(crate::ConfigSensitivity::Secret), crate::ConfigSensitivity::Secret);
assert_eq!(super::ConfigSensitivity::Secret.strongest(super::ConfigSensitivity::Public), super::ConfigSensitivity::Secret); assert_eq!(crate::ConfigSensitivity::Secret.strongest(crate::ConfigSensitivity::Public), crate::ConfigSensitivity::Secret);
} }
#[test] #[test]
fn provenance_exposes_names_and_sources_without_values() { fn provenance_exposes_names_and_sources_without_values() {
let process = super::ConfigValueProvenance::EnvironmentProcess { variable_name: "KSP_SECRET_TOKEN".to_owned() }; let process = crate::ConfigValueProvenance::EnvironmentProcess { variable_name: "KSP_SECRET_TOKEN".to_owned() };
let dotenv = super::ConfigValueProvenance::EnvironmentDotEnv { variable_name: "KSP_MODE".to_owned() }; let dotenv = crate::ConfigValueProvenance::EnvironmentDotEnv { variable_name: "KSP_MODE".to_owned() };
let fallback = super::ConfigValueProvenance::EnvironmentFallback { variable_name: "KSP_PUBLIC_HOST".to_owned() }; let fallback = crate::ConfigValueProvenance::EnvironmentFallback { variable_name: "KSP_PUBLIC_HOST".to_owned() };
assert_eq!(process.variable_name(), std::option::Option::Some("KSP_SECRET_TOKEN")); assert_eq!(process.variable_name(), std::option::Option::Some("KSP_SECRET_TOKEN"));
assert_eq!(process.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process)); assert_eq!(process.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process));
assert_eq!(dotenv.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::DotEnv)); assert_eq!(dotenv.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::DotEnv));
assert_eq!(fallback.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Fallback)); assert_eq!(fallback.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Fallback));
assert_eq!(super::ConfigValueProvenance::DocumentLiteral.variable_name(), std::option::Option::None); assert_eq!(crate::ConfigValueProvenance::DocumentLiteral.variable_name(), std::option::Option::None);
} }

View File

@@ -0,0 +1,202 @@
// file: crates/ksp-config-lib/unit_tests/transport.rs
// version: 1
#[test]
fn fixture_transport_profile_maps_complete_runtime_contract() {
let engine = fixture_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_transport_config(std::option::Option::None, &environment);
assert!(resolved.is_ok(), "fixture Transport Config should map: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
assert_eq!(resolved.profile_id(), "secret_test");
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
assert_eq!(resolved.settings().retry().max_retries(), 4);
assert_eq!(resolved.settings().retry().initial_backoff(), std::time::Duration::from_millis(125));
assert_eq!(resolved.settings().retry().max_backoff(), std::time::Duration::from_millis(2500));
assert_eq!(resolved.settings().endpoints().len(), 1);
let endpoint = &resolved.settings().endpoints()[0];
assert_eq!(endpoint.name(), "fixture_private");
assert_eq!(endpoint.provider().as_str(), "fixture-provider");
assert_eq!(endpoint.cluster().as_str(), "fixture-cluster");
assert_eq!(endpoint.url().as_str(), "https://fallback.invalid");
assert_eq!(endpoint.connect_timeout(), std::time::Duration::from_millis(750));
assert_eq!(endpoint.request_timeout(), std::time::Duration::from_millis(2500));
assert_eq!(endpoint.max_idle_connections_per_host(), std::option::Option::Some(3));
assert_eq!(endpoint.roles().len(), 1);
let role = &endpoint.roles()[0];
assert_eq!(role.role().as_str(), "default");
assert!(role.enabled());
assert_eq!(role.priority(), 7);
assert_eq!(role.request_kinds().len(), 1);
assert!(role.request_kinds()[0].is_wildcard());
assert_eq!(role.limits().requests_per_second().map(std::num::NonZeroU32::get), std::option::Option::Some(9));
assert_eq!(role.limits().burst_capacity().map(std::num::NonZeroU32::get), std::option::Option::Some(12));
assert_eq!(role.limits().max_concurrent_requests().map(std::num::NonZeroU32::get), std::option::Option::Some(4));
assert_eq!(role.limits().pause_after_rate_limit(), std::option::Option::Some(std::time::Duration::from_millis(650)));
}
#[test]
fn committed_transport_document_maps_default_and_explicit_profiles() {
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 default = engine.load_resolved_transport_config(std::option::Option::None, &environment);
let mainnet = engine.load_resolved_transport_config(std::option::Option::Some("mainnet_public"), &environment);
assert!(default.is_ok(), "committed default Transport profile should map: {default:?}");
assert!(mainnet.is_ok(), "committed explicit Transport profile should map: {mainnet:?}");
if let std::result::Result::Ok(default) = default {
assert_eq!(default.profile_id(), "devnet_public");
assert_eq!(default.settings().endpoints()[0].cluster().as_str(), "devnet");
assert_eq!(default.settings().endpoints()[0].url().as_str(), "https://api.devnet.solana.com");
}
if let std::result::Result::Ok(mainnet) = mainnet {
assert_eq!(mainnet.profile_id(), "mainnet_public");
assert_eq!(mainnet.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
assert_eq!(mainnet.settings().endpoints()[0].cluster().as_str(), "mainnet-beta");
assert_eq!(mainnet.settings().endpoints()[0].url().as_str(), "https://api.mainnet-beta.solana.com");
}
}
#[test]
fn transport_profile_preserves_global_and_profile_origin() {
let engine = committed_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_TRANSPORT);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let profile = engine.load_resolved_profile(&file_id, std::option::Option::None);
assert!(profile.is_ok(), "committed Transport profile should resolve: {profile:?}");
if let std::result::Result::Ok(profile) = profile {
assert_eq!(profile.origin("retry"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
assert_eq!(profile.origin("endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
assert_eq!(profile.origin("format_version"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
}
}
#[test]
fn secret_transport_url_is_runtime_available_but_safe_projection_is_redacted() {
let engine = fixture_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let canary = "https://secret-provider.invalid/?api-key=transport-secret-canary";
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), canary.to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
assert!(resolved.is_ok(), "secret Transport endpoint should map without being rejected: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(resolved.effective().sensitivity(), crate::ConfigSensitivity::Secret);
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), canary);
let safe_url = resolved.effective().safe_value().pointer("/endpoints/0/url").and_then(serde_json::Value::as_str);
assert_eq!(safe_url, std::option::Option::Some(crate::REDACTED_CONFIG_VALUE));
let debug = format!("{resolved:?}");
assert!(!debug.contains("transport-secret-canary"));
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
}
#[test]
fn transport_secret_url_provenance_uses_process_and_process_beats_dotenv() {
let engine = fixture_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_TRANSPORT_TEST_URL".to_owned(), "https://process.invalid".to_owned());
let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), "https://dotenv.invalid".to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, dotenv);
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
assert!(resolved.is_ok(), "secret Transport environment precedence should map: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://process.invalid");
let provenance = resolved.effective().provenance_at("/endpoints/0/url");
assert!(provenance.is_some(), "endpoint URL should retain environment provenance");
if let std::option::Option::Some(provenance) = provenance {
assert_eq!(provenance.len(), 1);
assert_eq!(provenance[0].environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process));
assert_eq!(provenance[0].variable_name(), std::option::Option::Some("KSP_SECRET_TRANSPORT_TEST_URL"));
}
}
#[test]
fn invalid_secret_transport_url_is_effective_config_error_without_secret_leak() {
let engine = fixture_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let canary = "transport-invalid-secret-canary";
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), canary.to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
assert!(resolved.is_err(), "invalid secret-derived endpoint URL must fail effective mapping");
if let std::result::Result::Err(error) = resolved {
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
let debug = format!("{error:?}");
assert!(!debug.contains(canary));
assert!(error.context().iter().any(|item| -> bool {
return item.key() == "transport_error_code" && item.value() == "invalid_settings";
}));
}
}
fn fixture_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let workspace = workspace_root();
let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures");
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture_root, 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 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 workspace_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
}

View File

@@ -1,5 +1,6 @@
// file: crates/ksp-core-lib/src/lib.rs // file: crates/ksp-core-lib/src/lib.rs
// version: 6 // version: 8
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-core-lib/src/program_ids.rs // file: crates/ksp-core-lib/src/program_ids.rs
// version: 2 // version: 4
const DOMAIN_SOLANA: &str = "solana"; const DOMAIN_SOLANA: &str = "solana";
const FAMILY_CONSENSUS: &str = "consensus"; const FAMILY_CONSENSUS: &str = "consensus";
@@ -266,6 +266,12 @@ impl<'a> ProgramIdFilter<'a> {
} }
} }
/// Returns the canonical KSP Program ID registry.
#[must_use]
pub const fn entries() -> &'static [crate::ProgramIdEntry] {
return PROGRAM_ID_ENTRIES;
}
const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[ const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[
crate::ProgramIdEntry::new( crate::ProgramIdEntry::new(
"solana.address_lookup_table", "solana.address_lookup_table",
@@ -438,12 +444,6 @@ const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[
), ),
]; ];
/// Returns the canonical KSP Program ID registry.
#[must_use]
pub const fn entries() -> &'static [crate::ProgramIdEntry] {
return PROGRAM_ID_ENTRIES;
}
/// Returns a lazy view of Program IDs matching all configured filter axes. /// Returns a lazy view of Program IDs matching all configured filter axes.
pub fn program_ids<'a>(filter: crate::ProgramIdFilter<'a>) -> impl std::iter::Iterator<Item = &'static crate::ProgramIdEntry> + 'a { pub fn program_ids<'a>(filter: crate::ProgramIdFilter<'a>) -> impl std::iter::Iterator<Item = &'static crate::ProgramIdEntry> + 'a {
return PROGRAM_ID_ENTRIES.iter().filter(move |entry| { return PROGRAM_ID_ENTRIES.iter().filter(move |entry| {

View File

@@ -0,0 +1,68 @@
// file: crates/ksp-core-lib/tests/workspace_dependencies.rs
// version: 1
//! Workspace-level dependency policy canaries owned by the foundational KSP test surface.
fn workspace_root() -> std::path::PathBuf {
let manifest_directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let parent = manifest_directory.parent();
assert!(parent.is_some(), "core crate must have a crates directory parent");
let parent = match parent {
std::option::Option::Some(value) => value,
std::option::Option::None => return manifest_directory.to_path_buf(),
};
let root = parent.parent();
assert!(root.is_some(), "core crate must have a workspace root");
return match root {
std::option::Option::Some(value) => value.to_path_buf(),
std::option::Option::None => parent.to_path_buf(),
};
}
#[test]
fn workspace_dependency_table_does_not_activate_consumer_features() {
let manifest_path = workspace_root().join("Cargo.toml");
let manifest = std::fs::read_to_string(manifest_path.as_path());
assert!(manifest.is_ok(), "workspace manifest must be readable during integration tests");
let manifest = match manifest {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let workspace_dependencies = match manifest.split("[workspace.dependencies]").nth(1) {
std::option::Option::Some(tail) => tail.split("[workspace.lints.rust]").next(),
std::option::Option::None => std::option::Option::None,
};
assert!(workspace_dependencies.is_some(), "workspace dependencies section must exist");
let workspace_dependencies = match workspace_dependencies {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
for line in workspace_dependencies.lines() {
let content = match line.split('#').next() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
for inline_field in content.split(',') {
let key = match inline_field.split('=').next() {
std::option::Option::Some(value) => value.trim().trim_start_matches('{').trim(),
std::option::Option::None => continue,
};
assert_ne!(key, "features", "consumer feature activation must stay in member manifests");
}
}
}
#[test]
fn transport_manifest_preserves_ksp_dependency_firewall() {
let manifest_path = workspace_root().join("crates/ksp-onchain-transport-lib/Cargo.toml");
let manifest = std::fs::read_to_string(manifest_path).expect("transport manifest must be readable during workspace integration tests");
for forbidden in ["ksp-config-lib", "ksp-store-api", "ksp-store-lib", "ksp-program-api", "ksp-program-lib", "tracing =", "tracing."] {
assert!(!manifest.contains(forbidden), "forbidden direct transport dependency detected: {forbidden}");
}
assert!(manifest.contains("ksp-core-lib"));
assert!(manifest.contains("ksp-logging-lib"));
assert!(manifest.contains("reqwest = { workspace = true, features = [\"rustls\"] }"));
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"sync\", \"time\"] }"));
assert!(manifest.contains("[dev-dependencies]"));
assert!(manifest.contains("tokio = { workspace = true, features = [\"rt\"] }"));
}

View File

@@ -0,0 +1,121 @@
// file: crates/ksp-core-lib/tests/workspace_logging.rs
// version: 1
//! Workspace-level logging ownership canaries for KSP behavioral crates.
fn workspace_root() -> std::path::PathBuf {
let manifest_directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let parent = manifest_directory.parent();
assert!(parent.is_some(), "core crate must have a crates directory parent");
let parent = match parent {
std::option::Option::Some(value) => value,
std::option::Option::None => return manifest_directory.to_path_buf(),
};
let root = parent.parent();
assert!(root.is_some(), "core crate must have a workspace root");
return match root {
std::option::Option::Some(value) => value.to_path_buf(),
std::option::Option::None => parent.to_path_buf(),
};
}
fn rust_source_files(directory: &std::path::Path) -> std::vec::Vec<std::path::PathBuf> {
let mut files = std::vec::Vec::new();
let entries = std::fs::read_dir(directory);
if entries.is_err() {
return files;
}
let entries = match entries {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return files,
};
for entry in entries {
let entry = match entry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
let path = entry.path();
if path.is_dir() {
files.extend(rust_source_files(path.as_path()));
} else if path.extension() == std::option::Option::Some(std::ffi::OsStr::new("rs")) {
files.push(path);
}
}
return files;
}
fn package_name(manifest: &str) -> std::option::Option<&str> {
for line in manifest.lines() {
let trimmed = line.trim();
if !trimmed.starts_with("name = ") {
continue;
}
return trimmed.split('"').nth(1);
}
return std::option::Option::None;
}
#[test]
fn behavioral_crates_own_explicit_tracing_targets() {
let crates_root = workspace_root().join("crates");
let entries = std::fs::read_dir(crates_root.as_path());
assert!(entries.is_ok(), "workspace crates directory must be readable");
let entries = match entries {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
for entry in entries {
let entry = match entry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
let crate_root = entry.path();
if !crate_root.is_dir() {
continue;
}
let source_root = crate_root.join("src");
let source_files = rust_source_files(source_root.as_path());
let mut emits_ksp_logs = false;
for source_file in source_files.iter() {
let source = std::fs::read_to_string(source_file.as_path());
assert!(source.is_ok(), "Rust source must be readable: {}", source_file.display());
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
if source.contains("ksp_logging_lib::") {
emits_ksp_logs = true;
}
assert!(
!source.contains("target: env!(\"CARGO_PKG_NAME\")"),
"KSP tracing target must be explicit rather than derived from Cargo metadata: {}",
source_file.display()
);
assert!(!source.contains("target: \"ksp-"), "KSP tracing target literals must be owned by src/constants.rs: {}", source_file.display());
}
if !emits_ksp_logs {
continue;
}
let manifest = std::fs::read_to_string(crate_root.join("Cargo.toml"));
assert!(manifest.is_ok(), "behavioral crate manifest must be readable: {}", crate_root.display());
let manifest = match manifest {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
let package_name = package_name(manifest.as_str());
assert!(package_name.is_some(), "behavioral crate package name must be discoverable: {}", crate_root.display());
let package_name = match package_name {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
let constants_path = source_root.join("constants.rs");
let constants = std::fs::read_to_string(constants_path.as_path());
assert!(constants.is_ok(), "behavioral crate must own src/constants.rs: {}", crate_root.display());
let constants = match constants {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
let expected = std::format!("pub(crate) const TRACING_TARGET: &str = \"{package_name}\";");
assert!(constants.contains(expected.as_str()), "behavioral crate must own its Cargo-name tracing target: {}", crate_root.display());
}
}

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-logging-lib/Cargo.toml # file: crates/ksp-logging-lib/Cargo.toml
# version: 4 # version: 5
[package] [package]
name = "ksp-logging-lib" name = "ksp-logging-lib"
@@ -9,12 +9,12 @@ repository.workspace = true
[dependencies] [dependencies]
ksp-core-lib = { path = "../ksp-core-lib" } ksp-core-lib = { path = "../ksp-core-lib" }
tracing.workspace = true tracing = { workspace = true, features = ["std"] }
tracing-subscriber.workspace = true tracing-subscriber = { workspace = true, features = ["fmt", "json", "ansi"] }
tracing-appender.workspace = true tracing-appender.workspace = true
[dev-dependencies] [dev-dependencies]
tokio.workspace = true tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] }
[lints] [lints]
workspace = true workspace = true

View File

@@ -1,37 +1,15 @@
// file: crates/ksp-logging-lib/src/domain.rs // file: crates/ksp-logging-lib/src/domain.rs
// version: 1 // version: 3
std::thread_local! { std::thread_local! {
static CURRENT_DOMAIN: std::cell::RefCell<std::option::Option<std::string::String>> = const { std::cell::RefCell::new(std::option::Option::None) }; static CURRENT_DOMAIN: std::cell::RefCell<std::option::Option<std::string::String>> = const { std::cell::RefCell::new(std::option::Option::None) };
} }
#[derive(Clone, Debug, Default, Eq, PartialEq)] /// Crate-internal `DomainContextLayer` state shared across the owning crate.
struct SpanDomain {
value: std::option::Option<std::string::String>,
}
#[derive(Default)]
struct DomainVisitor {
value: std::option::Option<std::string::String>,
}
impl tracing::field::Visit for DomainVisitor {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "domain" {
self.value = std::option::Option::Some(value.to_string());
}
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "domain" {
self.value = std::option::Option::Some(format!("{value:?}"));
}
}
}
pub(crate) struct DomainContextLayer; pub(crate) struct DomainContextLayer;
impl DomainContextLayer { impl DomainContextLayer {
/// Creates a new `DomainContextLayer` value.
pub(crate) const fn new() -> Self { pub(crate) const fn new() -> Self {
return Self; return Self;
} }
@@ -99,6 +77,31 @@ where
} }
} }
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct SpanDomain {
value: std::option::Option<std::string::String>,
}
#[derive(Default)]
struct DomainVisitor {
value: std::option::Option<std::string::String>,
}
impl tracing::field::Visit for DomainVisitor {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "domain" {
self.value = std::option::Option::Some(value.to_string());
}
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "domain" {
self.value = std::option::Option::Some(format!("{value:?}"));
}
}
}
/// Executes the crate-internal current domain matches operation for the owning module.
pub(crate) fn current_domain_matches(selectors: &[std::string::String]) -> bool { pub(crate) fn current_domain_matches(selectors: &[std::string::String]) -> bool {
if let [selector] = selectors if let [selector] = selectors
&& selector == "*" && selector == "*"

View File

@@ -1,13 +1,13 @@
// file: crates/ksp-logging-lib/src/error.rs // file: crates/ksp-logging-lib/src/error.rs
// version: 4 // version: 5
/// Error code used when runtime logging settings are invalid.
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_settings");
/// Error code used when a global logging subscriber is already installed. /// Error code used when a global logging subscriber is already installed.
pub const ERROR_CODE_ALREADY_INITIALIZED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "already_initialized"); pub const ERROR_CODE_ALREADY_INITIALIZED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "already_initialized");
/// Error code used when a hot reload cannot replace the active runtime layers.
pub const ERROR_CODE_RELOAD_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "reload_failed");
/// Error code used when the rolling file output cannot be initialized. /// Error code used when the rolling file output cannot be initialized.
pub const ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "file_output_initialization_failed"); pub const ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "file_output_initialization_failed");
/// Error code used when an application Logging runtime identity is invalid. /// Error code used when an application Logging runtime identity is invalid.
pub const ERROR_CODE_INVALID_RUNTIME_IDENTITY: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_runtime_identity"); pub const ERROR_CODE_INVALID_RUNTIME_IDENTITY: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_runtime_identity");
/// Error code used when runtime logging settings are invalid.
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_settings");
/// Error code used when a hot reload cannot replace the active runtime layers.
pub const ERROR_CODE_RELOAD_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "reload_failed");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/identity.rs // file: crates/ksp-logging-lib/src/identity.rs
// version: 1 // version: 2
//! Stable runtime identity used to separate persistent file outputs between application launches. //! Stable runtime identity used to separate persistent file outputs between application launches.
@@ -41,6 +41,7 @@ impl LoggingRuntimeIdentity {
return self.launch_timestamp.as_str(); return self.launch_timestamp.as_str();
} }
/// Executes the crate-internal file name prefix operation for `LoggingRuntimeIdentity`.
pub(crate) fn file_name_prefix(&self, configured_prefix: &str) -> std::string::String { pub(crate) fn file_name_prefix(&self, configured_prefix: &str) -> std::string::String {
return format!("{}.{}.{}", self.application_id, self.launch_timestamp, configured_prefix); return format!("{}.{}.{}", self.application_id, self.launch_timestamp, configured_prefix);
} }

View File

@@ -1,5 +1,6 @@
// file: crates/ksp-logging-lib/src/lib.rs // file: crates/ksp-logging-lib/src/lib.rs
// version: 8 // version: 10
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -69,6 +70,17 @@ pub use self::span::Span;
/// Instruments an asynchronous future with a KSP span. /// Instruments an asynchronous future with a KSP span.
pub use self::span::instrument; pub use self::span::instrument;
/// Crate-internal `DomainContextLayer` state shared across the owning crate.
pub(crate) use self::domain::DomainContextLayer;
/// Executes the crate-internal current domain matches operation for the owning module.
pub(crate) use self::domain::current_domain_matches;
/// Crate-internal `RouteMakeWriter` state shared across the owning crate.
pub(crate) use self::writer::RouteMakeWriter;
/// Crate-internal `RoutedWriter` variants used by the owning crate.
pub(crate) use self::writer::RoutedWriter;
/// Crate-internal `StripAnsiWriter` state shared across the owning crate.
pub(crate) use self::writer::StripAnsiWriter;
/// Hidden tracing crate bridge used exclusively by exported KSP logging macros.
#[doc(hidden)] #[doc(hidden)]
/// Internal macro bridge. KSP consumers must not use this reexport directly. pub extern crate tracing;
pub extern crate tracing as __private_tracing;

View File

@@ -1,11 +1,11 @@
// file: crates/ksp-logging-lib/src/macros.rs // file: crates/ksp-logging-lib/src/macros.rs
// version: 1 // version: 2
/// Emits a KSP error event with an explicit owning target. /// Emits a KSP error event with an explicit owning target.
#[macro_export] #[macro_export]
macro_rules! error { macro_rules! error {
(target: $target:expr, $($argument:tt)+) => {{ (target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::error!(target: $target, $($argument)+); $crate::tracing::error!(target: $target, $($argument)+);
}}; }};
} }
@@ -13,7 +13,7 @@ macro_rules! error {
#[macro_export] #[macro_export]
macro_rules! warn { macro_rules! warn {
(target: $target:expr, $($argument:tt)+) => {{ (target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::warn!(target: $target, $($argument)+); $crate::tracing::warn!(target: $target, $($argument)+);
}}; }};
} }
@@ -21,7 +21,7 @@ macro_rules! warn {
#[macro_export] #[macro_export]
macro_rules! info { macro_rules! info {
(target: $target:expr, $($argument:tt)+) => {{ (target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::info!(target: $target, $($argument)+); $crate::tracing::info!(target: $target, $($argument)+);
}}; }};
} }
@@ -29,7 +29,7 @@ macro_rules! info {
#[macro_export] #[macro_export]
macro_rules! debug { macro_rules! debug {
(target: $target:expr, $($argument:tt)+) => {{ (target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::debug!(target: $target, $($argument)+); $crate::tracing::debug!(target: $target, $($argument)+);
}}; }};
} }
@@ -37,7 +37,7 @@ macro_rules! debug {
#[macro_export] #[macro_export]
macro_rules! trace { macro_rules! trace {
(target: $target:expr, $($argument:tt)+) => {{ (target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::trace!(target: $target, $($argument)+); $crate::tracing::trace!(target: $target, $($argument)+);
}}; }};
} }
@@ -45,10 +45,10 @@ macro_rules! trace {
#[macro_export] #[macro_export]
macro_rules! error_span { macro_rules! error_span {
(target: $target:expr, $name:expr) => {{ (target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::error_span!(target: $target, $name)) $crate::Span::__from_tracing($crate::tracing::error_span!(target: $target, $name))
}}; }};
(target: $target:expr, $name:expr, $($field:tt)+) => {{ (target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::error_span!(target: $target, $name, $($field)+)) $crate::Span::__from_tracing($crate::tracing::error_span!(target: $target, $name, $($field)+))
}}; }};
} }
@@ -56,10 +56,10 @@ macro_rules! error_span {
#[macro_export] #[macro_export]
macro_rules! warn_span { macro_rules! warn_span {
(target: $target:expr, $name:expr) => {{ (target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::warn_span!(target: $target, $name)) $crate::Span::__from_tracing($crate::tracing::warn_span!(target: $target, $name))
}}; }};
(target: $target:expr, $name:expr, $($field:tt)+) => {{ (target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::warn_span!(target: $target, $name, $($field)+)) $crate::Span::__from_tracing($crate::tracing::warn_span!(target: $target, $name, $($field)+))
}}; }};
} }
@@ -67,10 +67,10 @@ macro_rules! warn_span {
#[macro_export] #[macro_export]
macro_rules! info_span { macro_rules! info_span {
(target: $target:expr, $name:expr) => {{ (target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::info_span!(target: $target, $name)) $crate::Span::__from_tracing($crate::tracing::info_span!(target: $target, $name))
}}; }};
(target: $target:expr, $name:expr, $($field:tt)+) => {{ (target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::info_span!(target: $target, $name, $($field)+)) $crate::Span::__from_tracing($crate::tracing::info_span!(target: $target, $name, $($field)+))
}}; }};
} }
@@ -78,10 +78,10 @@ macro_rules! info_span {
#[macro_export] #[macro_export]
macro_rules! debug_span { macro_rules! debug_span {
(target: $target:expr, $name:expr) => {{ (target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::debug_span!(target: $target, $name)) $crate::Span::__from_tracing($crate::tracing::debug_span!(target: $target, $name))
}}; }};
(target: $target:expr, $name:expr, $($field:tt)+) => {{ (target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::debug_span!(target: $target, $name, $($field)+)) $crate::Span::__from_tracing($crate::tracing::debug_span!(target: $target, $name, $($field)+))
}}; }};
} }
@@ -89,9 +89,9 @@ macro_rules! debug_span {
#[macro_export] #[macro_export]
macro_rules! trace_span { macro_rules! trace_span {
(target: $target:expr, $name:expr) => {{ (target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::trace_span!(target: $target, $name)) $crate::Span::__from_tracing($crate::tracing::trace_span!(target: $target, $name))
}}; }};
(target: $target:expr, $name:expr, $($field:tt)+) => {{ (target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::trace_span!(target: $target, $name, $($field)+)) $crate::Span::__from_tracing($crate::tracing::trace_span!(target: $target, $name, $($field)+))
}}; }};
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/runtime.rs // file: crates/ksp-logging-lib/src/runtime.rs
// version: 13 // version: 16
use tracing_subscriber::Layer; // rust-rules: trait-import use tracing_subscriber::Layer; // rust-rules: trait-import
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
@@ -228,31 +228,6 @@ pub fn initialize_with_identity(settings: &crate::LoggingSettings, identity: &cr
return initialize_runtime(settings, std::option::Option::Some(identity.clone())); return initialize_runtime(settings, std::option::Option::Some(identity.clone()));
} }
fn initialize_runtime(
settings: &crate::LoggingSettings,
runtime_identity: std::option::Option<crate::LoggingRuntimeIdentity>,
) -> ksp_core_lib::Result<crate::LoggingGuard> {
return prepare_runtime_with_identity(settings, runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
let PreparedRuntime { layers, outputs } = prepared;
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(layers);
let subscriber = tracing_subscriber::registry().with(reload_layer);
let install_result = tracing::subscriber::set_global_default(subscriber);
return match install_result {
std::result::Result::Ok(()) => std::result::Result::Ok(crate::LoggingGuard {
reload_handle,
settings: settings.clone(),
runtime_identity,
outputs,
retired_dropped_lines: crate::DroppedLines::zero(),
retired_file_dropped_lines: std::collections::HashMap::new(),
}),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error),
),
};
});
}
/// Replaces the active KSP logging settings and non-blocking outputs without reinstalling the global subscriber. /// Replaces the active KSP logging settings and non-blocking outputs without reinstalling the global subscriber.
/// ///
/// New runtime layers, writers and guards are fully prepared before the reload is attempted. If validation or preparation fails, the currently active /// New runtime layers, writers and guards are fully prepared before the reload is attempted. If validation or preparation fails, the currently active
@@ -282,6 +257,31 @@ pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSe
}); });
} }
fn initialize_runtime(
settings: &crate::LoggingSettings,
runtime_identity: std::option::Option<crate::LoggingRuntimeIdentity>,
) -> ksp_core_lib::Result<crate::LoggingGuard> {
return prepare_runtime_with_identity(settings, runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
let PreparedRuntime { layers, outputs } = prepared;
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(layers);
let subscriber = tracing_subscriber::registry().with(reload_layer);
let install_result = tracing::subscriber::set_global_default(subscriber);
return match install_result {
std::result::Result::Ok(()) => std::result::Result::Ok(crate::LoggingGuard {
reload_handle,
settings: settings.clone(),
runtime_identity,
outputs,
retired_dropped_lines: crate::DroppedLines::zero(),
retired_file_dropped_lines: std::collections::HashMap::new(),
}),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error),
),
};
});
}
fn prepare_runtime_with_identity( fn prepare_runtime_with_identity(
settings: &crate::LoggingSettings, settings: &crate::LoggingSettings,
runtime_identity: std::option::Option<&crate::LoggingRuntimeIdentity>, runtime_identity: std::option::Option<&crate::LoggingRuntimeIdentity>,
@@ -315,7 +315,7 @@ fn prepare_runtime_with_identity(
if output_layers.is_empty() { if output_layers.is_empty() {
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs }); return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs });
} }
output_layers.insert(0, crate::domain::DomainContextLayer::new().boxed()); output_layers.insert(0, crate::DomainContextLayer::new().boxed());
let takeover_layer = build_target_filter(settings).and_then(output_layers).boxed(); let takeover_layer = build_target_filter(settings).and_then(output_layers).boxed();
return std::result::Result::Ok(PreparedRuntime { layers: vec![takeover_layer], outputs }); return std::result::Result::Ok(PreparedRuntime { layers: vec![takeover_layer], outputs });
} }
@@ -368,7 +368,7 @@ fn build_file_output(
); );
}, },
}; };
let stripped_writer = crate::writer::StripAnsiWriter::new(appender); let stripped_writer = crate::StripAnsiWriter::new(appender);
let thread_name = format!("ksp-logging-{}", file.output_id()); let thread_name = format!("ksp-logging-{}", file.output_id());
let prepared = build_non_blocking_output(stripped_writer, thread_name.as_str(), settings.span_events(), false, false, file.format(), file.filter()); let prepared = build_non_blocking_output(stripped_writer, thread_name.as_str(), settings.span_events(), false, false, file.format(), file.filter());
let metadata = crate::RuntimeFileMetadata { let metadata = crate::RuntimeFileMetadata {
@@ -413,7 +413,7 @@ fn build_format_layer(
let span_events = map_span_events(span_events); let span_events = map_span_events(span_events);
return match format { return match format {
crate::LogFormat::Human => tracing_subscriber::fmt::layer() crate::LogFormat::Human => tracing_subscriber::fmt::layer()
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone())) .with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
.with_ansi(ansi) .with_ansi(ansi)
.with_ansi_sanitization(ansi_sanitization) .with_ansi_sanitization(ansi_sanitization)
.with_target(true) .with_target(true)
@@ -423,7 +423,7 @@ fn build_format_layer(
.boxed(), .boxed(),
crate::LogFormat::Compact => tracing_subscriber::fmt::layer() crate::LogFormat::Compact => tracing_subscriber::fmt::layer()
.compact() .compact()
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone())) .with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
.with_ansi(ansi) .with_ansi(ansi)
.with_ansi_sanitization(ansi_sanitization) .with_ansi_sanitization(ansi_sanitization)
.with_target(true) .with_target(true)
@@ -433,7 +433,7 @@ fn build_format_layer(
.boxed(), .boxed(),
crate::LogFormat::Pretty => tracing_subscriber::fmt::layer() crate::LogFormat::Pretty => tracing_subscriber::fmt::layer()
.pretty() .pretty()
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone())) .with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
.with_ansi(ansi) .with_ansi(ansi)
.with_ansi_sanitization(ansi_sanitization) .with_ansi_sanitization(ansi_sanitization)
.with_target(true) .with_target(true)
@@ -443,7 +443,7 @@ fn build_format_layer(
.boxed(), .boxed(),
crate::LogFormat::Json => tracing_subscriber::fmt::layer() crate::LogFormat::Json => tracing_subscriber::fmt::layer()
.json() .json()
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone())) .with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
.with_ansi(false) .with_ansi(false)
.with_target(true) .with_target(true)
.with_file(true) .with_file(true)

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/writer.rs // file: crates/ksp-logging-lib/src/writer.rs
// version: 3 // version: 5
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum StripAnsiState { enum StripAnsiState {
@@ -12,12 +12,14 @@ enum StripAnsiState {
StringEscape, StringEscape,
} }
/// Crate-internal `StripAnsiWriter` state shared across the owning crate.
pub(crate) struct StripAnsiWriter<W> { pub(crate) struct StripAnsiWriter<W> {
inner: W, inner: W,
state: StripAnsiState, state: StripAnsiState,
} }
impl<W> StripAnsiWriter<W> { impl<W> StripAnsiWriter<W> {
/// Creates a new `StripAnsiWriter` value.
pub(crate) const fn new(inner: W) -> Self { pub(crate) const fn new(inner: W) -> Self {
return Self { inner, state: StripAnsiState::Text }; return Self { inner, state: StripAnsiState::Text };
} }
@@ -100,6 +102,7 @@ impl<W> StripAnsiWriter<W> {
} }
} }
/// Crate-internal `RouteMakeWriter` state shared across the owning crate.
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct RouteMakeWriter<W> { pub(crate) struct RouteMakeWriter<W> {
inner: W, inner: W,
@@ -107,17 +110,19 @@ pub(crate) struct RouteMakeWriter<W> {
} }
impl<W> RouteMakeWriter<W> { impl<W> RouteMakeWriter<W> {
/// Creates a new `RouteMakeWriter` value.
pub(crate) fn new(inner: W, filter: crate::OutputFilter) -> Self { pub(crate) fn new(inner: W, filter: crate::OutputFilter) -> Self {
return Self { inner, filter }; return Self { inner, filter };
} }
} }
/// Crate-internal `RoutedWriter` variants used by the owning crate.
pub(crate) enum RoutedWriter<W> { pub(crate) enum RoutedWriter<W> {
Enabled(W), Enabled(W),
Disabled, Disabled,
} }
impl<W> std::io::Write for RoutedWriter<W> impl<W> std::io::Write for crate::RoutedWriter<W>
where where
W: std::io::Write, W: std::io::Write,
{ {
@@ -140,17 +145,17 @@ impl<'writer, W> tracing_subscriber::fmt::MakeWriter<'writer> for RouteMakeWrite
where where
W: tracing_subscriber::fmt::MakeWriter<'writer>, W: tracing_subscriber::fmt::MakeWriter<'writer>,
{ {
type Writer = RoutedWriter<W::Writer>; type Writer = crate::RoutedWriter<W::Writer>;
fn make_writer(&'writer self) -> Self::Writer { fn make_writer(&'writer self) -> Self::Writer {
return RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer(&self.inner)); return crate::RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer(&self.inner));
} }
fn make_writer_for(&'writer self, metadata: &tracing::Metadata<'_>) -> Self::Writer { fn make_writer_for(&'writer self, metadata: &tracing::Metadata<'_>) -> Self::Writer {
if metadata_matches_filter(metadata, &self.filter) { if metadata_matches_filter(metadata, &self.filter) {
return RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer_for(&self.inner, metadata)); return crate::RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer_for(&self.inner, metadata));
} }
return RoutedWriter::Disabled; return crate::RoutedWriter::Disabled;
} }
} }
@@ -163,7 +168,7 @@ fn metadata_matches_filter(metadata: &tracing::Metadata<'_>, filter: &crate::Out
}) { }) {
return false; return false;
} }
return crate::domain::current_domain_matches(filter.domains()); return crate::current_domain_matches(filter.domains());
} }
fn level_is_enabled(level: &tracing::Level, filter: crate::LogFilterLevel) -> bool { fn level_is_enabled(level: &tracing::Level, filter: crate::LogFilterLevel) -> bool {

View File

@@ -1,12 +1,12 @@
// file: crates/ksp-logging-lib/tests/overhead.rs // file: crates/ksp-logging-lib/tests/overhead.rs
// version: 1 // version: 2
//! Diagnostic gross-overhead probe for the reload layer used by KSP Logging. //! Diagnostic gross-overhead probe for the reload layer used by KSP Logging.
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
const TEST_TARGET: &str = "ksp-logging-lib";
const ITERATIONS: u64 = 200_000; const ITERATIONS: u64 = 200_000;
const TEST_TARGET: &str = "ksp-logging-lib";
fn emit_probe_events() { fn emit_probe_events() {
for sequence in 0..ITERATIONS { for sequence in 0..ITERATIONS {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/tests/ownership.rs // file: crates/ksp-logging-lib/tests/ownership.rs
// version: 2 // version: 3
//! Integration audit ensuring KSP crates do not bypass the logging facade. //! Integration audit ensuring KSP crates do not bypass the logging facade.
@@ -157,6 +157,11 @@ fn workspace_crates_do_not_bypass_ksp_logging_facade() {
std::result::Result::Err(_) => continue, std::result::Result::Err(_) => continue,
}; };
assert!(!source_uses_direct_path(source.as_str(), "tracing::"), "{} bypasses ksp-logging-lib via tracing", rust_file.display()); assert!(!source_uses_direct_path(source.as_str(), "tracing::"), "{} bypasses ksp-logging-lib via tracing", rust_file.display());
assert!(
!source_uses_direct_path(source.as_str(), "ksp_logging_lib::tracing::"),
"{} bypasses ksp-logging-lib via its hidden tracing bridge",
rust_file.display(),
);
assert!( assert!(
!source_uses_direct_path(source.as_str(), "tracing_subscriber::"), !source_uses_direct_path(source.as_str(), "tracing_subscriber::"),
"{} bypasses ksp-logging-lib via tracing-subscriber", "{} bypasses ksp-logging-lib via tracing-subscriber",

View File

@@ -1,12 +1,12 @@
// file: crates/ksp-logging-lib/tests/runtime.rs // file: crates/ksp-logging-lib/tests/runtime.rs
// version: 9 // version: 10
//! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload. //! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload.
const EXTERNAL_TARGET: &str = "sqlx";
const JSON_KSP_TARGET: &str = "ksp-logging-json-test";
const LOGGING_TARGET: &str = "ksp-logging-lib"; const LOGGING_TARGET: &str = "ksp-logging-lib";
const OTHER_KSP_TARGET: &str = "ksp-store-lib"; const OTHER_KSP_TARGET: &str = "ksp-store-lib";
const JSON_KSP_TARGET: &str = "ksp-logging-json-test";
const EXTERNAL_TARGET: &str = "sqlx";
fn logging_trace_enabled() -> bool { fn logging_trace_enabled() -> bool {
return tracing::enabled!(target: LOGGING_TARGET, tracing::Level::TRACE); return tracing::enabled!(target: LOGGING_TARGET, tracing::Level::TRACE);

View File

@@ -1,20 +1,20 @@
// file: crates/ksp-logging-lib/unit_tests/domain.rs // file: crates/ksp-logging-lib/unit_tests/domain.rs
// version: 1 // version: 2
#[test] #[test]
fn wildcard_domain_matches_with_or_without_current_domain() { fn wildcard_domain_matches_with_or_without_current_domain() {
super::set_current_domain(std::option::Option::None); super::set_current_domain(std::option::Option::None);
assert!(super::current_domain_matches(&["*".to_string()])); assert!(crate::current_domain_matches(&["*".to_string()]));
super::set_current_domain(std::option::Option::Some("logging")); super::set_current_domain(std::option::Option::Some("logging"));
assert!(super::current_domain_matches(&["*".to_string()])); assert!(crate::current_domain_matches(&["*".to_string()]));
} }
#[test] #[test]
fn named_domain_requires_current_matching_prefix() { fn named_domain_requires_current_matching_prefix() {
super::set_current_domain(std::option::Option::None); super::set_current_domain(std::option::Option::None);
assert!(!super::current_domain_matches(&["logging".to_string()])); assert!(!crate::current_domain_matches(&["logging".to_string()]));
super::set_current_domain(std::option::Option::Some("logging.runtime")); super::set_current_domain(std::option::Option::Some("logging.runtime"));
assert!(super::current_domain_matches(&["logging".to_string()])); assert!(crate::current_domain_matches(&["logging".to_string()]));
assert!(super::current_domain_matches(&["store".to_string(), "logging.runtime".to_string()])); assert!(crate::current_domain_matches(&["store".to_string(), "logging.runtime".to_string()]));
assert!(!super::current_domain_matches(&["store".to_string()])); assert!(!crate::current_domain_matches(&["store".to_string()]));
} }

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-logging-lib/unit_tests/writer.rs // file: crates/ksp-logging-lib/unit_tests/writer.rs
// version: 2 // version: 3
#[test] #[test]
fn ansi_writer_strips_csi_sequences() { fn ansi_writer_strips_csi_sequences() {
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new()); let mut writer = crate::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
let write_result = std::io::Write::write_all(&mut writer, b"before\x1b[31mred\x1b[0mafter"); let write_result = std::io::Write::write_all(&mut writer, b"before\x1b[31mred\x1b[0mafter");
assert!(write_result.is_ok()); assert!(write_result.is_ok());
assert_eq!(writer.into_inner(), b"beforeredafter"); assert_eq!(writer.into_inner(), b"beforeredafter");
@@ -11,7 +11,7 @@ fn ansi_writer_strips_csi_sequences() {
#[test] #[test]
fn ansi_writer_preserves_state_across_split_writes() { fn ansi_writer_preserves_state_across_split_writes() {
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new()); let mut writer = crate::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
let first = std::io::Write::write_all(&mut writer, b"a\x1b["); let first = std::io::Write::write_all(&mut writer, b"a\x1b[");
let second = std::io::Write::write_all(&mut writer, b"32mb"); let second = std::io::Write::write_all(&mut writer, b"32mb");
assert!(first.is_ok()); assert!(first.is_ok());
@@ -21,7 +21,7 @@ fn ansi_writer_preserves_state_across_split_writes() {
#[test] #[test]
fn ansi_writer_strips_osc_sequences_terminated_by_bell_or_st() { fn ansi_writer_strips_osc_sequences_terminated_by_bell_or_st() {
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new()); let mut writer = crate::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
let first = std::io::Write::write_all(&mut writer, b"a\x1b]0;title\x07b"); let first = std::io::Write::write_all(&mut writer, b"a\x1b]0;title\x07b");
let second = std::io::Write::write_all(&mut writer, b"c\x1b]8;;https://example.invalid\x1b\\d"); let second = std::io::Write::write_all(&mut writer, b"c\x1b]8;;https://example.invalid\x1b\\d");
assert!(first.is_ok()); assert!(first.is_ok());
@@ -43,7 +43,7 @@ fn output_level_routing_covers_all_filter_levels() {
#[test] #[test]
fn disabled_routed_writer_discards_bytes_without_error() { fn disabled_routed_writer_discards_bytes_without_error() {
let mut writer = super::RoutedWriter::<std::vec::Vec<u8>>::Disabled; let mut writer = crate::RoutedWriter::<std::vec::Vec<u8>>::Disabled;
let write_result = std::io::Write::write_all(&mut writer, b"discarded"); let write_result = std::io::Write::write_all(&mut writer, b"discarded");
let flush_result = std::io::Write::flush(&mut writer); let flush_result = std::io::Write::flush(&mut writer);
assert!(write_result.is_ok()); assert!(write_result.is_ok());

View File

@@ -0,0 +1,22 @@
# file: crates/ksp-onchain-transport-lib/Cargo.toml
# version: 3
[package]
name = "ksp-onchain-transport-lib"
version.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
reqwest = { workspace = true, features = ["rustls"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
tokio = { workspace = true, features = ["macros", "sync", "time"] }
[dev-dependencies]
tokio = { workspace = true, features = ["rt"] }
[lints]
workspace = true

View File

@@ -0,0 +1,151 @@
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
<!-- version: 9 -->
# `ksp-onchain-transport-lib`
`ksp-onchain-transport-lib` est la bibliothèque KSP propriétaire du transport on-chain Solana. Sa première surface est le transport HTTP JSON-RPC ; les extensions WebSocket et gRPC sont introduites séparément lorsque leur release les cible.
## Responsabilités
La crate possède :
- les settings runtime HTTP publics ;
- les endpoints nommés et leurs metadata provider/cluster ;
- les rôles, capabilities/request kinds et priorités ;
- la sélection/fairness/fallback du pool ;
- les limites RPS/burst/concurrence et le cooldown ;
- les deadlines et timeouts ;
- le retry/backoff borné et la règle no-resend après dispatch ambigu ;
- les enveloppes JSON-RPC 2.0 et leur validation ;
- le registre audité des méthodes Solana HTTP ;
- l'exécution générique des méthodes standard supportées ;
- les wrappers typés explicitement livrés par KSP ;
- les snapshots runtime sûrs ;
- l'observabilité Transport via `ksp-logging-lib`.
La crate ne possède ni documents Config, ni persistence Store, ni modèles Program/métier.
## Frontières de dépendances
La direction autorisée est :
```text
ksp-config-lib
-> ksp-onchain-transport-lib
-> ksp-core-lib
-> ksp-logging-lib
-> reqwest / tokio / serde
```
La direction inverse est interdite :
```text
ksp-onchain-transport-lib -X-> ksp-config-lib
ksp-onchain-transport-lib -X-> Store
ksp-onchain-transport-lib -X-> Program
ksp-onchain-transport-lib -X-> tracing direct
```
`ksp-config-lib` peut donc charger `std.transport.json` et construire `HttpTransportSettings`, tandis que Transport reste directement utilisable par un consumer qui fournit lui-même ses settings.
## Surface HTTP standard
Le registre KSP conserve deux inventaires distincts :
```text
52 méthodes HTTP courantes
14 méthodes historiques Deprecated / runtime Removed
```
Le registre porte notamment :
- catégorie ;
- request kind ;
- statut documentaire ;
- statut runtime ;
- forme de requête stable/legacy ;
- type d'opération ;
- classe de retry ;
- remplacement historique éventuel ;
- release de couverture typée KSP.
La release stable `0.2.4` complète la surface typée des **52 méthodes courantes** :
```text
0.2.1 foundation : 4
0.2.2 Accounts/Tokens/Cluster : 22
0.2.3 Transactions : 11
0.2.4 Blocks/Economics : 15
total : 52
```
`0.2.4` ajoute les dix wrappers Blocks et les cinq wrappers Economics. Les points sensibles restent notamment `getBlock` moderne + bare encoding legacy deprecated, les quatre `transactionDetails`, les versions transaction numériques génériques, `numRewardPartitions`, `commissionBps`, les overloads de `getBlocks`, les ranges de production, les valeurs d'inflation/minimum de délégation fournies par le runtime et les `null` positionnels de `getInflationReward`.
`KSP-TRANSPORT-007` impose qu'un wrapper typé couvre toutes les possibilités RPC supportées retenues par l'audit : paramètres/options, overloads et formes legacy encore supportées, contraintes déterministes utiles et variantes de réponse pertinentes sans perte. Le réaudit final `0.2.4` agrège le réaudit 37/37 de `0.2.3` avec les 15 nouveaux wrappers et porte la preuve stable à **52/52**.
Les 14 méthodes historiques restent découvrables pour la compliance mais sont `Removed` et ne sont pas simulées comme appelables.
## Résilience
L'admission est calculée par couple endpoint/rôle. Le pool applique :
1. rôle et capability ;
2. priorité croissante ;
3. round-robin dans le meilleur tier ;
4. RPS/burst ;
5. concurrence ;
6. cooldown rate-limit ;
7. fallback vers les pairs puis les tiers inférieurs ;
8. deadline commune à l'opération et ses retries.
Les retries ne sont autorisés que lorsque la metadata de méthode et l'état de dispatch les rendent sûrs. `WriteSubmission / NeverAfterDispatch` interdit tout resend automatique après un dispatch ambigu.
Les erreurs JSON-RPC applicatives ne sont pas transformées en retries transport génériques.
## Sécurité et diagnostics
Les URLs d'endpoint peuvent contenir des credentials. Elles ne sont donc pas exposées par les `Debug`, snapshots ou logs ordinaires.
Les `reqwest::Error` attachées comme source sont neutralisées avec `without_url()` avant exposition dans le contrat d'erreur KSP.
Le target de tracing est possédé explicitement par :
```text
src/constants.rs
TRACING_TARGET = "ksp-onchain-transport-lib"
```
La configuration Logging de référence conserve un fichier dédié Transport à niveau `info`. Un niveau `debug`/`trace` ciblé peut être réactivé temporairement via Config lors d'un développement ou diagnostic explicite.
## Tests
Les tests par défaut sont déterministes et n'exigent pas Internet : fixtures JSON et serveur HTTP local couvrent requêtes, réponses, retry, 429, timeout, redaction et routing.
Deux smokes Devnet opt-in sont séparés par responsabilité :
```text
Transport pur : settings programmatiques -> HttpTransportPool
-> Accounts/Tokens/Cluster représentatifs
-> trois reads Transactions
-> getBlockHeight
-> getInflationRate/getStakeMinimumDelegation
Composition historique : Config -> std.transport/devnet_public -> HttpTransportPool
-> getHealth/getGenesisHash/getVersion/getBalance
```
Le smoke Transport utilise pour sa branche Token la forme Devnet documentée `getTokenAccountsByOwner(owner, { programId }, { commitment: finalized, encoding: jsonParsed })`. L'owner est une Pubkey ordinaire de l'exemple officiel ; aucune présence de token account n'est exigée, donc une liste vide reste valide.
Les deux sont `ignored` par défaut. Le smoke Transport appartient durablement à cette crate ; le smoke cross-crates hébergé dans Config reste transitoire jusqu'à l'existence d'une surface KSP d'intégration/orchestration appropriée.
## Documentation
- [`USAGE.md`](USAGE.md) — consommation directe, Config -> Transport, API typed/raw, smokes et inspection runtime ;
- [`../../docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](../../docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md) — foundation HTTP stable ;
- [`../../docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`](../../docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md) — extension typed Accounts/Tokens/Cluster ;
- [`../../docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`](../../docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md) — matrice finale validée `0.2.2` ;
- [`../../docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](../../docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) — plan historique clôturé de `0.2.3` ;
- [`../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md) — matrice finale validée `0.2.3` ;
- [`../../docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](../../docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) — plan Blocks/Economics et compliance HTTP finale ;
- [`../../docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md`](../../docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md) — matrice finale validée `52/52 + 14/14` et audit `KSP-TRANSPORT-007` global ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — configuration standard HTTP.

View File

@@ -0,0 +1,201 @@
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
<!-- version: 9 -->
# Utilisation de `ksp-onchain-transport-lib`
Ce guide présente les surfaces publiques destinées aux consumers. Les notes de release restent dans `CHANGELOG.md` et les deltas.
## 1. Construction directe du runtime
Transport peut être utilisé sans Config. Le consumer construit les settings publics puis le pool :
```rust
let url = match ksp_onchain_transport_lib::HttpEndpointUrl::parse("https://api.devnet.solana.com") {
Ok(value) => value,
Err(error) => return Err(error),
};
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
ksp_onchain_transport_lib::HttpRoleName::new("default"),
true,
vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()],
100,
ksp_onchain_transport_lib::HttpRoleLimits::new(None, None, None, None),
);
let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new(
"solana_devnet_public",
true,
ksp_onchain_transport_lib::HttpProviderName::new("solana-public"),
ksp_onchain_transport_lib::HttpClusterName::new("devnet"),
url,
std::time::Duration::from_secs(5),
std::time::Duration::from_secs(15),
Some(8),
vec![role],
);
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(
vec![endpoint],
ksp_onchain_transport_lib::HttpRetrySettings::new(
2,
std::time::Duration::from_millis(100),
std::time::Duration::from_secs(2),
),
);
let pool = match ksp_onchain_transport_lib::HttpTransportPool::new(settings) {
Ok(value) => value,
Err(error) => return Err(error),
};
```
`HttpTransportSettings::validate()` peut être appelé explicitement avant la construction du pool lorsque le consumer veut séparer validation et initialisation.
## 2. Construction via `ksp-config-lib`
Lorsque le consumer utilise Config, la direction reste Config -> Transport :
```rust
let resolved = match engine.load_resolved_transport_config(Some("devnet_public"), &environment) {
Ok(value) => value,
Err(error) => return Err(error),
};
let pool = match ksp_onchain_transport_lib::HttpTransportPool::new(resolved.into_settings()) {
Ok(value) => value,
Err(error) => return Err(error),
};
```
Le document standard peut contenir une URL provenant d'un `KSP_SECRET_*`. La valeur réelle est transmise au runtime, mais les projections sûres et `Debug` restent redacted.
## 3. Appels typés
Les wrappers typés se trouvent directement sur `HttpTransportPool`.
```rust
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
let health = pool.get_health(&role).await;
let genesis_hash = pool.get_genesis_hash(&role).await;
let version = pool.get_version(&role).await;
let balance = pool
.get_balance(
&role,
&ksp_core_lib::PRGIDPK_SOLANA_SYSTEM,
Some(&ksp_onchain_transport_lib::GetBalanceConfig::new(
Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
None,
)),
)
.await;
```
Les quatre canaris `0.2.1` restent disponibles. `0.2.2` ajoute les wrappers typés Accounts, Tokens et Cluster. Exemples représentatifs :
```rust
let account = pool
.get_account_info(&role, &ksp_core_lib::PRGIDPK_SOLANA_SYSTEM, None)
.await;
let epoch = pool.get_epoch_info(&role, None).await;
let vote_accounts = pool.get_vote_accounts(&role, None).await;
```
La release stable `0.2.4` contient les **52 wrappers typés courants** : 4 foundation + 22 Accounts/Tokens/Cluster + 11 Transactions + 10 Blocks + 5 Economics. Les DTOs Transport conservent les `null`, options, overloads et formes wire sans décodage Program/SPL métier.
Exemples Transaction représentatifs :
```rust
let context = ksp_onchain_transport_lib::SolanaContextConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
std::option::Option::None,
);
let latest = pool
.get_latest_blockhash(&role, std::option::Option::Some(&context))
.await;
let transaction_count = pool
.get_transaction_count(&role, std::option::Option::Some(&context))
.await;
```
`getTransaction` expose la config moderne complète et une forme bare-encoding legacy séparée et deprecated. `requestAirdrop` et `sendTransaction` sont des write submissions : elles utilisent la protection centrale `NeverAfterDispatch`. `simulateTransaction` reste une simulation retry-safe et conserve son résultat riche sans introduire de décodage Program.
Exemples Blocks/Economics représentatifs :
```rust
let block_height = pool.get_block_height(&role, Some(&context)).await;
let inflation_rate = pool.get_inflation_rate(&role).await;
let stake_minimum = pool.get_stake_minimum_delegation(&role, Some(&context)).await;
```
`getBlock` possède également une forme bare-encoding legacy séparée et deprecated. Les valeurs Economics restent celles du runtime : le consumer ne doit pas supposer localement un taux d'inflation ou un minimum de délégation constant.
## 4. Exécution JSON-RPC standard générique
Une méthode courante auditée peut être appelée via son descriptor :
```rust
if let Some(descriptor) = ksp_onchain_transport_lib::find_http_rpc_method("getSlot") {
let _result = pool.execute_standard_rpc(&role, descriptor, vec![]).await;
}
```
Cette API retourne un `serde_json::Value`. Elle reste utile pour les extensions provider, les diagnostics et les méthodes hors registre standard, mais elle ne remplace jamais le wrapper typé d'une méthode HTTP standard désormais couverte par KSP.
Avant exécution, `ensure_runtime_supported()` est appliqué. Une méthode historique `Removed` retourne `ERROR_CODE_METHOD_REMOVED` au lieu d'émettre un appel réseau fictif.
## 5. Sélection et admission sans exécuter la requête
Pour inspecter le routing :
```rust
if let Some(descriptor) = ksp_onchain_transport_lib::find_http_rpc_method("getBalance") {
let _selection = pool.select_for_method(&role, descriptor);
let _permit = pool.acquire_for_method(&role, descriptor).await;
}
```
Dans le même bloc, `acquire_for_method()` réserve réellement la capacité RPS/concurrence sous deadline.
`HttpRequestPermit` détient la capacité de concurrence jusqu'à sa destruction. Aucun verrou synchrone n'est conservé pendant l'attente réseau.
## 6. Snapshots runtime
`HttpTransportPool::snapshot()` fournit une vue sûre des endpoints/rôles : disponibilité, limites, requêtes en vol, cooldown restant et compteurs runtime.
Les URLs d'endpoint n'y apparaissent jamais.
## 7. Retry et write submissions
La policy de retry est portée par la metadata des méthodes et `evaluate_transport_retry()`.
Les reads/simulations classés `RetrySafe` peuvent être réessayés dans le budget configuré lorsqu'une cause transport est explicitement retryable.
Pour une opération `WriteSubmission / NeverAfterDispatch`, un timeout ou autre résultat ambigu après dispatch arrête la resoumission automatique. Le consumer métier ne doit pas contourner cette protection avec une boucle de retry externe aveugle.
## 8. Logging
Les événements Transport utilisent le target :
```text
ksp-onchain-transport-lib
```
Ne jamais journaliser l'URL complète, un token provider, un body massif, une transaction complète ou une réponse complète.
La configuration standard route les événements `info` de Transport vers un fichier dédié. Pour une investigation temporaire, élever uniquement ce target/sink à `debug` ou `trace`, puis revenir à `info` avant clôture du développement.
## 9. Smokes Devnet opt-in
Le smoke **Transport pur** construit ses settings programmatiquement et exerce un sous-ensemble représentatif d'Accounts/Tokens/Cluster, trois reads Transactions, puis des reads Blocks/Economics de la release stable `0.2.4` :
```bash
cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture
```
Il appelle `getAccountInfo`, `getTokenAccountsByOwner`, `getEpochInfo`, `getVoteAccounts`, puis `getLatestBlockhash`, `isBlockhashValid`, `getTransactionCount`, `getBlockHeight`, `getInflationRate` et `getStakeMinimumDelegation`. La branche Token suit la forme Devnet documentée : owner Pubkey ordinaire de l'exemple officiel, selector `programId` avec l'ID canonique du programme SPL Token, puis config explicite `commitment: finalized` + `encoding: jsonParsed`. Une réponse vide reste acceptable. La branche Transaction reste read-only : elle ne déclenche ni airdrop ni soumission de transaction et ne remplace pas les fixtures déterministes couvrant les 11 wrappers.
Le smoke historique de **composition Config -> Transport** reste également disponible :
```bash
cargo test -p ksp-config-lib --test transport_devnet_smoke -- --ignored --nocapture
```
Il valide le profil committé `devnet_public` et les quatre canaris foundation. Il reste transitoirement hébergé dans Config : les futurs smokes cross-crates ne doivent pas faire de Config leur destination générale et devront migrer vers une surface d'intégration/orchestration dédiée lorsqu'elle existera.
Les endpoints publics Solana sont rate-limités et non destinés à la production. Un échec réseau externe n'est pas assimilé automatiquement à une régression locale ; les fixtures HTTP locales restent les gates reproductibles.

View File

@@ -0,0 +1,5 @@
[
{"lamports":1,"data":"3MN5","owner":"11111111111111111111111111111111","executable":false,"rentEpoch":0,"space":null},
{"lamports":2,"data":["KLUv/Q==","base64+zstd"],"owner":"11111111111111111111111111111111","executable":false,"rentEpoch":1,"space":4},
{"lamports":3,"data":{"program":"spl-token","parsed":{"type":"account","info":{"state":"initialized"}},"space":165},"owner":"11111111111111111111111111111111","executable":false,"rentEpoch":2,"space":165}
]

View File

@@ -0,0 +1,4 @@
[
{"commitment":[1,2,3,4],"totalStake":1000000000},
{"commitment":null,"totalStake":1000000000}
]

View File

@@ -0,0 +1,7 @@
{
"byIdentity": {
"11111111111111111111111111111111": [8,7],
"Vote111111111111111111111111111111111111111": [4,4]
},
"range": {"firstSlot":430000000,"lastSlot":430000099}
}

View File

@@ -0,0 +1 @@
{"pubkey":"11111111111111111111111111111111","featureSet":123,"gossip":"127.0.0.1:8001","pubsub":null,"rpc":"127.0.0.1:8899","serveRepair":"127.0.0.1:8003","shredVersion":456,"tpu":"127.0.0.1:8004","tpuForwards":null,"tpuForwardsQuic":"127.0.0.1:8006","tpuQuic":"127.0.0.1:8005","tpuVote":null,"tvu":"127.0.0.1:8002","version":"4.2.1","clientId":"Agave"}

View File

@@ -0,0 +1,20 @@
[
{
"previousBlockhash":"previous-a",
"blockhash":"block-a",
"parentSlot":1,
"blockTime":null,
"blockHeight":null
},
{
"previousBlockhash":"previous-b",
"blockhash":"block-b",
"parentSlot":2,
"transactions":null,
"signatures":null,
"rewards":null,
"numRewardPartitions":null,
"blockTime":null,
"blockHeight":null
}
]

View File

@@ -0,0 +1,36 @@
{
"previousBlockhash":"previous-blockhash-fixture",
"blockhash":"blockhash-fixture",
"parentSlot":430000122,
"transactions":[
{
"transaction":{"signatures":["signature-fixture"],"message":{"accountKeys":[],"recentBlockhash":"recent-fixture","instructions":[]}},
"meta":{"err":null,"fee":5000},
"version":1
},
{
"transaction":["AQID","base64"],
"meta":null
}
],
"rewards":[
{
"pubkey":"11111111111111111111111111111111",
"lamports":2500,
"postBalance":1000002500,
"rewardType":"Fee",
"commission":null,
"commissionBps":1234
},
{
"pubkey":"Vote111111111111111111111111111111111111111",
"lamports":1250,
"postBalance":2000001250,
"rewardType":"Voting",
"commission":5
}
],
"numRewardPartitions":4,
"blockTime":1787072400,
"blockHeight":410000000
}

View File

@@ -0,0 +1,36 @@
[
{
"slot": 430000123,
"blockTime": 1787000000,
"transaction": ["AQIDBAUGBwg=", "base64"],
"meta": {
"err": null,
"status": {"Ok": null},
"fee": 5000,
"preBalances": [10000],
"postBalances": [5000]
},
"version": 0,
"transactionIndex": 7
},
{
"slot": 430000100,
"blockTime": null,
"transaction": "3Bxs4NN8M2Yn4TLb",
"meta": null,
"version": "legacy",
"transactionIndex": null
},
{
"slot": 430000050,
"blockTime": null,
"transaction": {
"signatures": [],
"message": {
"accountKeys": [],
"recentBlockhash": "11111111111111111111111111111111",
"instructions": []
}
}
}
]

View File

@@ -0,0 +1,12 @@
[
"3Bxs4NN8M2Yn4TLb",
["AQIDBAUGBwg=", "base64"],
{
"signatures": ["5h6xBEauJ3PK6SWQkgLxHcQpWyRZyG7c4pUpK9fd2B2B7hRQ1S6F9J7BqvQPEmxQxK4DThfYF5f5f5f5f5f5f5f5"],
"message": {
"accountKeys": [],
"recentBlockhash": "11111111111111111111111111111111",
"instructions": []
}
}
]

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000003},"value":{"data":["","base64"],"executable":false,"lamports":1,"owner":"not-a-pubkey","rentEpoch":0,"space":0}},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000002},"value":null},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000001},"value":{"data":["AQIDBA==","base64"],"executable":false,"lamports":2039280,"owner":"11111111111111111111111111111111","rentEpoch":18446744073709551615,"space":4}},"id":1}

Some files were not shown because too many files have changed in this diff Show More