v0.3.15-pre.006

This commit is contained in:
2026-09-13 11:30:56 +02:00
parent 9ee3cd4a53
commit 0a8ce6e5f9
23 changed files with 1298 additions and 112 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 601
# version: 602
[workspace]
resolver = "3"
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
[workspace.package]
version = "0.3.15-pre.5.fix.2"
version = "0.3.15-pre.6"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-app-raw-transaction-ingest-desk/Cargo.toml
# version: 1
# version: 2
[package]
name = "ksp-app-raw-transaction-ingest-desk"
@@ -27,6 +27,7 @@ fs2.workspace = true
ksp-config-lib = { path = "../ksp-config-lib" }
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
serde = { workspace = true, features = ["derive"] }
tauri.workspace = true
tauri-plugin-tracing.workspace = true

View File

@@ -1,11 +1,11 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# `ksp-app-raw-transaction-ingest-desk`
Application desktop Tauri KSP destinée à composer et superviser les routes live alimentant le Store commun en `RawTransaction`.
Le composant conserve les frontières KSP : Config possède les profils/endpoints/secrets, Transport les clients et sessions, Store la persistance et le Worker la logique d'ingestion. Le shell desktop ne doit jamais déplacer ces responsabilités vers le frontend.
Le composant conserve les frontières KSP : Config possède les profils/endpoints/secrets, Transport les clients et sessions, Store la persistance et le Worker la logique d'ingestion. Le shell desktop ne déplace jamais ces responsabilités vers le frontend.
## Package
@@ -18,14 +18,18 @@ bin : ksp-app-raw-transaction-ingest-desk
## Frontières
```text
Config -> composite + Logging + ressources packagées
Desk -> route IDs/states statiques + Tauri + projections sûres
Transport / Store / Worker -> non construits dans cette tranche
Config -> composite, profils, secrets et résolution effective
Transport -> capabilities HTTP/WS/gRPC et ressources physiques
Desk -> catalogue logique, inventaire Config-only et projections sûres
Store -> persistance via la façade KSP
Worker -> acquisition continue et lifecycle d'exécution
```
Le frontend ne reçoit aucune URL, credential, URI Store, source key ou payload `RawTransaction`.
L'inventaire de routes est calculé côté Rust à partir du composite Config validé et des settings Transport/Store résolus. Il ne lance aucun probe réseau et n'ouvre ni Store ni Worker.
## Routes V1 réservées
Le frontend ne reçoit aucune URL, credential, URI Store, metadata secrète, source key, handle runtime ou payload `RawTransaction`.
## Routes V1
```text
yellowstone-hydrated
@@ -35,7 +39,7 @@ helius-transaction-hydrated
http-block-polling
```
Leur présence dans l'interface n'est pas une preuve de composabilité : seule la projection backend issue de Config et des préconditions runtime peut déclarer une route disponible.
Une route `Configured` est uniquement **composable depuis Config**. La disponibilité réelle des connexions, du Store et du Worker est revalidée lors du Start avant qu'une route puisse devenir `Running`.
## Ports de développement
@@ -50,4 +54,6 @@ Lancement :
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
```
Le cycle frontend est déclenché par Tauri via ses hooks configurés ; les scripts npm applicatifs ne sont pas lancés directement par l'opérateur.
Voir [`USAGE.md`](USAGE.md) et [`../../docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md`](../../docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md).

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# Utilisation de `ksp-app-raw-transaction-ingest-desk`
@@ -11,22 +11,56 @@ Depuis la racine du workspace :
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
```
Tauri déclenche lui-même les hooks frontend configurés. Les scripts npm applicatifs de développement, contrôle ou build ne sont pas lancés directement par l'opérateur.
Le runtime prépare les ressources Config nécessaires, charge le composite dédié, initialise Logging puis ouvre le shell desktop.
## 2. Vue Routes
La vue Routes présente les routes logiques connues par l'application et leur état applicatif. La présence d'un identifiant de route dans l'interface ne constitue pas, à elle seule, une preuve de composabilité ou de disponibilité runtime ; l'état projeté par le backend reste l'autorité.
La vue Routes charge un inventaire déterministe construit côté Rust depuis le composite Config validé. Le sélecteur présente les profils logiques du composite et la vue projette, pour chaque route V1 :
Les URLs, tokens, metadata secrètes, handles Transport/Store/Worker et payloads RAW restent backend-only.
```text
route_id
famille et label sûrs
réseau logique lorsque résolu
état Config
selectable oui/non
raison sûre lorsque la route est indisponible
génération d'inventaire
```
## 3. Diagnostics
`Configured` signifie uniquement que toutes les requirements Config nécessaires à la route sont cohérentes. Aucun probe réseau, aucune ouverture de Store et aucun Worker ne sont déclenchés par l'inventaire.
La vue Diagnostics expose uniquement des informations techniques sûres nécessaires à l'exploitation du Desk, notamment l'identité de version, l'état du shell, la composition Config active, l'état Logging et les diagnostics projetés par codes/messages sûrs.
Un refresh reconstruit l'inventaire depuis Config et publie une nouvelle génération. La sélection visuelle d'un profil ne choisit aucune URL ou ressource physique et ne démarre aucune acquisition.
Les URLs, tokens, metadata secrètes, handles Transport/Store/Worker, URI Store, source keys et payloads RAW restent backend-only.
## 3. Raisons d'indisponibilité
Les reasons exposées sont des codes applicatifs bornés, par exemple :
```text
profile_unresolved
network_mismatch
missing_http_get_transaction
missing_http_block_scan
missing_ws_logs_capability
missing_ws_block_capability
missing_helius_transaction_capability
missing_yellowstone_grpc
missing_required_secret
```
Le texte arbitraire d'une erreur provider ou d'un secret Config n'est jamais projeté dans l'inventaire.
## 4. Diagnostics
La vue Diagnostics expose uniquement des informations techniques sûres nécessaires à l'exploitation du Desk, notamment l'identité de version, l'état du shell, la composition Config bootstrap, l'état Logging et la génération d'inventaire.
Les diagnostics ne doivent pas exposer de credentials, d'URI Store, d'URL d'endpoint, de source key, de handle runtime ou de payload `RawTransaction`.
## 4. Runtime packagé
## 5. Runtime packagé
Le build Tauri embarque les documents Config et schemas enregistrés. `ksp-config-lib` prépare ensuite la racine KSP user-writable sans embarquer `.env`.
Les fichiers Config et les secrets restent administrés par les mécanismes Config KSP ; l'interface ne doit pas contourner cette ownership.
Les fichiers Config et les secrets restent administrés par les mécanismes Config KSP ; l'interface ne contourne pas cette ownership.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html -->
<!-- version: 2 -->
<!-- version: 3 -->
<!DOCTYPE html>
<html lang="fr">
@@ -21,7 +21,10 @@
<span class="mx-2 fs-5 text-body-secondary" aria-hidden="true"></span>
<span id="headerViewTitle" class="fs-5 text-body text-nowrap">Routes</span>
</div>
<span id="headerPhase" class="badge text-bg-secondary ms-auto">scaffold</span>
<div class="d-flex align-items-center gap-2 ms-auto">
<span id="headerProfile" class="badge text-bg-light border text-dark"></span>
<span id="headerPhase" class="badge text-bg-secondary">inventory</span>
</div>
</div>
</nav>
</header>
@@ -48,29 +51,38 @@
<div class="d-flex align-items-start justify-content-between gap-3 flex-wrap mb-4">
<div>
<h1 class="h3 mb-1">Routes live</h1>
<p class="text-body-secondary mb-0">Fondation statique V1 ; la composabilité Config sera matérialisée en pre.006.</p>
<p class="text-body-secondary mb-0">Inventaire déterministe depuis Config, sans probe réseau ni démarrage de Worker.</p>
</div>
<button id="refreshRouteFoundation" class="btn btn-outline-primary btn-sm" type="button">
<button id="refreshRouteInventory" class="btn btn-outline-primary btn-sm" type="button">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
</button>
</div>
<div class="alert alert-info" role="status">
Aucun Worker n'est démarré par ce scaffold. Les cinq IDs ci-dessous ne constituent aucune preuve de disponibilité runtime.
</div>
<div id="routeFoundation" class="row g-3 app-route-grid" aria-live="polite"></div>
<div class="card shadow-sm mt-4">
<div class="card-header fw-semibold">États réservés</div>
<div class="card-body">
<div id="routeStates" class="d-flex flex-wrap gap-2"></div>
<div class="row g-3 align-items-end mb-4">
<div class="col-12 col-md-6">
<label class="form-label" for="routeProfile">Profil applicatif</label>
<select id="routeProfile" class="form-select" aria-label="Profil Raw Transaction Ingest Desk"></select>
</div>
<div class="col-6 col-md-3">
<div class="small text-body-secondary">Réseau</div>
<div id="routeNetwork" class="fw-semibold app-route-code"></div>
</div>
<div class="col-6 col-md-3">
<div class="small text-body-secondary">Génération</div>
<div id="routeInventoryGeneration" class="fw-semibold app-route-code"></div>
</div>
</div>
<div class="alert alert-info" role="status">
<strong>Configured</strong> signifie uniquement que Config prouve la composabilité. Les connexions, le Store et le Worker seront revalidés au Start dans les tranches suivantes.
</div>
<div class="small text-body-secondary mb-3">Profil affiché : <span id="routeSelectedProfile" class="app-route-code"></span></div>
<div id="routeFoundation" class="row g-3 app-route-grid" aria-live="polite"></div>
</section>
<section data-view-panel="diagnostics" hidden>
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
<div>
<h1 class="h3 mb-1">Diagnostics</h1>
<p class="text-body-secondary mb-0">État sûr du scaffold Config, Logging et desktop.</p>
<p class="text-body-secondary mb-0">État sûr du shell Config, Logging et de la génération d'inventaire.</p>
</div>
<button id="refreshRuntimeStatus" class="btn btn-outline-primary btn-sm" type="button">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
@@ -83,9 +95,11 @@
<dt class="col-sm-5">Version</dt><dd id="runtimeVersion" class="col-sm-7"></dd>
<dt class="col-sm-5">Phase</dt><dd id="runtimeShellPhase" class="col-sm-7"></dd>
<dt class="col-sm-5">Documents Config</dt><dd id="runtimeConfigDocuments" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil composite</dt><dd id="runtimeCompositeProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil composite bootstrap</dt><dd id="runtimeCompositeProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil Logging</dt><dd id="runtimeLoggingProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Fallback Logging</dt><dd id="runtimeLoggingFallback" class="col-sm-7"></dd>
<dt class="col-sm-5">Inventaire default</dt><dd id="runtimeInventoryDefaultProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Génération inventaire</dt><dd id="runtimeInventoryGeneration" class="col-sm-7"></dd>
</dl>
<div id="runtimeDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
</div>

View File

@@ -1,11 +1,11 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts
// version: 2
// version: 3
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
import "simplebar";
import type { ShellStatusDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_common/ShellStatusDto.ts";
import type { RawIngestRouteFoundationDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteFoundationDto.ts";
import type { RawIngestRouteInventoryDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteInventoryDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
@@ -19,6 +19,9 @@ const viewTitles: Record<ViewId, string> = {
diagnostics: "Diagnostics",
};
let routeInventory: RawIngestRouteInventoryDto | null = null;
let selectedProfileId: string | null = null;
function isViewId(value: string | undefined): value is ViewId {
return value === "routes" || value === "diagnostics";
}
@@ -75,22 +78,40 @@ function bindNavigation(): void {
});
}
function routeLabel(routeId: string): string {
function reasonLabel(reason: string | null): string {
const labels: Record<string, string> = {
"yellowstone-hydrated": "Yellowstone + HTTP hydration",
"standard-logs-hydrated": "Standard Logs + HTTP hydration",
"standard-block-direct": "Standard Block direct",
"helius-transaction-hydrated": "Helius Transaction + HTTP hydration",
"http-block-polling": "HTTP Block Polling",
profile_unresolved: "profil Config non résolu",
network_mismatch: "réseau Transport/Store incohérent",
missing_http_get_transaction: "HTTP getTransaction indisponible",
missing_http_block_scan: "HTTP block scan incomplet",
missing_ws_logs_capability: "capability WS Logs absente",
missing_ws_block_capability: "capability WS Block absente",
missing_helius_transaction_capability: "capability Helius Transaction absente",
missing_yellowstone_grpc: "Yellowstone gRPC absent",
missing_required_secret: "secret Config requis non résolu",
};
return labels[routeId] ?? routeId;
if (reason === null) {
return "composable depuis Config";
}
return labels[reason] ?? reason;
}
function renderRouteFoundation(foundation: RawIngestRouteFoundationDto): void {
function renderSelectedProfile(): void {
if (routeInventory === null) {
return;
}
const profile = routeInventory.profiles.find(candidate => candidate.profileId === selectedProfileId) ?? routeInventory.profiles[0];
if (!profile) {
return;
}
selectedProfileId = profile.profileId;
text("#routeNetwork", profile.network ?? "non résolu");
text("#routeSelectedProfile", profile.profileId);
text("#headerProfile", profile.profileId);
const routeRoot = document.querySelector<HTMLElement>("#routeFoundation");
if (routeRoot) {
routeRoot.replaceChildren();
for (const routeId of foundation.routeIds) {
for (const route of profile.routes) {
const column = document.createElement("div");
column.className = "col-12 col-xl-6 app-route-column";
const card = document.createElement("div");
@@ -99,32 +120,58 @@ function renderRouteFoundation(foundation: RawIngestRouteFoundationDto): void {
body.className = "card-body";
const title = document.createElement("h2");
title.className = "h6 mb-2";
title.textContent = routeLabel(routeId);
title.textContent = route.label;
const code = document.createElement("div");
code.className = "app-route-code text-body-secondary small";
code.textContent = routeId;
const badge = document.createElement("span");
badge.className = "badge text-bg-secondary mt-3";
badge.textContent = "foundation only";
body.append(title, code, badge);
code.textContent = route.routeId;
const meta = document.createElement("div");
meta.className = "d-flex flex-wrap gap-2 mt-3 app-route-meta";
const state = document.createElement("span");
state.className = route.selectable ? "badge text-bg-success" : "badge text-bg-secondary";
state.textContent = route.state;
const network = document.createElement("span");
network.className = "badge text-bg-light border text-dark";
network.textContent = route.network ?? "network unresolved";
const reason = document.createElement("span");
reason.className = route.selectable ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
reason.textContent = reasonLabel(route.reason);
meta.append(state, network, reason);
body.append(title, code, meta);
card.append(body);
column.append(card);
routeRoot.append(column);
}
}
const statesRoot = document.querySelector<HTMLElement>("#routeStates");
if (statesRoot) {
statesRoot.replaceChildren();
for (const state of foundation.states) {
const badge = document.createElement("span");
badge.className = "badge text-bg-light border text-dark";
badge.textContent = state;
statesRoot.append(badge);
frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory profile rendered", {
profileId: profile.profileId,
routeCount: profile.routes.length,
selectableCount: profile.routes.filter(route => route.selectable).length,
});
}
function renderRouteInventory(inventory: RawIngestRouteInventoryDto): void {
routeInventory = inventory;
const requestedProfile = selectedProfileId;
const selectedStillExists = requestedProfile !== null && inventory.profiles.some(profile => profile.profileId === requestedProfile);
selectedProfileId = selectedStillExists ? requestedProfile : inventory.defaultProfile;
const selector = document.querySelector<HTMLSelectElement>("#routeProfile");
if (selector) {
selector.replaceChildren();
for (const profile of inventory.profiles) {
const option = document.createElement("option");
option.value = profile.profileId;
option.textContent = profile.isDefault ? `${profile.profileId} (default)` : profile.profileId;
option.selected = profile.profileId === selectedProfileId;
selector.append(option);
}
}
frontendTrace("main", "Raw Transaction Ingest Desk route foundation rendered", {
routeCount: foundation.routeIds.length,
stateCount: foundation.states.length,
text("#routeInventoryGeneration", inventory.generation.toString());
text("#runtimeInventoryGeneration", inventory.generation.toString());
text("#runtimeInventoryDefaultProfile", inventory.defaultProfile);
renderSelectedProfile();
frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory rendered", {
generation: inventory.generation,
profileCount: inventory.profiles.length,
});
}
@@ -154,10 +201,10 @@ function renderRuntimeStatus(status: ShellStatusDto): void {
});
}
async function refreshRouteFoundation(): Promise<void> {
frontendDebug("main", "Raw Transaction Ingest Desk route foundation refresh requested");
const foundation = await invokeKsp<RawIngestRouteFoundationDto>("main", "get_route_foundation");
renderRouteFoundation(foundation);
async function refreshRouteInventory(): Promise<void> {
frontendDebug("main", "Raw Transaction Ingest Desk Config route inventory refresh requested");
const inventory = await invokeKsp<RawIngestRouteInventoryDto>("main", "get_route_inventory");
renderRouteInventory(inventory);
}
async function refreshRuntimeStatus(): Promise<void> {
@@ -167,12 +214,21 @@ async function refreshRuntimeStatus(): Promise<void> {
}
function bindRefreshControls(): void {
document.querySelector<HTMLButtonElement>("#refreshRouteFoundation")?.addEventListener("click", () => {
void refreshRouteFoundation().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk route foundation refresh failed"));
document.querySelector<HTMLButtonElement>("#refreshRouteInventory")?.addEventListener("click", () => {
void refreshRouteInventory().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk Config route inventory refresh failed"));
});
document.querySelector<HTMLButtonElement>("#refreshRuntimeStatus")?.addEventListener("click", () => {
void refreshRuntimeStatus().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk runtime status refresh failed"));
});
document.querySelector<HTMLSelectElement>("#routeProfile")?.addEventListener("change", event => {
const selector = event.currentTarget;
if (!(selector instanceof HTMLSelectElement)) {
return;
}
selectedProfileId = selector.value;
frontendDebug("main", "Raw Transaction Ingest Desk logical profile selected", { profileId: selectedProfileId });
renderSelectedProfile();
});
}
async function initializeMain(): Promise<void> {
@@ -181,9 +237,9 @@ async function initializeMain(): Promise<void> {
bindNavigation();
bindRefreshControls();
activateView("routes", "startup");
await refreshRouteFoundation();
await refreshRouteInventory();
await refreshRuntimeStatus();
frontendInfo("main", "Raw Transaction Ingest Desk scaffold frontend ready");
frontendInfo("main", "Raw Transaction Ingest Desk Config inventory frontend ready");
}
void initializeMain().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk frontend initialization failed"));

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
// version: 1
// version: 2
//! Shared backend state owned by the Raw Transaction Ingest Desk Tauri application.
@@ -7,6 +7,7 @@
pub(crate) struct AppState {
active_composite_profile: std::option::Option<String>,
config_management: ksp_config_lib::ConfigManagement,
inventory_generation: std::sync::Mutex<u32>,
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
splash_settings: crate::SplashSettings,
splash_sequence_started: std::sync::atomic::AtomicBool,
@@ -47,6 +48,7 @@ impl crate::AppState {
return std::result::Result::Ok(Self {
active_composite_profile,
config_management,
inventory_generation: std::sync::Mutex::new(0),
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
guard: logging_startup.guard,
active_profile_id: logging_startup.active_profile_id,
@@ -65,6 +67,37 @@ impl crate::AppState {
return crate::RawIngestRouteFoundationDto::scaffold();
}
/// Rebuilds the complete Config-only route inventory and publishes a fresh monotonic generation.
pub(crate) fn route_inventory(&self) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
let generation = self.inventory_generation.lock();
let mut generation = match generation {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
));
},
};
let next_generation = generation.checked_add(1);
let next_generation = match next_generation {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_INVENTORY_INVALID,
"Raw Transaction Ingest Desk route inventory generation is exhausted",
));
},
};
let inventory = crate::build_route_inventory(&self.config_management, next_generation);
let inventory = match inventory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
*generation = next_generation;
return std::result::Result::Ok(inventory);
}
/// Builds the safe scaffold status exposed by the shell.
pub(crate) fn shell_status(&self) -> ksp_core_lib::Result<crate::ShellStatusDto> {
let document_count = self.config_management.engine().registry().descriptors().count();
@@ -98,7 +131,7 @@ impl crate::AppState {
application_version: env!("CARGO_PKG_VERSION").to_owned(),
config_document_count: document_count,
fallback_logging_active: runtime.fallback_active,
shell_phase: "pre.005-scaffold".to_owned(),
shell_phase: "pre.006-route-inventory".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
});
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/bootstrap.rs
// version: 1
// version: 2
//! Config composite and Logging bootstrap for Raw Transaction Ingest Desk.
@@ -46,13 +46,21 @@ pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_li
/// Loads the concrete Raw Transaction Ingest Desk Config composite using its registered logical `file_id` and autonomous default profile.
pub(crate) fn load_raw_transaction_ingest_desk_composite(
management: &ksp_config_lib::ConfigManagement,
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
return crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::None);
}
/// Loads one explicit or default Raw Transaction Ingest Desk composite profile by registered logical file identifier.
pub(crate) fn load_raw_transaction_ingest_desk_composite_profile(
management: &ksp_config_lib::ConfigManagement,
requested_profile: std::option::Option<&str>,
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return management.engine().load_resolved_composite(&file_id, std::option::Option::None);
return management.engine().load_resolved_composite(&file_id, requested_profile);
}
/// Returns one required standard profile from the Raw Transaction Ingest Desk composite after validating the referenced logical `file_id`.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs
// version: 1
// version: 2
//! Application-owned tracing targets and Config component identifiers.
@@ -13,6 +13,8 @@ pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT: &str = "transport";
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "raw_transaction_ingest.bootstrap";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used while rebuilding Config-only route inventory.
pub(crate) const TRACING_DOMAIN_INVENTORY: &str = "raw_transaction_ingest.inventory";
/// Structured domain used by the Raw Transaction Ingest Desk shell.
pub(crate) const TRACING_DOMAIN_SHELL: &str = "raw_transaction_ingest.shell";
/// Structured domain used by Tauri window lifecycle operations.

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
// version: 1
// version: 2
//! Safe route-identity and lifecycle-state foundation for Raw Transaction Ingest Desk.
//! Safe route-identity, Config-composability and lifecycle DTOs for Raw Transaction Ingest Desk.
use ts_rs::TS; // rust-rules: trait-import
@@ -28,6 +28,72 @@ impl crate::RawIngestRouteId {
pub(crate) fn all() -> std::vec::Vec<Self> {
return vec![Self::YellowstoneHydrated, Self::StandardLogsHydrated, Self::StandardBlockDirect, Self::HeliusTransactionHydrated, Self::HttpBlockPolling];
}
/// Returns the application-owned route family used by safe UI projections.
#[must_use]
pub(crate) const fn family(self) -> crate::RawIngestRouteFamily {
return match self {
Self::YellowstoneHydrated => crate::RawIngestRouteFamily::Yellowstone,
Self::StandardLogsHydrated => crate::RawIngestRouteFamily::StandardLogs,
Self::StandardBlockDirect => crate::RawIngestRouteFamily::StandardBlock,
Self::HeliusTransactionHydrated => crate::RawIngestRouteFamily::HeliusTransaction,
Self::HttpBlockPolling => crate::RawIngestRouteFamily::HttpBlockPolling,
};
}
/// Returns the stable operator-facing route label without exposing physical endpoint metadata.
#[must_use]
pub(crate) const fn label(self) -> &'static str {
return match self {
Self::YellowstoneHydrated => "Yellowstone + HTTP hydration",
Self::StandardLogsHydrated => "Standard Logs + HTTP hydration",
Self::StandardBlockDirect => "Standard Block direct",
Self::HeliusTransactionHydrated => "Helius Transaction + HTTP hydration",
Self::HttpBlockPolling => "HTTP Block Polling",
};
}
}
/// Stable application-owned route families used only for safe presentation and orchestration.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteFamily.ts")]
pub(crate) enum RawIngestRouteFamily {
/// Yellowstone gRPC subscription route.
Yellowstone,
/// Standard Solana logs WebSocket route.
StandardLogs,
/// Standard Solana block WebSocket route.
StandardBlock,
/// Helius transaction WebSocket route.
HeliusTransaction,
/// HTTP block polling route.
HttpBlockPolling,
}
/// Safe reasons explaining why one logical route cannot currently be composed from Config.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteUnavailableReason.ts")]
pub(crate) enum RawIngestRouteUnavailableReason {
/// The selected composite or one of its typed Config components could not be resolved safely.
ProfileUnresolved,
/// Transport and Store do not prove one identical logical network.
NetworkMismatch,
/// No HTTP role can satisfy `getTransaction` for same-network hydration.
MissingHttpGetTransaction,
/// No HTTP role can satisfy the complete block-polling method set.
MissingHttpBlockScan,
/// No enabled standard WebSocket endpoint explicitly declares `Logs`.
MissingWsLogsCapability,
/// No enabled standard WebSocket endpoint explicitly declares `Block`.
MissingWsBlockCapability,
/// No enabled Helius WebSocket endpoint explicitly declares `HeliusTransaction`.
MissingHeliusTransactionCapability,
/// No enabled same-network Yellowstone gRPC endpoint is configured.
MissingYellowstoneGrpc,
/// A required Config secret is not resolved for the selected composite profile.
MissingRequiredSecret,
}
/// Stable Raw Transaction Ingest Desk route lifecycle states.
@@ -59,25 +125,104 @@ impl crate::RawIngestRouteState {
}
}
/// Static route/state foundation exposed by the scaffold before Config route inventory exists.
/// Static route/state foundation retained as the bounded vocabulary behind the live Config inventory.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteFoundationDto.ts")]
pub(crate) struct RawIngestRouteFoundationDto {
/// Five stable V1 logical route identifiers; this is not a composability claim.
pub(crate) route_ids: std::vec::Vec<crate::RawIngestRouteId>,
/// Stable lifecycle state vocabulary reserved for later runtime projections.
/// Stable lifecycle state vocabulary reserved for runtime projections.
pub(crate) states: std::vec::Vec<crate::RawIngestRouteState>,
}
impl crate::RawIngestRouteFoundationDto {
/// Builds the static scaffold contract without resolving endpoints, secrets or Worker resources.
/// Builds the bounded route/state vocabulary without resolving endpoints, secrets or Worker resources.
#[must_use]
pub(crate) fn scaffold() -> Self {
return Self { route_ids: crate::RawIngestRouteId::all(), states: crate::RawIngestRouteState::all() };
}
}
/// Safe Config-only composability projection for one logical route.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteDto.ts")]
pub(crate) struct RawIngestRouteDto {
/// Stable route family used for presentation and bounded orchestration.
pub(crate) family: crate::RawIngestRouteFamily,
/// Stable operator-facing label with no physical resource identity.
pub(crate) label: String,
/// Logical network proven by Config when available.
pub(crate) network: std::option::Option<String>,
/// Safe reason explaining an unavailable route.
pub(crate) reason: std::option::Option<crate::RawIngestRouteUnavailableReason>,
/// Stable logical V1 route identifier.
pub(crate) route_id: crate::RawIngestRouteId,
/// Whether all Config requirements for the route are currently composable.
pub(crate) selectable: bool,
/// Config-level lifecycle projection; `Configured` is not a runtime health claim.
pub(crate) state: crate::RawIngestRouteState,
}
impl crate::RawIngestRouteDto {
/// Builds one route that is composable from Config without claiming runtime operability.
#[must_use]
pub(crate) fn configured(route_id: crate::RawIngestRouteId, network: &str) -> Self {
return Self {
family: route_id.family(),
label: route_id.label().to_owned(),
network: std::option::Option::Some(network.to_owned()),
reason: std::option::Option::None,
route_id,
selectable: true,
state: crate::RawIngestRouteState::Configured,
};
}
/// Builds one route that is not composable from the current Config profile.
#[must_use]
pub(crate) fn unavailable(route_id: crate::RawIngestRouteId, network: std::option::Option<&str>, reason: crate::RawIngestRouteUnavailableReason) -> Self {
return Self {
family: route_id.family(),
label: route_id.label().to_owned(),
network: network.map(str::to_owned),
reason: std::option::Option::Some(reason),
route_id,
selectable: false,
state: crate::RawIngestRouteState::Unavailable,
};
}
}
/// Safe route inventory for one logical Raw Transaction Ingest Desk composite profile.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestProfileInventoryDto.ts")]
pub(crate) struct RawIngestProfileInventoryDto {
/// Whether this profile is the composite's autonomous default.
pub(crate) is_default: bool,
/// Logical network proven from the resolved Config profile when available.
pub(crate) network: std::option::Option<String>,
/// Stable composite profile identifier.
pub(crate) profile_id: String,
/// Five logical routes in stable V1 presentation order.
pub(crate) routes: std::vec::Vec<crate::RawIngestRouteDto>,
}
/// Deterministic Config-only route inventory returned to the frontend.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteInventoryDto.ts")]
pub(crate) struct RawIngestRouteInventoryDto {
/// Composite default profile identifier.
pub(crate) default_profile: String,
/// Monotonic application-owned generation incremented after each successful inventory rebuild.
pub(crate) generation: u32,
/// Safe inventories for every validated composite profile in source order.
pub(crate) profiles: std::vec::Vec<crate::RawIngestProfileInventoryDto>,
}
#[cfg(test)]
#[path = "../unit_tests/dto_route.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
// version: 1
// version: 2
//! Stable Raw Transaction Ingest Desk application error codes.
@@ -20,6 +20,9 @@ pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode
/// Raw Transaction Ingest Desk could not install managed Logging or its safe fallback.
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "logging_bootstrap_failed");
/// Config-only route inventory cannot be projected safely from the validated composite.
pub(crate) const ERROR_CODE_ROUTE_INVENTORY_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_inventory_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("raw_transaction_ingest_desk", "splash_origin_invalid");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
// version: 1
// version: 2
//! Tauri desktop application scaffold for composing and supervising KSP live RAW transaction ingest routes.
@@ -15,6 +15,7 @@ mod dto_route;
mod errors;
mod frontend_logging;
mod logging_runtime;
mod route_inventory;
mod splash;
mod tauri;
mod tw_main;
@@ -33,6 +34,8 @@ pub(crate) use self::bootstrap::config_management;
pub(crate) use self::bootstrap::initialize_logging;
/// Loads the Raw Transaction Ingest Desk composite selected by its registered logical file identifier.
pub(crate) use self::bootstrap::load_raw_transaction_ingest_desk_composite;
/// Loads one explicit or default Raw Transaction Ingest Desk composite profile.
pub(crate) use self::bootstrap::load_raw_transaction_ingest_desk_composite_profile;
/// Resolves one required standard Config profile from the Raw Transaction Ingest Desk composite.
pub(crate) use self::bootstrap::required_composite_component_profile;
/// Composite-local identifier for the standard Logging component.
@@ -45,6 +48,8 @@ pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT;
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
/// Structured domain used by technical frontend events.
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
/// Structured domain used while rebuilding Config-only route inventory.
pub(crate) use self::constants::TRACING_DOMAIN_INVENTORY;
/// Structured domain used by the Raw Transaction Ingest Desk shell.
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
/// Structured domain used by Tauri window lifecycle operations.
@@ -61,12 +66,22 @@ pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
pub(crate) use self::dto_common::CommandErrorDto;
/// Safe scaffold/runtime snapshot exposed to the shell.
pub(crate) use self::dto_common::ShellStatusDto;
/// Static route/state foundation exposed before route inventory is implemented.
/// Safe route inventory for one logical composite profile.
pub(crate) use self::dto_route::RawIngestProfileInventoryDto;
/// Safe Config-only composability projection for one logical route.
pub(crate) use self::dto_route::RawIngestRouteDto;
/// Stable application-owned route family.
pub(crate) use self::dto_route::RawIngestRouteFamily;
/// Static route/state foundation retained as the bounded vocabulary behind the Config inventory.
pub(crate) use self::dto_route::RawIngestRouteFoundationDto;
/// Stable logical V1 route identifiers owned by the Desk.
pub(crate) use self::dto_route::RawIngestRouteId;
/// Stable route lifecycle states reserved for later runtime projections.
/// Complete deterministic Config-only route inventory.
pub(crate) use self::dto_route::RawIngestRouteInventoryDto;
/// Stable route lifecycle states reserved for runtime projections.
pub(crate) use self::dto_route::RawIngestRouteState;
/// Safe reason explaining why one logical route is not composable from Config.
pub(crate) use self::dto_route::RawIngestRouteUnavailableReason;
/// Shared Raw Transaction Ingest Desk application state is internally inconsistent.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Raw Transaction Ingest Desk runtime state cannot be locked safely.
@@ -79,6 +94,8 @@ pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
/// Managed and fallback Logging initialization failed.
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
/// Config-only route inventory cannot be projected safely.
pub(crate) use self::errors::ERROR_CODE_ROUTE_INVENTORY_INVALID;
/// Splash readiness originated from an invalid window.
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
/// Splash timing configuration is invalid.
@@ -95,6 +112,8 @@ pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
pub(crate) use self::frontend_logging::emit_frontend_log_event;
/// Creates the stable runtime identity for this Raw Transaction Ingest Desk process launch.
pub(crate) use self::logging_runtime::launch_identity;
/// Rebuilds the complete safe route inventory from validated Config without runtime I/O.
pub(crate) use self::route_inventory::build_route_inventory;
/// Command emitted by Rust to the splash frontend.
pub(crate) use self::splash::SplashOrderDto;
/// Runtime timings used by the common desk splash lifecycle.

View File

@@ -0,0 +1,375 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs
// version: 1
//! Config-only logical route inventory for Raw Transaction Ingest Desk.
struct CompositeProfileCatalog {
default_profile: String,
profile_ids: std::vec::Vec<String>,
}
/// Rebuilds the complete safe route inventory from validated Config without opening any network, Store or Worker resource.
pub(crate) fn build_route_inventory(management: &ksp_config_lib::ConfigManagement, generation: u32) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
let catalog = composite_profile_catalog(management);
let catalog = match catalog {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut profiles = std::vec::Vec::with_capacity(catalog.profile_ids.len());
for profile_id in &catalog.profile_ids {
profiles.push(build_profile_inventory(management, &environment, profile_id.as_str(), profile_id == &catalog.default_profile));
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_INVENTORY,
generation,
profile_count = profiles.len(),
"rebuilt Raw Transaction Ingest Desk Config-only route inventory"
);
return std::result::Result::Ok(crate::RawIngestRouteInventoryDto { default_profile: catalog.default_profile, generation, profiles });
}
fn composite_profile_catalog(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<CompositeProfileCatalog> {
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let document = management.engine().load_validated_document(&file_id);
let document = match document {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let root = document.value().as_object();
let root = match root {
std::option::Option::Some(value) => value,
std::option::Option::None => return inventory_invalid("validated Raw Transaction Ingest Desk composite root is not an object"),
};
let default_profile = root.get("default_profile").and_then(|value| return value.as_str());
let default_profile = match default_profile {
std::option::Option::Some(value) if !value.is_empty() => value.to_owned(),
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite has no readable default profile"),
};
let profile_values = root.get("profiles").and_then(|value| return value.as_array());
let profile_values = match profile_values {
std::option::Option::Some(value) if !value.is_empty() => value,
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite has no profile inventory"),
};
let mut profile_ids = std::vec::Vec::with_capacity(profile_values.len());
let mut seen = std::collections::BTreeSet::<String>::new();
for profile in profile_values {
let profile_id = profile.get("profile_id").and_then(|value| return value.as_str());
let profile_id = match profile_id {
std::option::Option::Some(value) if !value.is_empty() => value,
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite contains an unreadable profile identifier"),
};
if !seen.insert(profile_id.to_owned()) {
return inventory_invalid("validated Raw Transaction Ingest Desk composite contains duplicate profile identifiers");
}
profile_ids.push(profile_id.to_owned());
}
if !seen.contains(default_profile.as_str()) {
return inventory_invalid("validated Raw Transaction Ingest Desk composite default profile is absent from the profile inventory");
}
return std::result::Result::Ok(CompositeProfileCatalog { default_profile, profile_ids });
}
fn build_profile_inventory(
management: &ksp_config_lib::ConfigManagement,
environment: &ksp_config_lib::ConfigEnvironment,
profile_id: &str,
is_default: bool,
) -> crate::RawIngestProfileInventoryDto {
let composite = crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::Some(profile_id));
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
},
};
let transport_profile =
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
let transport_profile = match transport_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
},
};
let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
let store_profile = match store_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
},
};
let transport = management.engine().resolve_transport_config_profile(&transport_profile, environment);
let store = management.engine().resolve_store_config_profile(&store_profile, environment);
let network_hint = resolved_network_hint(&transport, &store);
if transport.is_err() || store.is_err() {
let reason = resolution_failure_reason(&transport, &store);
return unavailable_profile(profile_id, is_default, network_hint.as_deref(), reason);
}
let transport = match transport {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return unavailable_profile(profile_id, is_default, network_hint.as_deref(), crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
},
};
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return unavailable_profile(profile_id, is_default, network_hint.as_deref(), crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
},
};
let store_network = store.settings().network().as_str().to_owned();
if !transport_matches_network(&transport, store_network.as_str()) {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(store_network.as_str()),
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
);
}
let routes = build_configured_profile_routes(&transport, store_network.as_str());
return crate::RawIngestProfileInventoryDto {
is_default,
network: std::option::Option::Some(store_network),
profile_id: profile_id.to_owned(),
routes,
};
}
fn build_configured_profile_routes(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> std::vec::Vec<crate::RawIngestRouteDto> {
let has_get_transaction = http_role_supports_methods(transport.http_settings(), &["getTransaction"]);
let has_http_block_scan = http_role_supports_methods(transport.http_settings(), &["getSlot", "getBlocksWithLimit", "getBlock"]);
let has_yellowstone = has_yellowstone_grpc(transport, network);
let has_standard_logs =
has_ws_capability(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Logs);
let has_standard_block =
has_ws_capability(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Block);
let has_helius_transaction = has_ws_capability(
transport,
network,
ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream,
ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction,
);
return vec![
route_with_requirements(
crate::RawIngestRouteId::YellowstoneHydrated,
network,
&[
(has_yellowstone, crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc),
(has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
],
),
route_with_requirements(
crate::RawIngestRouteId::StandardLogsHydrated,
network,
&[
(has_standard_logs, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability),
(has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
],
),
route_with_requirements(
crate::RawIngestRouteId::StandardBlockDirect,
network,
&[(has_standard_block, crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability)],
),
route_with_requirements(
crate::RawIngestRouteId::HeliusTransactionHydrated,
network,
&[
(has_helius_transaction, crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability),
(has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
],
),
route_with_requirements(
crate::RawIngestRouteId::HttpBlockPolling,
network,
&[(has_http_block_scan, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)],
),
];
}
fn route_with_requirements(
route_id: crate::RawIngestRouteId,
network: &str,
requirements: &[(bool, crate::RawIngestRouteUnavailableReason)],
) -> crate::RawIngestRouteDto {
for (available, reason) in requirements.iter().copied() {
if !available {
return crate::RawIngestRouteDto::unavailable(route_id, std::option::Option::Some(network), reason);
}
}
return crate::RawIngestRouteDto::configured(route_id, network);
}
fn unavailable_profile(
profile_id: &str,
is_default: bool,
network: std::option::Option<&str>,
reason: crate::RawIngestRouteUnavailableReason,
) -> crate::RawIngestProfileInventoryDto {
let routes = crate::RawIngestRouteId::all()
.into_iter()
.map(|route_id| return crate::RawIngestRouteDto::unavailable(route_id, network, reason))
.collect::<std::vec::Vec<_>>();
return crate::RawIngestProfileInventoryDto { is_default, network: network.map(str::to_owned), profile_id: profile_id.to_owned(), routes };
}
fn resolution_failure_reason(
transport: &ksp_core_lib::Result<ksp_config_lib::ResolvedTransportConfig>,
store: &ksp_core_lib::Result<ksp_config_lib::ResolvedStoreConfig>,
) -> crate::RawIngestRouteUnavailableReason {
let transport_missing_secret = match transport {
std::result::Result::Err(error) => error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING,
std::result::Result::Ok(_) => false,
};
let store_missing_secret = match store {
std::result::Result::Err(error) => error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING,
std::result::Result::Ok(_) => false,
};
if transport_missing_secret || store_missing_secret {
return crate::RawIngestRouteUnavailableReason::MissingRequiredSecret;
}
return crate::RawIngestRouteUnavailableReason::ProfileUnresolved;
}
fn resolved_network_hint(
transport: &ksp_core_lib::Result<ksp_config_lib::ResolvedTransportConfig>,
store: &ksp_core_lib::Result<ksp_config_lib::ResolvedStoreConfig>,
) -> std::option::Option<String> {
if let std::result::Result::Ok(store) = store {
return std::option::Option::Some(store.settings().network().as_str().to_owned());
}
if let std::result::Result::Ok(transport) = transport {
let networks = enabled_transport_networks(transport);
if networks.len() == 1 {
return networks.into_iter().next();
}
}
return std::option::Option::None;
}
fn transport_matches_network(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
let networks = enabled_transport_networks(transport);
return networks.len() == 1 && networks.contains(network);
}
fn enabled_transport_networks(transport: &ksp_config_lib::ResolvedTransportConfig) -> std::collections::BTreeSet<String> {
let mut networks = std::collections::BTreeSet::<String>::new();
for endpoint in transport.http_settings().endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
if let std::option::Option::Some(settings) = transport.ws_settings() {
for endpoint in settings.endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
}
if let std::option::Option::Some(settings) = transport.grpc_settings() {
for endpoint in settings.endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
}
return networks;
}
fn has_ws_capability(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
protocol: ksp_onchain_transport_lib::WsProtocolKind,
capability: ksp_onchain_transport_lib::WsSubscriptionKind,
) -> bool {
let settings = match transport.ws_settings() {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
for endpoint in settings.endpoints() {
if endpoint.enabled() && endpoint.cluster().as_str() == network && endpoint.protocol() == protocol && endpoint.supports_subscription(capability) {
return true;
}
}
return false;
}
fn has_yellowstone_grpc(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
let settings = match transport.grpc_settings() {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
return settings.endpoints().iter().any(|endpoint| return endpoint.enabled() && endpoint.cluster().as_str() == network);
}
fn http_role_supports_methods(settings: &ksp_onchain_transport_lib::HttpTransportSettings, methods: &[&'static str]) -> bool {
let mut request_kinds = std::vec::Vec::<&'static str>::with_capacity(methods.len());
for method_name in methods {
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name);
let descriptor = match descriptor {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
request_kinds.push(descriptor.request_kind());
}
let mut role_names = std::collections::BTreeSet::<String>::new();
for endpoint in settings.endpoints() {
if !endpoint.enabled() {
continue;
}
for role in endpoint.roles() {
if role.enabled() {
role_names.insert(role.role().as_str().to_owned());
}
}
}
for role_name in role_names {
let mut complete = true;
for request_kind in &request_kinds {
if !http_role_supports_request_kind(settings, role_name.as_str(), request_kind) {
complete = false;
break;
}
}
if complete {
return true;
}
}
return false;
}
fn http_role_supports_request_kind(settings: &ksp_onchain_transport_lib::HttpTransportSettings, role_name: &str, request_kind: &str) -> bool {
for endpoint in settings.endpoints() {
if !endpoint.enabled() {
continue;
}
for role in endpoint.roles() {
if !role.enabled() || role.role().as_str() != role_name {
continue;
}
for capability in role.request_kinds() {
if capability.is_wildcard() || capability.as_str() == request_kind {
return true;
}
}
}
}
return false;
}
fn inventory_invalid(message: &'static str) -> ksp_core_lib::Result<CompositeProfileCatalog> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, message));
}
#[cfg(test)]
#[path = "../unit_tests/route_inventory.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
// version: 2
// version: 3
//! Tauri runtime assembly for the KSP Raw Transaction Ingest desktop application.
@@ -74,7 +74,13 @@ fn configure_packaged_runtime(context: &tauri::Context<tauri::Wry>) -> ksp_core_
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![emit_frontend_log, get_route_foundation, get_runtime_status, splash_frontend_ready]);
return builder.invoke_handler(tauri::generate_handler![
emit_frontend_log,
get_route_foundation,
get_route_inventory,
get_runtime_status,
splash_frontend_ready
]);
}
fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto {
@@ -103,6 +109,15 @@ fn get_route_foundation(state: tauri::State<'_, crate::AppState>) -> crate::RawI
return state.route_foundation();
}
#[tauri::command]
fn get_route_inventory(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::RawIngestRouteInventoryDto, crate::CommandErrorDto> {
let result = state.route_inventory();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_route_inventory", crate::TRACING_DOMAIN_INVENTORY, &error)),
};
}
#[tauri::command]
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::ShellStatusDto, crate::CommandErrorDto> {
let result = state.shell_status();

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/dependency_boundary.rs
// version: 1
// version: 2
//! Dependency-boundary canaries for the Raw Transaction Ingest Desk scaffold.
//! Dependency-boundary canaries for Raw Transaction Ingest Desk Config-only route inventory.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
@@ -21,13 +21,12 @@ fn read_text(path: &std::path::Path) -> String {
}
#[test]
fn pre_005_manifest_is_scaffold_only_and_backend_neutral() {
fn pre_006_manifest_opens_only_transport_for_typed_config_composability() {
let manifest = read_text(app_root().join("Cargo.toml").as_path());
for required in ["ksp-config-lib", "ksp-core-lib", "ksp-logging-lib", "tauri-plugin-tracing"] {
assert!(manifest.contains(required), "missing scaffold dependency {required}");
for required in ["ksp-config-lib", "ksp-core-lib", "ksp-logging-lib", "ksp-onchain-transport-lib", "tauri-plugin-tracing"] {
assert!(manifest.contains(required), "missing pre.006 dependency {required}");
}
for forbidden in [
"ksp-onchain-transport-lib",
"ksp-store-lib",
"ksp-store-api",
"ksp-store-postgres-lib",
@@ -39,12 +38,12 @@ fn pre_005_manifest_is_scaffold_only_and_backend_neutral() {
"tonic",
"yellowstone-grpc",
] {
assert!(!manifest.contains(forbidden), "scaffold opens forbidden dependency {forbidden}");
assert!(!manifest.contains(forbidden), "pre.006 opens forbidden dependency {forbidden}");
}
}
#[test]
fn pre_005_production_sources_do_not_construct_transport_store_or_worker_runtime() {
fn pre_006_production_sources_use_transport_settings_only_without_runtime_io_or_store_open() {
let root = app_root().join("src");
let entries = std::fs::read_dir(root.as_path());
assert!(entries.is_ok());
@@ -57,11 +56,13 @@ fn pre_005_production_sources_do_not_construct_transport_store_or_worker_runtime
let source = read_text(path.as_path());
for forbidden in [
"HttpTransportPool",
"WsTransportSettings",
"WsProtocolSession",
"YellowstoneGrpcChannel",
"ksp_store_lib::Store",
"Store::open",
"RawTransactionIngestWorker",
"RawTransactionIngestRuntimeResources",
"start_with_runtime_resources",
"request_stop",
] {
assert!(!source.contains(forbidden), "{} contains premature runtime marker {forbidden}", path.display());
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
// version: 2
// version: 3
//! Desktop scaffold contract canaries for Raw Transaction Ingest Desk.
@@ -108,7 +108,7 @@ fn pre_005_splash_assets_match_existing_ksp_desk_template_byte_exact() {
}
#[test]
fn pre_005_route_foundation_is_backend_owned_and_has_exact_five_ids_and_seven_states() {
fn pre_006_route_inventory_is_backend_owned_and_keeps_exact_five_ids_and_seven_states() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
for route in ["YellowstoneHydrated", "StandardLogsHydrated", "StandardBlockDirect", "HeliusTransactionHydrated", "HttpBlockPolling"] {
assert!(dto.contains(route));
@@ -118,7 +118,8 @@ fn pre_005_route_foundation_is_backend_owned_and_has_exact_five_ids_and_seven_st
}
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("get_route_foundation"));
for forbidden in ["start_worker", "stop_worker", "RawTransactionIngestWorker", "Store::open", "HttpTransportPool"] {
assert!(tauri.contains("get_route_inventory"));
for forbidden in ["start_worker", "stop_worker", "RawTransactionIngestWorker", "Store::open"] {
assert!(!tauri.contains(forbidden), "scaffold Tauri surface contains premature runtime marker {forbidden}");
}
}
@@ -153,3 +154,18 @@ fn pre_005_fix_002_shell_reuses_common_light_template_and_bounds_route_foundatio
assert!(styles.contains(required), "missing bounded layout marker: {required}");
}
}
#[test]
fn pre_006_frontend_projects_profile_network_generation_and_safe_route_reasons() {
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in ["id=\"routeProfile\"", "id=\"routeNetwork\"", "id=\"routeInventoryGeneration\"", "id=\"routeFoundation\""] {
assert!(html.contains(required), "missing route inventory UI marker: {required}");
}
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in ["get_route_inventory", "missing_ws_block_capability", "missing_required_secret", "Config route inventory"] {
assert!(main.contains(required), "missing route inventory frontend marker: {required}");
}
for forbidden in ["endpointUrl", "connectionUri", "apiKey", "sourceKey", "x-token"] {
assert!(!main.contains(forbidden), "frontend exposes forbidden physical marker: {forbidden}");
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
// version: 1
// version: 2
//! Desktop security canaries for the Raw Transaction Ingest Desk scaffold.
@@ -58,15 +58,27 @@ fn pre_005_frontend_has_no_network_persistence_or_native_dialog_surface() {
}
#[test]
fn pre_005_frontend_tracing_is_control_and_command_oriented_only() {
fn pre_006_frontend_tracing_is_control_inventory_and_command_oriented_only() {
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
assert!(main.contains("frontend control clicked"));
assert!(main.contains("navigation tab clicked"));
assert!(main.contains("runtime status refresh requested"));
assert!(main.contains("route foundation refresh requested"));
assert!(main.contains("Config route inventory refresh requested"));
assert!(main.contains("logical profile selected"));
let invoke = read_text(app_root().join("frontend/ts/invoke.ts").as_path());
assert!(invoke.contains("Frontend IPC command requested"));
for forbidden in ["apiKey", "authorization", "connectionUri", "endpointUrl", "sourceKey", "rawTransaction", "transactionPayload"] {
for forbidden in ["apiKey", "authorization", "connectionUri", "endpointUrl", "sourceKey", "rawTransaction", "transactionPayload", "x-token"] {
assert!(!main.contains(forbidden), "frontend contains forbidden sensitive marker {forbidden}");
}
}
#[test]
fn pre_006_route_inventory_dto_surface_excludes_endpoint_secret_and_physical_store_fields() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
for required in ["RawIngestRouteInventoryDto", "RawIngestProfileInventoryDto", "RawIngestRouteUnavailableReason"] {
assert!(dto.contains(required));
}
for forbidden in ["endpoint_name", "endpoint_url", "api_key", "connection_uri", "source_key", "secret_metadata", "worker_handle"] {
assert!(!dto.contains(forbidden), "route DTO contains forbidden field marker {forbidden}");
}
}

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
// version: 2
// version: 3
//! Release-completeness canaries for the Raw Transaction Ingest Desk scaffold tranche.
//! Release-completeness canaries for Raw Transaction Ingest Desk Config-only route inventory.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
@@ -21,7 +21,7 @@ fn read_text(path: &std::path::Path) -> String {
}
#[test]
fn pre_005_production_module_inventory_is_exact_and_scaffold_only() {
fn pre_006_production_module_inventory_adds_only_route_inventory_to_the_scaffold() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
let expected = [
"mod app_state;",
@@ -32,6 +32,7 @@ fn pre_005_production_module_inventory_is_exact_and_scaffold_only() {
"mod errors;",
"mod frontend_logging;",
"mod logging_runtime;",
"mod route_inventory;",
"mod splash;",
"mod tauri;",
"mod tw_main;",
@@ -44,14 +45,14 @@ fn pre_005_production_module_inventory_is_exact_and_scaffold_only() {
}
#[test]
fn pre_005_public_surface_exposes_only_application_run_entry_point() {
fn pre_006_public_surface_still_exposes_only_application_run_entry_point() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
let public_reexports = lib.lines().filter(|line| return line.starts_with("pub use ")).collect::<std::vec::Vec<_>>();
assert_eq!(public_reexports, vec!["pub use self::tauri::run;"]);
}
#[test]
fn pre_005_scaffold_keeps_start_stop_inventory_and_runtime_for_later_tranches() {
fn pre_006_config_inventory_keeps_start_stop_and_runtime_resources_for_later_tranches() {
let app = read_text(app_root().join("src/app_state.rs").as_path());
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for forbidden in [
@@ -68,3 +69,22 @@ fn pre_005_scaffold_keeps_start_stop_inventory_and_runtime_for_later_tranches()
assert!(!tauri.contains(forbidden));
}
}
#[test]
fn pre_006_inventory_source_is_config_only_and_contains_all_five_capability_requirements() {
let inventory = read_text(app_root().join("src/route_inventory.rs").as_path());
for required in [
"WsSubscriptionKind::Logs",
"WsSubscriptionKind::Block",
"WsSubscriptionKind::HeliusTransaction",
"getTransaction",
"getBlocksWithLimit",
"MissingYellowstoneGrpc",
"MissingRequiredSecret",
] {
assert!(inventory.contains(required), "missing pre.006 inventory requirement {required}");
}
for forbidden in ["connect(", "Store::open", "start_with_runtime_resources", "request_stop"] {
assert!(!inventory.contains(forbidden), "pre.006 inventory performs premature runtime operation {forbidden}");
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_route.rs
// version: 1
// version: 2
#[test]
fn pre_005_route_ids_are_exact_and_stably_ordered() {
@@ -25,3 +25,36 @@ fn pre_005_route_states_are_exact_without_runtime_claims() {
assert_eq!(value, serde_json::json!(["unavailable", "configured", "starting", "running", "stopping", "stopped", "faulted"]));
}
}
#[test]
fn pre_006_unavailable_reason_codes_are_exact_and_safe() {
let values = [
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction,
crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan,
crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability,
crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability,
crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability,
crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc,
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret,
];
let serialized = serde_json::to_value(values);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
assert_eq!(
serialized,
serde_json::json!([
"profile_unresolved",
"network_mismatch",
"missing_http_get_transaction",
"missing_http_block_scan",
"missing_ws_logs_capability",
"missing_ws_block_capability",
"missing_helius_transaction_capability",
"missing_yellowstone_grpc",
"missing_required_secret"
])
);
}
}

View File

@@ -0,0 +1,112 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_inventory.rs
// version: 1
#[test]
fn pre_006_route_requirement_projection_is_fail_closed_and_ordered() {
let route = super::route_with_requirements(
crate::RawIngestRouteId::StandardLogsHydrated,
"devnet",
&[
(false, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability),
(false, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
],
);
assert!(!route.selectable);
assert_eq!(route.state, crate::RawIngestRouteState::Unavailable);
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability));
assert_eq!(route.network.as_deref(), std::option::Option::Some("devnet"));
let configured = super::route_with_requirements(
crate::RawIngestRouteId::HttpBlockPolling,
"mainnet",
&[(true, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)],
);
assert!(configured.selectable);
assert_eq!(configured.state, crate::RawIngestRouteState::Configured);
assert_eq!(configured.reason, std::option::Option::None);
}
#[test]
fn pre_006_unavailable_profile_projects_all_five_routes_without_physical_material() {
let profile = super::unavailable_profile(
"provider_profile",
false,
std::option::Option::Some("testnet"),
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret,
);
assert_eq!(profile.routes.len(), 5);
assert_eq!(profile.network.as_deref(), std::option::Option::Some("testnet"));
for route in profile.routes {
assert!(!route.selectable);
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingRequiredSecret));
}
}
#[test]
fn pre_006_committed_devnet_public_inventory_proves_only_config_composability() {
let arguments = std::vec::Vec::<std::ffi::OsString>::new();
let management = crate::config_management(arguments.as_slice());
assert!(management.is_ok());
let management = match management {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let inventory = crate::build_route_inventory(&management, 7);
assert!(inventory.is_ok());
let inventory = match inventory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(inventory.generation, 7);
assert_eq!(inventory.default_profile, "devnet_public");
let profile = inventory.profiles.iter().find(|profile| return profile.profile_id == "devnet_public");
assert!(profile.is_some());
let profile = match profile {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert!(profile.is_default);
assert_eq!(profile.network.as_deref(), std::option::Option::Some("devnet"));
assert_eq!(profile.routes.len(), 5);
assert_route(
profile,
crate::RawIngestRouteId::YellowstoneHydrated,
false,
std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc),
);
assert_route(profile, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
assert_route(
profile,
crate::RawIngestRouteId::StandardBlockDirect,
false,
std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability),
);
assert_route(
profile,
crate::RawIngestRouteId::HeliusTransactionHydrated,
false,
std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability),
);
assert_route(profile, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
let serialized = serde_json::to_string(&inventory);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
for forbidden in ["http://", "https://", "ws://", "wss://", "api-key", "x-token", "connection_uri", "source_key", "endpoint_name"] {
assert!(!serialized.contains(forbidden), "safe inventory leaked forbidden marker {forbidden}");
}
}
}
fn assert_route(
profile: &crate::RawIngestProfileInventoryDto,
route_id: crate::RawIngestRouteId,
selectable: bool,
reason: std::option::Option<crate::RawIngestRouteUnavailableReason>,
) {
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
assert!(route.is_some());
if let std::option::Option::Some(route) = route {
assert_eq!(route.selectable, selectable);
assert_eq!(route.reason, reason);
assert_eq!(route.state, if selectable { crate::RawIngestRouteState::Configured } else { crate::RawIngestRouteState::Unavailable });
}
}

271
deltas/0.3.15/pre.006.md Normal file
View File

@@ -0,0 +1,271 @@
<!-- file: deltas/0.3.15/pre.006.md -->
<!-- version: 1 -->
# Delta `0.3.15-pre.006` — inventaire Config-only des routes Raw Transaction Ingest
## Base requise
```text
0.3.15-pre.005-fix.002
workspace.package.version = 0.3.15-pre.5.fix.2
deltas/0.3.15/pre.005.md présent
deltas/0.3.15/pre.005-fix.001.md présent
deltas/0.3.15/pre.005-fix.002.md présent
```
Le gate opérateur de `pre.005-fix.002` est PASS : audits, `cargo check`, Clippy strict, tests ciblés Desk, workspace all-targets/all-features et parcours visuel via `cargo tauri dev`. Le shell clair commun et le bornage de `routeFoundation` sont confirmés.
Conformément à `KSP-APP-025` / `KSP-APP-034`, aucun script npm applicatif n'est lancé directement par l'opérateur. Tauri possède le cycle frontend via ses hooks crate-local.
## Objectif
Remplacer la fondation statique de routes par un inventaire **Config-only** déterministe qui permet au Desk de distinguer les routes composables et non composables avant toute reconstruction de ressource Transport, ouverture Store ou création Worker.
`Configured` signifie uniquement « requirements Config satisfaites ». Il ne constitue aucune preuve de disponibilité réseau, Store ou Worker.
## Inventaire backend
Le backend :
```text
charge le composite Raw Transaction Ingest Desk validé ;
énumère ses profils dans l'ordre source ;
résout pour chaque profil les composants Transport et Store via ksp-config-lib ;
exige un unique réseau logique identique entre tous les endpoints Transport activés et Store ;
calcule les cinq routes V1 avec des capabilities typées ;
retourne une projection sûre accompagnée d'une génération monotone.
```
Le calcul ne lance aucun probe réseau et ne construit aucun client/session Transport.
## Requirements des cinq routes
### `yellowstone-hydrated`
```text
Yellowstone gRPC activé sur le réseau logique ;
HTTP getTransaction composable sur un même rôle logique.
```
### `standard-logs-hydrated`
```text
WebSocket SolanaStandard activé ;
capability explicite Logs ;
HTTP getTransaction composable sur un même rôle logique.
```
### `standard-block-direct`
```text
WebSocket SolanaStandard activé ;
capability explicite Block.
```
Aucun HTTP n'est exigé pour cette route directe.
### `helius-transaction-hydrated`
```text
WebSocket HeliusLaserStream activé ;
capability explicite HeliusTransaction ;
HTTP getTransaction composable sur un même rôle logique.
```
### `http-block-polling`
Un même rôle HTTP logique doit couvrir la composition suivante :
```text
getSlot
getBlocksWithLimit
getBlock
```
La preuve HTTP utilise directement les `HttpTransportSettings`, rôles et `request_kinds` typés. Aucun `HttpTransportPool` ni client HTTP n'est construit dans cette tranche.
## Reasons sûres
L'inventaire projette uniquement les reasons applicatives bornées suivantes :
```text
profile_unresolved
network_mismatch
missing_http_get_transaction
missing_http_block_scan
missing_ws_logs_capability
missing_ws_block_capability
missing_helius_transaction_capability
missing_yellowstone_grpc
missing_required_secret
```
Les erreurs Config arbitraires, URL, tokens, metadata, URI Store et identités physiques ne traversent pas le DTO d'inventaire.
## Projection frontend
La vue `Routes` expose uniquement :
```text
profil logique ;
profil default ;
réseau logique lorsque résolu ;
génération ;
route_id ;
famille et label sûrs ;
state ;
selectable ;
reason sûre.
```
Le sélecteur de profil ne modifie aucun backend runtime : il sélectionne uniquement la projection déjà calculée à afficher.
Un refresh reconstruit un inventaire complet depuis Config et ne publie la nouvelle génération qu'après succès.
## Frontières conservées
```text
aucun Store::open ;
aucun ksp-store-lib direct ;
aucun HttpTransportPool ;
aucune WsProtocolSession ;
aucun YellowstoneGrpcChannel ;
aucun RawTransactionIngestWorker ;
aucun RawTransactionIngestRuntimeResources ;
aucun Start/Stop Worker ;
aucune logique Backfill ;
aucune inférence de capability depuis le provider ;
aucun endpoint, URL, secret, metadata, Store URI, source key, handle ou payload RAW côté frontend.
```
La seule nouvelle dépendance KSP directe du Desk est `ksp-onchain-transport-lib`, nécessaire pour consommer les contracts typés de capabilities Transport déjà établis.
## Cas committed attendu
Sur le profil autonome `devnet_public` :
```text
yellowstone-hydrated -> Unavailable / missing_yellowstone_grpc
standard-logs-hydrated -> Configured
standard-block-direct -> Unavailable / missing_ws_block_capability
helius-transaction-hydrated -> Unavailable / missing_helius_transaction_capability
http-block-polling -> Configured
```
Ce résultat reste une preuve Config uniquement.
## Workflow Tauri
Le guide du Desk rappelle le workflow normatif :
```bash
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
```
Les scripts `npm run ...` présents dans `tauri.conf.json` sont des hooks déclenchés par Tauri et ne sont pas des commandes de gate opérateur directes. Le `cargo tauri build` final reste réservé à la validation finale et doit être la dernière opération de cette validation conformément à `KSP-APP-034`.
## Fichiers ajoutés
```text
crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs
crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_inventory.rs
deltas/0.3.15/pre.006.md
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-app-raw-transaction-ingest-desk/Cargo.toml
crates/ksp-app-raw-transaction-ingest-desk/README.md
crates/ksp-app-raw-transaction-ingest-desk/USAGE.md
crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html
crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts
crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
crates/ksp-app-raw-transaction-ingest-desk/src/bootstrap.rs
crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs
crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/dependency_boundary.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_route.rs
docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md
docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md
```
## Fichiers supprimés
```text
aucun
```
## Version Cargo
```text
header Cargo.toml : 601 -> 602
workspace.package.version : 0.3.15-pre.5.fix.2 -> 0.3.15-pre.6
```
## Validations exécutées dans l'environnement d'assemblage
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean
HTML parse: clean
contrôle des headers/version de fichiers modifiés : clean
absence de lockfiles, target et node_modules livrés : clean après purge des caches d'audit
```
## Validations non exécutées dans l'environnement d'assemblage
La toolchain Cargo/Rust n'est pas disponible dans cet environnement. Les validations suivantes restent donc autoritatives côté opérateur :
```text
cargo fmt
cargo check
cargo clippy -D warnings
tests Desk
tests workspace
parcours Tauri dev
```
Aucun `npm run build` direct ne fait partie du gate.
## Gate opérateur attendu
```bash
cargo fmt --all
cargo fmt --all -- --check
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.15
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p ksp-app-raw-transaction-ingest-desk --all-targets --all-features
cargo test --workspace --all-targets --all-features
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
```
Le lancement Tauri sert ici au parcours fonctionnel/visuel du nouvel inventaire. Il ne remplace pas les gates Rust précédents.
## Décisions prises
```text
pre.006 reste purement déclarative et Config-only ;
la reconstruction concrète des ressources Transport est réservée à pre.007 ;
Store et Worker restent réservés à pre.008 ;
HTTP block polling exige une couverture par un même rôle logique, potentiellement répartie sur plusieurs endpoints du même réseau ;
les profils nécessitant un secret non résolu sont fail-closed avec missing_required_secret ;
la sélection frontend d'un profil n'est pas une sélection de ressource runtime.
```
## Questions ouvertes
```text
aucune pour pre.006 ; les choix physiques d'endpoint et la revalidation Start restent explicitement dans pre.007.
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md -->
<!-- version: 5 -->
<!-- version: 6 -->
# Plan v0.3.15 — Raw Transaction Ingest Desk
@@ -710,14 +710,24 @@ Budget cible : une tranche Worker.
Créer `ksp-app-raw-transaction-ingest-desk` sur le gabarit KSP existant, avec shell, splash, dépendances frontend, DTO de base, IDs de routes et états, sans démarrer de Worker.
**État : implémenté côté assemblage.** La crate mixte lib/bin reprend le gabarit des Desk KSP, réserve les ports stricts `1440/1441`, embarque les 16 ressources Config du registre, expose seulement Config/Logging + shell/splash/tracing, et fixe les cinq IDs V1 ainsi que les sept états applicatifs. Le contrat `route_foundation` est statique et ne prétend aucune composabilité. Transport, Store et Worker restent absents du graphe direct de cette tranche ; aucun Start/Stop Worker n'existe encore.
**État : implémenté et gate opérateur PASS après `pre.005-fix.001` puis `pre.005-fix.002`.** La crate mixte lib/bin reprend le gabarit des Desk KSP, réserve les ports stricts `1440/1441`, embarque les 16 ressources Config du registre, expose Config/Logging + shell/splash/tracing, et fixe les cinq IDs V1 ainsi que les sept états applicatifs. Le premier fix retire un import Rust inutilisé ; le second restaure le shell clair KSP commun, borne `#routeFoundation` contre les débordements et corrige le canari Clippy `implicit_return`. Le gate opérateur final passe audits, `cargo check`, Clippy strict, tests Desk, workspace complet et parcours visuel via `cargo tauri dev`. Aucun script npm applicatif n'est exécuté directement ; Tauri possède ce cycle. Transport, Store et Worker runtime restent non construits et aucun Start/Stop Worker n'existe encore.
Budget cible : une tranche Desk fondation.
#### `pre.005-fix.001` — correction Clippy import
Correctif Rust minimal de l'import `tauri::Manager` inutilisé dans `src/tauri.rs`.
#### `pre.005-fix.002` — conformité template + overflow + Clippy test
Restauration du template Desk KSP clair, bornage de la grille de routes et correction du retour explicite exigé dans `tests/release_completeness.rs`.
### `pre.006` — inventaire de routes composables
Résoudre Config côté Rust et projeter les routes composables/non composables avec raisons sûres et génération d'inventaire, sans endpoint, URL, secret ou source key côté frontend.
**État : implémenté côté assemblage.** Le Desk ouvre uniquement la dépendance directe `ksp-onchain-transport-lib` nécessaire aux capabilities typées. Le backend énumère les profils du composite validé, résout Transport et Store via `ksp-config-lib`, exige un réseau logique unique identique au Store, puis calcule les cinq routes sans I/O réseau. HTTP est prouvé directement par rôles + `request_kinds` sur les settings typés, sans construire de pool/client ; WS repose sur `WsProtocolKind` + `WsSubscriptionKind`; Yellowstone exige un endpoint gRPC activé. L'inventaire expose profil, réseau, génération, route/state/selectable et reason codes sûrs. Il n'ouvre ni Store, ni session Transport, ni Worker et ne projette aucune ressource physique.
Budget cible : une tranche composition.
### `pre.007` — reconstruction Start des ressources Transport

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# Validation v0.3.15 — Raw Transaction Ingest Desk
@@ -356,11 +356,11 @@ Le forecast durable est porté par la section 20 du plan `036` sous forme hiéra
### `pre.005` — scaffold Desk
**Statut : réalisé côté assemblage ; gate Cargo/npm opérateur requis.** `ksp-app-raw-transaction-ingest-desk` existe comme crate Tauri mixte lib/bin sur le gabarit KSP, ports `1440/1441`, splash/assets/fonts communs, Config/Logging et tracing frontend. Le backend fixe exactement cinq route IDs (`yellowstone-hydrated`, `standard-logs-hydrated`, `standard-block-direct`, `helius-transaction-hydrated`, `http-block-polling`) et sept états (`unavailable`, `configured`, `starting`, `running`, `stopping`, `stopped`, `faulted`) dans un DTO de fondation explicitement non-composable. Le manifest ne dépend encore ni de Transport, ni Store, ni Worker ; aucune commande Start/Stop n'existe. Les audits Rust/Markdown disponibles dans le sandbox sont propres ; la parité du socle npm, les 16 ressources Config, les ports et les assets communs sont contrôlés statiquement. Cargo n'est pas disponible dans le sandbox et le build npm reste un gate opérateur.
**Statut : réalisé et gate opérateur PASS après deux fixes.** `ksp-app-raw-transaction-ingest-desk` existe comme crate Tauri mixte lib/bin, ports `1440/1441`, splash/assets/fonts communs, Config/Logging et tracing frontend. `pre.005-fix.001` retire un import Rust inutilisé détecté par Clippy. `pre.005-fix.002` restaure le template clair commun des Desk KSP, corrige le débordement de `#routeFoundation` et le retour explicite du canari release-completeness. Le gate final passe audits, `cargo check`, Clippy strict, tests Desk, `cargo test --workspace --all-targets --all-features` et `cargo tauri dev`; le parcours visuel confirme le shell corrigé. Aucun script npm applicatif n'est lancé directement : Tauri déclenche ses hooks conformément à `KSP-APP-034`.
### `pre.006` — inventaire composable
Config -> inventaire de routes composables/non composables + raisons sûres.
**Statut : réalisé côté assemblage ; gate opérateur requis.** Le backend énumère les profils du composite Config validé et reconstruit une génération complète d'inventaire sans probe réseau. Transport et Store sont résolus par `ksp-config-lib`; la cohérence impose un réseau unique identique au Store. Les capabilities HTTP sont vérifiées par rôle/méthodes sans dispatch, les capabilities WS par `WsProtocolKind` + `WsSubscriptionKind`, et Yellowstone par présence d'un endpoint gRPC activé. Les cinq routes produisent uniquement `Configured` ou `Unavailable` avec reason codes sûrs. Le frontend reçoit seulement profil logique, réseau, génération, IDs/familles/labels, state/selectable/reason ; aucun endpoint, URL, secret, metadata, Store URI, source key ou handle runtime n'est projeté. Aucun Store, session Transport ou Worker n'est démarré.
### `pre.007` — ressources Start