diff --git a/Cargo.toml b/Cargo.toml
index c2017b2..cb59462 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,12 +1,12 @@
# file: Cargo.toml
-# version: 424
+# version: 425
[workspace]
resolver = "3"
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-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-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
[workspace.package]
-version = "0.3.7-pre.6.fix.1"
+version = "0.3.7-pre.7"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
diff --git a/crates/ksp-app-backfill-desk/Cargo.toml b/crates/ksp-app-backfill-desk/Cargo.toml
index a13c2c9..56f0730 100644
--- a/crates/ksp-app-backfill-desk/Cargo.toml
+++ b/crates/ksp-app-backfill-desk/Cargo.toml
@@ -1,5 +1,5 @@
# file: crates/ksp-app-backfill-desk/Cargo.toml
-# version: 3
+# version: 4
[package]
name = "ksp-app-backfill-desk"
@@ -26,6 +26,8 @@ chrono = { workspace = true, features = ["std", "now"] }
fs2.workspace = true
ksp-config-lib = { path = "../ksp-config-lib" }
ksp-core-lib = { path = "../ksp-core-lib" }
+ksp-job-api = { path = "../ksp-job-api" }
+ksp-job-backfill-lib = { path = "../ksp-job-backfill-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
ksp-store-lib = { path = "../ksp-store-lib" }
diff --git a/crates/ksp-app-backfill-desk/frontend/main.html b/crates/ksp-app-backfill-desk/frontend/main.html
index e13b2bb..a7ccb6a 100644
--- a/crates/ksp-app-backfill-desk/frontend/main.html
+++ b/crates/ksp-app-backfill-desk/frontend/main.html
@@ -1,5 +1,5 @@
-
+
@@ -47,12 +47,12 @@
Backfill
-
Scaffold desktop prêt. Les contrôles Backfill seront branchés dans les tranches runtime suivantes.
+
Mainnet par défaut, transport HTTP sélectionnable et mapping de campagne validé côté Rust avant tout démarrage de job.
—
-
+
-
-
-
-
-
Campagne Backfill
-
Les scopes, commitments et le bouton Start seront branchés dans la tranche suivante. Le transport reste HTTP uniquement en v0.3.7.
+
+
+
+
+
+
+
+
+
Projection backend sûre
+
+ - Réseau
- —
+ - Route HTTP
- —
+ - Commitment
- —
+ - Scope
- —
+ - Signatures explicites
- —
+ - page/pages/candidates/concurrence
- —
+ - Min context slot
- —
+
+
+
diff --git a/crates/ksp-app-backfill-desk/frontend/ts/main.ts b/crates/ksp-app-backfill-desk/frontend/ts/main.ts
index a3850eb..fccb445 100644
--- a/crates/ksp-app-backfill-desk/frontend/ts/main.ts
+++ b/crates/ksp-app-backfill-desk/frontend/ts/main.ts
@@ -1,10 +1,12 @@
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
-// version: 2
+// version: 3
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
import "simplebar";
import { getCurrentWindow } from "@tauri-apps/api/window";
+import type { BackfillRequestPreviewDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts";
+import type { BackfillStartRequestDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.ts";
import type { BackfillDeskOptionsDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillDeskOptionsDto.ts";
import type { BackfillHttpRouteOptionDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillHttpRouteOptionDto.ts";
import type { ShellStatusDto } from "./bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts";
@@ -21,6 +23,19 @@ const viewTitles: Record
= {
diagnostics: "Diagnostics",
};
+const commitmentLabels: Record = {
+ confirmed: "confirmed",
+ finalized: "finalized",
+};
+
+const scopeLabels: Record = {
+ latest_address: "Adresse — historique le plus récent",
+ before_address: "Adresse — avant une signature",
+ after_address: "Adresse — après une signature",
+ explicit_signatures: "Signatures explicites",
+};
+
+
function isViewId(value: string | undefined): value is ViewId {
return value === "backfill" || value === "diagnostics";
}
@@ -79,6 +94,245 @@ function bindNavigation(): void {
frontendTrace("main", "Backfill Desk navigation handlers installed");
}
+function populateCodeSelect(selectId: string, values: string[], labels: Record): void {
+ const select = document.querySelector(`#${selectId}`);
+ if (!select) {
+ return;
+ }
+ const previous = select.value;
+ select.replaceChildren();
+ for (const value of values) {
+ const option = document.createElement("option");
+ option.value = value;
+ option.textContent = labels[value] ?? value;
+ select.append(option);
+ }
+ select.disabled = values.length === 0;
+ if (values.includes(previous)) {
+ select.value = previous;
+ }
+}
+
+function applyNumberInputContract(inputId: string, maximum: number, defaultValue: number): void {
+ const input = document.querySelector(`#${inputId}`);
+ if (!input) {
+ return;
+ }
+ input.min = "1";
+ input.max = maximum.toString();
+ if (input.value === "") {
+ input.value = defaultValue.toString();
+ }
+}
+
+function renderCampaignContract(options: BackfillDeskOptionsDto): void {
+ populateCodeSelect("backfillScopeKind", options.scopeKinds, scopeLabels);
+ populateCodeSelect("backfillCommitment", options.commitments, commitmentLabels);
+ applyNumberInputContract("backfillPageSize", options.limits.maxPageSize, options.limits.defaultPageSize);
+ applyNumberInputContract("backfillMaxPages", options.limits.maxPages, options.limits.defaultMaxPages);
+ applyNumberInputContract("backfillMaxCandidates", options.limits.maxCandidates, options.limits.defaultMaxCandidates);
+ applyNumberInputContract("backfillHydrationConcurrency", options.limits.maxHydrationConcurrency, options.limits.defaultHydrationConcurrency);
+ const signatureHelp = document.querySelector("#backfillExplicitSignaturesHelp");
+ if (signatureHelp) {
+ signatureHelp.textContent = `Une signature par ligne, ${options.limits.minSignatureTextBytes}–${options.limits.maxSignatureTextBytes} octets encodés ; maximum ${options.limits.maxCandidates}.`;
+ }
+ const validateButton = document.querySelector("#validateBackfillRequest");
+ if (validateButton) {
+ validateButton.disabled = !options.compositionReady || options.httpRoutes.length === 0;
+ }
+ updateCampaignScopeFields();
+ frontendTrace("main", "Backfill Desk campaign contract rendered", {
+ commitmentCount: options.commitments.length,
+ compositionReady: options.compositionReady,
+ scopeCount: options.scopeKinds.length,
+ });
+}
+
+function currentScopeKind(): string {
+ const select = document.querySelector("#backfillScopeKind");
+ return select?.value ?? "";
+}
+
+function updateCampaignScopeFields(): void {
+ const scopeKind = currentScopeKind();
+ const addressGroup = document.querySelector("#backfillAddressGroup");
+ const anchorGroup = document.querySelector("#backfillAnchorGroup");
+ const signaturesGroup = document.querySelector("#backfillExplicitSignaturesGroup");
+ const minContextSlotGroup = document.querySelector("#backfillMinContextSlotGroup");
+ const addressScope = scopeKind === "latest_address" || scopeKind === "before_address" || scopeKind === "after_address";
+ const anchoredScope = scopeKind === "before_address" || scopeKind === "after_address";
+ const explicitScope = scopeKind === "explicit_signatures";
+ if (addressGroup) {
+ addressGroup.hidden = !addressScope;
+ }
+ if (anchorGroup) {
+ anchorGroup.hidden = !anchoredScope;
+ }
+ if (signaturesGroup) {
+ signaturesGroup.hidden = !explicitScope;
+ }
+ if (minContextSlotGroup) {
+ minContextSlotGroup.hidden = explicitScope;
+ }
+ frontendTrace("main", "Backfill Desk campaign scope fields updated", { scopeKind: scopeKind || "none" });
+}
+
+function invalidateCampaignValidation(source: "input" | "scope" | "route"): void {
+ const status = document.querySelector("#backfillRequestValidation");
+ if (status) {
+ status.hidden = true;
+ status.textContent = "";
+ status.classList.remove("alert-success", "alert-danger");
+ status.classList.add("alert-secondary");
+ }
+ const summary = document.querySelector("#backfillRequestPreview");
+ if (summary) {
+ summary.hidden = true;
+ }
+ frontendTrace("main", "Backfill Desk campaign validation invalidated", { source });
+}
+
+function numericInputValue(inputId: string): number {
+ const input = document.querySelector(`#${inputId}`);
+ if (!input || !Number.isSafeInteger(input.valueAsNumber) || input.valueAsNumber < 0) {
+ return 0;
+ }
+ return input.valueAsNumber;
+}
+
+function rawOptionalInput(inputId: string, enabled: boolean): string | null {
+ if (!enabled) {
+ return null;
+ }
+ const input = document.querySelector(`#${inputId}`);
+ if (!input || input.value === "") {
+ return null;
+ }
+ return input.value;
+}
+
+function explicitSignatureLines(): string[] {
+ const textarea = document.querySelector("#backfillExplicitSignatures");
+ if (!textarea || currentScopeKind() !== "explicit_signatures") {
+ return [];
+ }
+ return textarea.value.split(/\r?\n/).filter(line => line.length > 0);
+}
+
+function buildBackfillStartRequest(): BackfillStartRequestDto {
+ const scopeKind = currentScopeKind();
+ const role = document.querySelector("#backfillHttpRoute")?.value ?? "";
+ const commitment = document.querySelector("#backfillCommitment")?.value ?? "";
+ const addressScope = scopeKind === "latest_address" || scopeKind === "before_address" || scopeKind === "after_address";
+ const anchoredScope = scopeKind === "before_address" || scopeKind === "after_address";
+ const minContextSlotEnabled = scopeKind !== "explicit_signatures";
+ return {
+ address: rawOptionalInput("backfillAddress", addressScope),
+ anchorSignature: rawOptionalInput("backfillAnchorSignature", anchoredScope),
+ commitment,
+ explicitSignatures: explicitSignatureLines(),
+ hydrationConcurrency: numericInputValue("backfillHydrationConcurrency"),
+ httpRole: role,
+ maxCandidates: numericInputValue("backfillMaxCandidates"),
+ maxPages: numericInputValue("backfillMaxPages"),
+ minContextSlot: rawOptionalInput("backfillMinContextSlot", minContextSlotEnabled),
+ pageSize: numericInputValue("backfillPageSize"),
+ scopeKind,
+ };
+}
+
+function renderRequestPreview(preview: BackfillRequestPreviewDto): void {
+ const values: Record = {
+ validatedNetwork: preview.network,
+ validatedHttpRole: preview.httpRole,
+ validatedCommitment: preview.commitment,
+ validatedScopeKind: preview.scopeKind,
+ validatedExplicitSignatureCount: preview.explicitSignatureCount.toString(),
+ validatedBounds: `${preview.pageSize} / ${preview.maxPages} / ${preview.maxCandidates} / ${preview.hydrationConcurrency}`,
+ validatedMinContextSlot: preview.minContextSlotPresent ? "présent" : "absent",
+ };
+ for (const [id, value] of Object.entries(values)) {
+ const element = document.querySelector(`#${id}`);
+ if (element) {
+ element.textContent = value;
+ }
+ }
+ const summary = document.querySelector("#backfillRequestPreview");
+ if (summary) {
+ summary.hidden = false;
+ }
+ const status = document.querySelector("#backfillRequestValidation");
+ if (status) {
+ status.hidden = false;
+ status.classList.remove("alert-secondary", "alert-danger");
+ status.classList.add("alert-success");
+ status.textContent = "Requête valide. Aucun job n'a été démarré.";
+ }
+ frontendTrace("main", "Backfill Desk validated request preview rendered", {
+ commitment: preview.commitment,
+ httpRole: preview.httpRole,
+ scopeKind: preview.scopeKind,
+ });
+}
+
+function renderRequestValidationError(caughtError: unknown): void {
+ const status = document.querySelector("#backfillRequestValidation");
+ if (!status) {
+ return;
+ }
+ let message = "Requête refusée par le backend.";
+ if (typeof caughtError === "object" && caughtError !== null) {
+ const candidate = caughtError as { code?: unknown; domain?: unknown; message?: unknown };
+ if (typeof candidate.domain === "string" && typeof candidate.code === "string" && typeof candidate.message === "string") {
+ message = `${candidate.domain}/${candidate.code}: ${candidate.message}`;
+ }
+ }
+ status.hidden = false;
+ status.classList.remove("alert-secondary", "alert-success");
+ status.classList.add("alert-danger");
+ status.textContent = message;
+}
+
+async function validateBackfillRequest(): Promise {
+ const request = buildBackfillStartRequest();
+ frontendDebug("main", "Backfill Desk campaign validation requested", {
+ commitment: request.commitment,
+ httpRole: request.httpRole,
+ scopeKind: request.scopeKind,
+ });
+ try {
+ const preview = await invokeKsp("main", "backfill_validate_request", { request });
+ renderRequestPreview(preview);
+ frontendDebug("main", "Backfill Desk campaign validation completed", {
+ commitment: preview.commitment,
+ httpRole: preview.httpRole,
+ scopeKind: preview.scopeKind,
+ });
+ } catch (caughtError) {
+ renderRequestValidationError(caughtError);
+ frontendWarn("main", "Backfill Desk campaign validation failed");
+ }
+}
+
+function bindCampaignForm(): void {
+ const scope = document.querySelector("#backfillScopeKind");
+ if (scope) {
+ scope.addEventListener("change", () => {
+ frontendDebug("main", "Backfill Desk campaign scope changed", { scopeKind: scope.value || "none" });
+ updateCampaignScopeFields();
+ invalidateCampaignValidation("scope");
+ });
+ }
+ const form = document.querySelector("#backfillCampaignForm");
+ if (form) {
+ form.addEventListener("input", () => invalidateCampaignValidation("input"));
+ form.addEventListener("submit", event => {
+ event.preventDefault();
+ void validateBackfillRequest();
+ });
+ }
+ frontendTrace("main", "Backfill Desk campaign form handlers installed");
+}
function routeLabel(route: BackfillHttpRouteOptionDto): string {
if (route.pooled) {
@@ -88,6 +342,7 @@ function routeLabel(route: BackfillHttpRouteOptionDto): string {
}
function renderBackfillOptions(options: BackfillDeskOptionsDto): void {
+ renderCampaignContract(options);
const select = document.querySelector("#backfillHttpRoute");
if (select) {
const previous = select.value;
@@ -143,6 +398,7 @@ function bindBackfillRouteSelection(): void {
}
select.addEventListener("change", () => {
frontendDebug("main", "Backfill Desk HTTP route selection changed", { role: select.value || "none" });
+ invalidateCampaignValidation("route");
});
frontendTrace("main", "Backfill Desk HTTP route selection handler installed");
}
@@ -202,6 +458,7 @@ async function initializeMain(): Promise {
bindNavigation();
bindRuntimeStatusRefresh();
bindBackfillRouteSelection();
+ bindCampaignForm();
activateView("backfill", "startup");
try {
await Promise.all([loadRuntimeStatus("startup"), loadBackfillOptions("startup")]);
diff --git a/crates/ksp-app-backfill-desk/src/app_state.rs b/crates/ksp-app-backfill-desk/src/app_state.rs
index 4d356a7..ee1d589 100644
--- a/crates/ksp-app-backfill-desk/src/app_state.rs
+++ b/crates/ksp-app-backfill-desk/src/app_state.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/app_state.rs
-// version: 4
+// version: 5
//! Shared backend state owned by the Backfill Desk Tauri application.
@@ -121,7 +121,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.006-mainnet-http-routing".to_owned(),
+ shell_phase: "pre.007-request-mapping".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
});
}
@@ -131,17 +131,27 @@ impl crate::AppState {
let runtime = self.transport_runtime.as_ref();
let options = match runtime {
std::option::Option::Some(value) => value.options(),
- std::option::Option::None => std::result::Result::Ok(crate::BackfillDeskOptionsDto {
- http_routes: std::vec::Vec::new(),
- composition_ready: false,
- configured_networks: std::vec::Vec::new(),
- network_coherent: false,
- store_diagnostic: std::option::Option::None,
- store_network: std::option::Option::None,
- store_ready: false,
- transport_diagnostic: self.transport_startup_diagnostic.clone(),
- transport_ready: false,
- }),
+ std::option::Option::None => {
+ let limits = crate::backfill_request_limits();
+ let limits = match limits {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ std::result::Result::Ok(crate::BackfillDeskOptionsDto {
+ commitments: crate::backfill_commitment_codes(),
+ composition_ready: false,
+ configured_networks: std::vec::Vec::new(),
+ http_routes: std::vec::Vec::new(),
+ limits,
+ network_coherent: false,
+ scope_kinds: crate::backfill_scope_kind_codes(),
+ store_diagnostic: std::option::Option::None,
+ store_network: std::option::Option::None,
+ store_ready: false,
+ transport_diagnostic: self.transport_startup_diagnostic.clone(),
+ transport_ready: false,
+ })
+ },
};
let mut options = match options {
std::result::Result::Ok(value) => value,
@@ -151,6 +161,39 @@ impl crate::AppState {
return std::result::Result::Ok(options);
}
+ /// Validates and maps one frontend campaign request without starting a Backfill Job.
+ pub(crate) fn validate_backfill_request(&self, request: crate::BackfillStartRequestDto) -> ksp_core_lib::Result {
+ let options = self.backfill_options();
+ let options = match options {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let job_id = ksp_job_api::JobId::new("backfill-desk-validation");
+ let job_id = match job_id {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let mapped = crate::map_backfill_request(request, &options, job_id);
+ let mapped = match mapped {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let preview = crate::project_backfill_request(&mapped);
+ let preview = match preview {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ ksp_logging_lib::debug!(
+ target: crate::TRACING_TARGET,
+ domain = crate::TRACING_DOMAIN_REQUEST,
+ commitment = preview.commitment.as_str(),
+ http_role = preview.http_role.as_str(),
+ scope_kind = preview.scope_kind.as_str(),
+ "validated Backfill Desk campaign request without starting a Job"
+ );
+ return std::result::Result::Ok(preview);
+ }
+
/// Marks graceful application shutdown as started and reports whether this caller won the one-shot transition.
pub(crate) fn begin_shutdown(&self) -> bool {
return self.shutdown_started.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire).is_ok();
diff --git a/crates/ksp-app-backfill-desk/src/backfill_request.rs b/crates/ksp-app-backfill-desk/src/backfill_request.rs
new file mode 100644
index 0000000..c8023b8
--- /dev/null
+++ b/crates/ksp-app-backfill-desk/src/backfill_request.rs
@@ -0,0 +1,313 @@
+// file: crates/ksp-app-backfill-desk/src/backfill_request.rs
+// version: 1
+
+//! Strict application mapping from frontend campaign DTOs to the KSP Backfill request contract.
+
+/// Maps one app-owned campaign request to a validated KSP Backfill request without starting a Job.
+pub(crate) fn map_backfill_request(
+ input: crate::BackfillStartRequestDto,
+ options: &crate::BackfillDeskOptionsDto,
+ job_id: ksp_job_api::JobId,
+) -> ksp_core_lib::Result {
+ let composition = validate_composition(options);
+ if let std::result::Result::Err(error) = composition {
+ return std::result::Result::Err(error);
+ }
+ let network = request_network(options);
+ let network = match network {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let role = request_role(input.http_role.as_str(), options);
+ let role = match role {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let commitment = request_commitment(input.commitment.as_str());
+ let commitment = match commitment {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let min_context_slot = parse_min_context_slot(input.min_context_slot.as_deref());
+ let min_context_slot = match min_context_slot {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let scope = request_scope(&input);
+ let scope = match scope {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let page_size = usize::try_from(input.page_size);
+ let page_size = match page_size {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("page_size", error)),
+ };
+ let max_pages = usize::try_from(input.max_pages);
+ let max_pages = match max_pages {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_pages", error)),
+ };
+ let max_candidates = usize::try_from(input.max_candidates);
+ let max_candidates = match max_candidates {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_candidates", error)),
+ };
+ let hydration_concurrency = usize::try_from(input.hydration_concurrency);
+ let hydration_concurrency = match hydration_concurrency {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("hydration_concurrency", error)),
+ };
+ return ksp_job_backfill_lib::BackfillRequest::new(
+ job_id,
+ network,
+ role,
+ commitment,
+ scope,
+ page_size,
+ max_pages,
+ max_candidates,
+ hydration_concurrency,
+ min_context_slot,
+ );
+}
+
+/// Builds a safe request preview from one already validated KSP Backfill request.
+pub(crate) fn project_backfill_request(request: &ksp_job_backfill_lib::BackfillRequest) -> ksp_core_lib::Result {
+ let explicit_signature_count = match request.scope().signatures() {
+ std::option::Option::Some(signatures) => u32::try_from(signatures.len()),
+ std::option::Option::None => std::result::Result::Ok(0),
+ };
+ let explicit_signature_count = match explicit_signature_count {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("explicit_signature_count", error)),
+ };
+ let hydration_concurrency = u32::try_from(request.hydration_concurrency());
+ let hydration_concurrency = match hydration_concurrency {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("hydration_concurrency", error)),
+ };
+ let max_candidates = u32::try_from(request.max_candidates());
+ let max_candidates = match max_candidates {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_candidates", error)),
+ };
+ let max_pages = u32::try_from(request.max_pages());
+ let max_pages = match max_pages {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_pages", error)),
+ };
+ let page_size = u32::try_from(request.page_size());
+ let page_size = match page_size {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("page_size", error)),
+ };
+ return std::result::Result::Ok(crate::BackfillRequestPreviewDto {
+ address_present: request.scope().address().is_some(),
+ anchor_present: request.scope().anchor().is_some(),
+ commitment: request.commitment().code().to_owned(),
+ explicit_signature_count,
+ hydration_concurrency,
+ http_role: request.role().as_str().to_owned(),
+ max_candidates,
+ max_pages,
+ min_context_slot_present: request.min_context_slot().is_some(),
+ network: request.network().as_str().to_owned(),
+ page_size,
+ scope_kind: request.scope().kind().code().to_owned(),
+ });
+}
+
+fn parse_min_context_slot(value: std::option::Option<&str>) -> ksp_core_lib::Result> {
+ let value = match value {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
+ };
+ if value.is_empty() || value.trim() != value {
+ return std::result::Result::Err(request_field_error("min_context_slot"));
+ }
+ let parsed = value.parse::();
+ return match parsed {
+ std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
+ std::result::Result::Err(error) => std::result::Result::Err(request_field_error("min_context_slot").with_source(error)),
+ };
+}
+
+fn parse_pubkey(value: &str, field: &'static str) -> ksp_core_lib::Result {
+ if value.is_empty() || value.trim() != value {
+ return std::result::Result::Err(request_field_error(field));
+ }
+ let parsed = value.parse::();
+ return match parsed {
+ std::result::Result::Ok(value) => std::result::Result::Ok(value),
+ std::result::Result::Err(error) => std::result::Result::Err(request_field_error(field).with_source(error)),
+ };
+}
+
+fn parse_signature(value: &str, field: &'static str) -> ksp_core_lib::Result {
+ if value.is_empty() || value.trim() != value {
+ return std::result::Result::Err(request_field_error(field));
+ }
+ let parsed = ksp_job_backfill_lib::BackfillSignature::new(value.to_owned());
+ return match parsed {
+ std::result::Result::Ok(value) => std::result::Result::Ok(value),
+ std::result::Result::Err(error) => std::result::Result::Err(request_field_error(field).with_source(error)),
+ };
+}
+
+fn request_commitment(value: &str) -> ksp_core_lib::Result {
+ return match value {
+ "confirmed" => std::result::Result::Ok(ksp_job_backfill_lib::BackfillCommitment::Confirmed),
+ "finalized" => std::result::Result::Ok(ksp_job_backfill_lib::BackfillCommitment::Finalized),
+ _ => std::result::Result::Err(request_field_error("commitment")),
+ };
+}
+
+fn request_conversion_error(field: &'static str, source: impl std::error::Error + Send + Sync + 'static) -> ksp_core_lib::Error {
+ return request_field_error(field).with_source(source);
+}
+
+fn request_field_error(field: &'static str) -> ksp_core_lib::Error {
+ return ksp_core_lib::Error::new(crate::ERROR_CODE_BACKFILL_REQUEST_INVALID, "Backfill Desk campaign request is invalid").with_context("field", field);
+}
+
+fn request_network(options: &crate::BackfillDeskOptionsDto) -> ksp_core_lib::Result {
+ let network = options.store_network.as_deref();
+ let network = match network {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return std::result::Result::Err(request_field_error("network")),
+ };
+ let parsed = ksp_store_lib::RawNetworkId::new(network.to_owned());
+ return match parsed {
+ std::result::Result::Ok(value) => std::result::Result::Ok(value),
+ std::result::Result::Err(error) => std::result::Result::Err(request_field_error("network").with_source(error)),
+ };
+}
+
+fn request_role(value: &str, options: &crate::BackfillDeskOptionsDto) -> ksp_core_lib::Result {
+ let admitted = options.http_routes.iter().any(|route| return route.role == value);
+ if !admitted || value.is_empty() || value.trim() != value {
+ return std::result::Result::Err(request_field_error("http_role"));
+ }
+ return std::result::Result::Ok(ksp_onchain_transport_lib::HttpRoleName::new(value.to_owned()));
+}
+
+fn request_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result {
+ return match input.scope_kind.as_str() {
+ "latest_address" => latest_address_scope(input),
+ "before_address" => before_address_scope(input),
+ "after_address" => after_address_scope(input),
+ "explicit_signatures" => explicit_signatures_scope(input),
+ _ => std::result::Result::Err(request_field_error("scope_kind")),
+ };
+}
+
+fn latest_address_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result {
+ let shape = require_no_anchor_or_signatures(input);
+ if let std::result::Result::Err(error) = shape {
+ return std::result::Result::Err(error);
+ }
+ let address = required_address(input.address.as_deref());
+ let address = match address {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ return std::result::Result::Ok(ksp_job_backfill_lib::BackfillScope::latest_address(address));
+}
+
+fn before_address_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result {
+ let shape = require_no_explicit_signatures(input);
+ if let std::result::Result::Err(error) = shape {
+ return std::result::Result::Err(error);
+ }
+ let address = required_address(input.address.as_deref());
+ let address = match address {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let anchor = required_anchor(input.anchor_signature.as_deref());
+ let anchor = match anchor {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ return std::result::Result::Ok(ksp_job_backfill_lib::BackfillScope::before_address(address, anchor));
+}
+
+fn after_address_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result {
+ let shape = require_no_explicit_signatures(input);
+ if let std::result::Result::Err(error) = shape {
+ return std::result::Result::Err(error);
+ }
+ let address = required_address(input.address.as_deref());
+ let address = match address {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let anchor = required_anchor(input.anchor_signature.as_deref());
+ let anchor = match anchor {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ return std::result::Result::Ok(ksp_job_backfill_lib::BackfillScope::after_address(address, anchor));
+}
+
+fn explicit_signatures_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result {
+ if input.address.is_some() || input.anchor_signature.is_some() || input.min_context_slot.is_some() {
+ return std::result::Result::Err(request_field_error("scope"));
+ }
+ if input.explicit_signatures.is_empty() || input.explicit_signatures.len() > ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES {
+ return std::result::Result::Err(request_field_error("explicit_signatures"));
+ }
+ let mut signatures = std::vec::Vec::with_capacity(input.explicit_signatures.len());
+ for value in &input.explicit_signatures {
+ let signature = parse_signature(value.as_str(), "explicit_signatures");
+ let signature = match signature {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ signatures.push(signature);
+ }
+ return ksp_job_backfill_lib::BackfillScope::explicit_signatures(signatures);
+}
+
+fn require_no_anchor_or_signatures(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<()> {
+ if input.anchor_signature.is_some() || !input.explicit_signatures.is_empty() {
+ return std::result::Result::Err(request_field_error("scope"));
+ }
+ return std::result::Result::Ok(());
+}
+
+fn require_no_explicit_signatures(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<()> {
+ if !input.explicit_signatures.is_empty() {
+ return std::result::Result::Err(request_field_error("explicit_signatures"));
+ }
+ return std::result::Result::Ok(());
+}
+
+fn required_address(value: std::option::Option<&str>) -> ksp_core_lib::Result {
+ return match value {
+ std::option::Option::Some(value) => parse_pubkey(value, "address"),
+ std::option::Option::None => std::result::Result::Err(request_field_error("address")),
+ };
+}
+
+fn required_anchor(value: std::option::Option<&str>) -> ksp_core_lib::Result {
+ return match value {
+ std::option::Option::Some(value) => parse_signature(value, "anchor_signature"),
+ std::option::Option::None => std::result::Result::Err(request_field_error("anchor_signature")),
+ };
+}
+
+fn validate_composition(options: &crate::BackfillDeskOptionsDto) -> ksp_core_lib::Result<()> {
+ if options.composition_ready && options.transport_ready && options.store_ready && options.network_coherent {
+ return std::result::Result::Ok(());
+ }
+ return std::result::Result::Err(ksp_core_lib::Error::new(
+ crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
+ "Backfill Desk cannot admit a campaign before Transport and Store composition is ready",
+ ));
+}
+
+#[cfg(test)]
+#[path = "../unit_tests/backfill_request.rs"]
+mod tests;
diff --git a/crates/ksp-app-backfill-desk/src/constants.rs b/crates/ksp-app-backfill-desk/src/constants.rs
index 8f9f9a5..c22aa1c 100644
--- a/crates/ksp-app-backfill-desk/src/constants.rs
+++ b/crates/ksp-app-backfill-desk/src/constants.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/constants.rs
-// version: 4
+// version: 5
//! Application-owned tracing targets and domains.
@@ -9,10 +9,20 @@ pub(crate) const COMPOSITE_COMPONENT_ID_LOGGING: &str = "logging";
pub(crate) const COMPOSITE_COMPONENT_ID_STORE: &str = "store";
/// Composite component identifier for Transport.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT: &str = "transport";
+/// Initial hydration concurrency proposed by the Desk for a new campaign.
+pub(crate) const DEFAULT_BACKFILL_HYDRATION_CONCURRENCY: u32 = 4;
+/// Initial candidate cap proposed by the Desk for a new campaign.
+pub(crate) const DEFAULT_BACKFILL_MAX_CANDIDATES: u32 = 1_000;
+/// Initial discovery-page cap proposed by the Desk for a new campaign.
+pub(crate) const DEFAULT_BACKFILL_MAX_PAGES: u32 = 10;
+/// Initial signature page size proposed by the Desk for a new campaign.
+pub(crate) const DEFAULT_BACKFILL_PAGE_SIZE: u32 = 100;
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "backfill.bootstrap";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
+/// Structured domain used by Backfill campaign admission and request mapping.
+pub(crate) const TRACING_DOMAIN_REQUEST: &str = "backfill.request";
/// Structured domain used by the Backfill Desk shell.
pub(crate) const TRACING_DOMAIN_SHELL: &str = "backfill.shell";
/// Structured domain used by Store readiness and shutdown operations.
diff --git a/crates/ksp-app-backfill-desk/src/dto_backfill.rs b/crates/ksp-app-backfill-desk/src/dto_backfill.rs
new file mode 100644
index 0000000..86d4729
--- /dev/null
+++ b/crates/ksp-app-backfill-desk/src/dto_backfill.rs
@@ -0,0 +1,196 @@
+// file: crates/ksp-app-backfill-desk/src/dto_backfill.rs
+// version: 1
+
+//! Application-owned Backfill campaign DTOs and backend-derived request limits.
+
+use ts_rs::TS; // rust-rules: trait-import
+
+/// Backend-derived limits and application defaults rendered by the Backfill campaign form.
+#[derive(Clone, Debug, serde::Serialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestLimitsDto.ts")]
+pub(crate) struct BackfillRequestLimitsDto {
+ /// Initial hydration concurrency proposed by this Desk while remaining below the Job-owned maximum.
+ pub(crate) default_hydration_concurrency: u32,
+ /// Initial candidate cap proposed by this Desk while remaining below the Job-owned maximum.
+ pub(crate) default_max_candidates: u32,
+ /// Initial discovery-page cap proposed by this Desk while remaining below the Job-owned maximum.
+ pub(crate) default_max_pages: u32,
+ /// Initial signature page size proposed by this Desk while remaining below the Job-owned maximum.
+ pub(crate) default_page_size: u32,
+ /// Maximum hydration concurrency owned by `ksp-job-backfill-lib`.
+ pub(crate) max_hydration_concurrency: u32,
+ /// Maximum candidate count owned by `ksp-job-backfill-lib`.
+ pub(crate) max_candidates: u32,
+ /// Maximum discovery-page count owned by `ksp-job-backfill-lib`.
+ pub(crate) max_pages: u32,
+ /// Maximum signature page size owned by `ksp-job-backfill-lib`.
+ pub(crate) max_page_size: u32,
+ /// Maximum encoded transaction-signature text length owned by `ksp-job-backfill-lib`.
+ pub(crate) max_signature_text_bytes: u32,
+ /// Minimum encoded transaction-signature text length owned by `ksp-job-backfill-lib`.
+ pub(crate) min_signature_text_bytes: u32,
+}
+
+/// App-owned request received from the Backfill campaign form before a Job is started.
+///
+/// Network and physical endpoint information are intentionally absent. The backend derives the
+/// network from the configured Store and maps `http_role` only after checking the current safe
+/// Transport inventory.
+#[derive(serde::Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.ts")]
+pub(crate) struct BackfillStartRequestDto {
+ /// Optional address text used only by address-based scopes.
+ pub(crate) address: std::option::Option,
+ /// Optional exclusive anchor signature used only by Before/After scopes.
+ pub(crate) anchor_signature: std::option::Option,
+ /// Stable Backfill commitment code.
+ pub(crate) commitment: String,
+ /// Explicit signature texts used only by the explicit-signatures scope.
+ pub(crate) explicit_signatures: std::vec::Vec,
+ /// Maximum number of concurrent candidate hydrations.
+ pub(crate) hydration_concurrency: u32,
+ /// Logical HTTP role selected from `BackfillDeskOptionsDto::http_routes`.
+ pub(crate) http_role: String,
+ /// Maximum number of candidates admitted by the campaign.
+ pub(crate) max_candidates: u32,
+ /// Maximum number of address-discovery pages admitted by the campaign.
+ pub(crate) max_pages: u32,
+ /// Optional minimum context slot encoded as decimal text to avoid JavaScript integer precision loss.
+ pub(crate) min_context_slot: std::option::Option,
+ /// Signature page size used by address discovery.
+ pub(crate) page_size: u32,
+ /// Stable Backfill scope kind code.
+ pub(crate) scope_kind: String,
+}
+
+/// Safe projection proving that one app request mapped to the KSP Backfill contract.
+///
+/// Address and signature values are intentionally reduced to presence/count metadata. This DTO is
+/// suitable for request validation feedback before the runtime Start slice exists.
+#[derive(Clone, Debug, serde::Serialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts")]
+pub(crate) struct BackfillRequestPreviewDto {
+ /// Whether an address participates in the validated scope.
+ pub(crate) address_present: bool,
+ /// Whether an exclusive anchor participates in the validated scope.
+ pub(crate) anchor_present: bool,
+ /// Stable validated commitment code.
+ pub(crate) commitment: String,
+ /// Number of explicit signatures after Backfill-owned stable deduplication.
+ pub(crate) explicit_signature_count: u32,
+ /// Validated hydration concurrency.
+ pub(crate) hydration_concurrency: u32,
+ /// Validated logical HTTP role.
+ pub(crate) http_role: String,
+ /// Validated maximum candidate count.
+ pub(crate) max_candidates: u32,
+ /// Validated maximum discovery-page count.
+ pub(crate) max_pages: u32,
+ /// Whether a minimum context slot participates in the validated request.
+ pub(crate) min_context_slot_present: bool,
+ /// Backend-derived logical Store network.
+ pub(crate) network: String,
+ /// Validated signature page size.
+ pub(crate) page_size: u32,
+ /// Stable validated scope kind code.
+ pub(crate) scope_kind: String,
+}
+
+/// Returns the exact commitment codes currently admitted by the Backfill runtime.
+#[must_use]
+pub(crate) fn backfill_commitment_codes() -> std::vec::Vec {
+ return vec![ksp_job_backfill_lib::BackfillCommitment::Finalized.code().to_owned(), ksp_job_backfill_lib::BackfillCommitment::Confirmed.code().to_owned()];
+}
+
+/// Returns the exact HTTP scope codes currently admitted by the Backfill runtime.
+#[must_use]
+pub(crate) fn backfill_scope_kind_codes() -> std::vec::Vec {
+ return vec![
+ ksp_job_backfill_lib::BackfillScopeKind::LatestAddress.code().to_owned(),
+ ksp_job_backfill_lib::BackfillScopeKind::BeforeAddress.code().to_owned(),
+ ksp_job_backfill_lib::BackfillScopeKind::AfterAddress.code().to_owned(),
+ ksp_job_backfill_lib::BackfillScopeKind::ExplicitSignatures.code().to_owned(),
+ ];
+}
+
+/// Builds frontend-safe request limits directly from the public Backfill constants.
+pub(crate) fn backfill_request_limits() -> ksp_core_lib::Result {
+ let max_hydration_concurrency = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_HYDRATION_CONCURRENCY, "max_hydration_concurrency");
+ let max_hydration_concurrency = match max_hydration_concurrency {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let max_candidates = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES, "max_candidates");
+ let max_candidates = match max_candidates {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let max_pages = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_PAGES, "max_pages");
+ let max_pages = match max_pages {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let max_page_size = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_PAGE_SIZE, "max_page_size");
+ let max_page_size = match max_page_size {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let max_signature_text_bytes = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_SIGNATURE_TEXT_BYTES, "max_signature_text_bytes");
+ let max_signature_text_bytes = match max_signature_text_bytes {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let min_signature_text_bytes = usize_to_u32(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES, "min_signature_text_bytes");
+ let min_signature_text_bytes = match min_signature_text_bytes {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ if crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY == 0 || crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY > max_hydration_concurrency {
+ return std::result::Result::Err(limit_contract_error("default_hydration_concurrency"));
+ }
+ if crate::DEFAULT_BACKFILL_MAX_CANDIDATES == 0 || crate::DEFAULT_BACKFILL_MAX_CANDIDATES > max_candidates {
+ return std::result::Result::Err(limit_contract_error("default_max_candidates"));
+ }
+ if crate::DEFAULT_BACKFILL_MAX_PAGES == 0 || crate::DEFAULT_BACKFILL_MAX_PAGES > max_pages {
+ return std::result::Result::Err(limit_contract_error("default_max_pages"));
+ }
+ if crate::DEFAULT_BACKFILL_PAGE_SIZE == 0 || crate::DEFAULT_BACKFILL_PAGE_SIZE > max_page_size {
+ return std::result::Result::Err(limit_contract_error("default_page_size"));
+ }
+ return std::result::Result::Ok(BackfillRequestLimitsDto {
+ default_hydration_concurrency: crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY,
+ default_max_candidates: crate::DEFAULT_BACKFILL_MAX_CANDIDATES,
+ default_max_pages: crate::DEFAULT_BACKFILL_MAX_PAGES,
+ default_page_size: crate::DEFAULT_BACKFILL_PAGE_SIZE,
+ max_hydration_concurrency,
+ max_candidates,
+ max_pages,
+ max_page_size,
+ max_signature_text_bytes,
+ min_signature_text_bytes,
+ });
+}
+
+fn limit_contract_error(field: &'static str) -> ksp_core_lib::Error {
+ return ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Backfill Desk request-limit projection is internally inconsistent")
+ .with_context("field", field);
+}
+
+fn usize_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result {
+ let converted = u32::try_from(value);
+ return match converted {
+ std::result::Result::Ok(value) => std::result::Result::Ok(value),
+ std::result::Result::Err(error) => std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Backfill Desk cannot project a Job-owned bound to the frontend")
+ .with_context("field", field)
+ .with_source(error),
+ ),
+ };
+}
+
+#[cfg(test)]
+#[path = "../unit_tests/dto_backfill.rs"]
+mod tests;
diff --git a/crates/ksp-app-backfill-desk/src/dto_common.rs b/crates/ksp-app-backfill-desk/src/dto_common.rs
index 03d4b47..980d2b9 100644
--- a/crates/ksp-app-backfill-desk/src/dto_common.rs
+++ b/crates/ksp-app-backfill-desk/src/dto_common.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/dto_common.rs
-// version: 4
+// version: 5
//! Common Tauri DTOs shared by the Backfill Desk shell.
@@ -21,15 +21,19 @@ pub(crate) struct BackfillHttpRouteOptionDto {
pub(crate) role: String,
}
-/// Safe Transport-and-Store readiness subset of the Backfill Desk options contract.
+/// Safe Backfill Desk options contract combining readiness, HTTP routes and Job-owned campaign bounds.
///
-/// Campaign scopes, commitments and backend-owned bounds are added in the dedicated request/DTO slice. Endpoint URLs and credentials are intentionally absent.
+/// Endpoint URLs, credentials and physical routing details are intentionally absent.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/BackfillDeskOptionsDto.ts")]
pub(crate) struct BackfillDeskOptionsDto {
+ /// Stable commitment codes currently admitted by `ksp-job-backfill-lib`.
+ pub(crate) commitments: std::vec::Vec,
/// Selectable logical HTTP routes that support both RPC methods required by the Backfill runtime.
pub(crate) http_routes: std::vec::Vec,
+ /// Backend-owned maxima plus application defaults used by the campaign form.
+ pub(crate) limits: crate::BackfillRequestLimitsDto,
/// Whether the Transport and Store readiness gates jointly permit later Backfill composition.
pub(crate) composition_ready: bool,
/// Distinct enabled HTTP cluster labels selected by the active Transport profile.
@@ -44,6 +48,8 @@ pub(crate) struct BackfillDeskOptionsDto {
pub(crate) store_ready: bool,
/// Safe startup diagnostic when Transport configuration could not be resolved or constructed.
pub(crate) transport_diagnostic: std::option::Option,
+ /// Stable HTTP scope codes currently admitted by `ksp-job-backfill-lib`.
+ pub(crate) scope_kinds: std::vec::Vec,
/// Whether Transport currently has one coherent network and at least one compatible available role.
pub(crate) transport_ready: bool,
}
diff --git a/crates/ksp-app-backfill-desk/src/errors.rs b/crates/ksp-app-backfill-desk/src/errors.rs
index d386eff..71f7c08 100644
--- a/crates/ksp-app-backfill-desk/src/errors.rs
+++ b/crates/ksp-app-backfill-desk/src/errors.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/errors.rs
-// version: 5
+// version: 6
//! Application-local error codes for Backfill Desk composition and desktop runtime surfaces.
@@ -7,6 +7,11 @@
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "app_state_invalid");
/// Shared Backfill 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("backfill_desk", "app_state_lock_failed");
+/// Backfill Desk cannot admit a campaign before Transport and Store composition is ready.
+pub(crate) const ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY: ksp_core_lib::ErrorCode =
+ ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_composition_not_ready");
+/// Backfill Desk received a campaign field or logical route that cannot map to the Backfill contract.
+pub(crate) const ERROR_CODE_BACKFILL_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_request_invalid");
/// Backfill Desk composite configuration is missing or references an unexpected document.
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "config_composite_invalid");
/// Frontend logging requested an unsupported level.
diff --git a/crates/ksp-app-backfill-desk/src/lib.rs b/crates/ksp-app-backfill-desk/src/lib.rs
index 84f76ae..3e6aeea 100644
--- a/crates/ksp-app-backfill-desk/src/lib.rs
+++ b/crates/ksp-app-backfill-desk/src/lib.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/lib.rs
-// version: 5
+// version: 6
//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs.
@@ -8,8 +8,10 @@
#![warn(missing_docs)]
mod app_state;
+mod backfill_request;
mod bootstrap;
mod constants;
+mod dto_backfill;
mod dto_common;
mod errors;
mod frontend_logging;
@@ -26,6 +28,10 @@ pub use self::tauri::run;
/// Shared Backfill Desk application state managed by Tauri.
pub(crate) use self::app_state::AppState;
+/// Maps one app-owned campaign DTO to the validated KSP Backfill request contract.
+pub(crate) use self::backfill_request::map_backfill_request;
+/// Projects one validated Backfill request without returning address or signature values.
+pub(crate) use self::backfill_request::project_backfill_request;
/// Crate-internal Logging startup state shared by the application state.
pub(crate) use self::bootstrap::LoggingStartup;
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
@@ -42,10 +48,20 @@ pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING;
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_STORE;
/// Composite component identifier for Transport.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT;
+/// Initial hydration concurrency proposed by the Backfill campaign form.
+pub(crate) use self::constants::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY;
+/// Initial candidate cap proposed by the Backfill campaign form.
+pub(crate) use self::constants::DEFAULT_BACKFILL_MAX_CANDIDATES;
+/// Initial discovery-page cap proposed by the Backfill campaign form.
+pub(crate) use self::constants::DEFAULT_BACKFILL_MAX_PAGES;
+/// Initial signature page size proposed by the Backfill campaign form.
+pub(crate) use self::constants::DEFAULT_BACKFILL_PAGE_SIZE;
/// Structured domain used while bootstrapping Config and Logging.
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 by Backfill campaign admission and request mapping.
+pub(crate) use self::constants::TRACING_DOMAIN_REQUEST;
/// Structured domain used by the Backfill Desk shell.
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
/// Structured domain used by Store readiness and shutdown operations.
@@ -62,7 +78,19 @@ pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
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;
-/// Safe Transport-and-Store readiness subset of the Backfill Desk options contract.
+/// Backend-derived request bounds and Desk defaults for the campaign form.
+pub(crate) use self::dto_backfill::BackfillRequestLimitsDto;
+/// Safe request preview produced after strict backend mapping.
+pub(crate) use self::dto_backfill::BackfillRequestPreviewDto;
+/// App-owned campaign request received from the frontend.
+pub(crate) use self::dto_backfill::BackfillStartRequestDto;
+/// Returns commitment codes admitted by the current Backfill contract.
+pub(crate) use self::dto_backfill::backfill_commitment_codes;
+/// Builds frontend-safe campaign limits from Job-owned public constants.
+pub(crate) use self::dto_backfill::backfill_request_limits;
+/// Returns scope kind codes admitted by the current Backfill contract.
+pub(crate) use self::dto_backfill::backfill_scope_kind_codes;
+/// Safe Backfill Desk options contract combining readiness and campaign metadata.
pub(crate) use self::dto_common::BackfillDeskOptionsDto;
/// Safe logical HTTP route exposed for operator selection.
pub(crate) use self::dto_common::BackfillHttpRouteOptionDto;
@@ -74,6 +102,10 @@ pub(crate) use self::dto_common::ShellStatusDto;
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Backfill Desk runtime state cannot be locked safely.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
+/// Backfill Desk cannot admit a campaign before Transport and Store composition is ready.
+pub(crate) use self::errors::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY;
+/// Backfill Desk received a campaign field that cannot map to the Backfill contract.
+pub(crate) use self::errors::ERROR_CODE_BACKFILL_REQUEST_INVALID;
/// Backfill Desk composite configuration is missing or references an unexpected document.
pub(crate) use self::errors::ERROR_CODE_CONFIG_COMPOSITE_INVALID;
/// Frontend logging requested an unsupported level.
diff --git a/crates/ksp-app-backfill-desk/src/tauri.rs b/crates/ksp-app-backfill-desk/src/tauri.rs
index eae1b32..30747d7 100644
--- a/crates/ksp-app-backfill-desk/src/tauri.rs
+++ b/crates/ksp-app-backfill-desk/src/tauri.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/tauri.rs
-// version: 4
+// version: 5
//! Tauri runtime assembly for the KSP Backfill desktop application.
@@ -75,7 +75,13 @@ fn configure_plugins(builder: tauri::Builder) -> tauri::Builder) -> tauri::Builder {
- return builder.invoke_handler(tauri::generate_handler![backfill_options, emit_frontend_log, get_runtime_status, splash_frontend_ready]);
+ return builder.invoke_handler(tauri::generate_handler![
+ backfill_options,
+ backfill_validate_request,
+ emit_frontend_log,
+ get_runtime_status,
+ splash_frontend_ready
+ ]);
}
fn configure_setup(builder: tauri::Builder) -> tauri::Builder {
@@ -158,6 +164,18 @@ fn backfill_options(state: tauri::State<'_, crate::AppState>) -> std::result::Re
};
}
+#[tauri::command]
+fn backfill_validate_request(
+ request: crate::BackfillStartRequestDto,
+ state: tauri::State<'_, crate::AppState>,
+) -> std::result::Result {
+ let result = state.validate_backfill_request(request);
+ 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("backfill_validate_request", crate::TRACING_DOMAIN_REQUEST, &error)),
+ };
+}
+
#[tauri::command]
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload);
diff --git a/crates/ksp-app-backfill-desk/src/transport_runtime.rs b/crates/ksp-app-backfill-desk/src/transport_runtime.rs
index 95dd0bd..9dbe3da 100644
--- a/crates/ksp-app-backfill-desk/src/transport_runtime.rs
+++ b/crates/ksp-app-backfill-desk/src/transport_runtime.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/transport_runtime.rs
-// version: 3
+// version: 4
//! Composite-selected HTTP Transport readiness and route inventory owned by Backfill Desk.
@@ -35,12 +35,20 @@ impl TransportRuntime {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
+ let limits = crate::backfill_request_limits();
+ let limits = match limits {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
let transport_ready = configured_networks.len() == 1 && !http_routes.is_empty() && snapshot.available_endpoint_count() > 0;
return std::result::Result::Ok(crate::BackfillDeskOptionsDto {
+ commitments: crate::backfill_commitment_codes(),
composition_ready: false,
configured_networks,
http_routes,
+ limits,
network_coherent: false,
+ scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None,
store_network: std::option::Option::None,
store_ready: false,
diff --git a/crates/ksp-app-backfill-desk/tests/desktop_contract.rs b/crates/ksp-app-backfill-desk/tests/desktop_contract.rs
index d73f558..470caad 100644
--- a/crates/ksp-app-backfill-desk/tests/desktop_contract.rs
+++ b/crates/ksp-app-backfill-desk/tests/desktop_contract.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
-// version: 6
+// version: 7
//! Structural desktop contract checks for the Backfill Desk scaffold.
@@ -240,8 +240,6 @@ fn pre_006_mainnet_default_and_http_route_selector_are_explicit_without_start_ru
for required in ["mainnet_backfill_pool", "backfill_pool", "backfill_publicnode", "backfill_solana_public", "solana-rpc.publicnode.com"] {
assert!(transport.contains(required), "missing Backfill HTTP route marker {required}");
}
- let app_state = read_text(root.join("src/app_state.rs").as_path());
- assert!(app_state.contains("pre.006-mainnet-http-routing"));
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(frontend.contains("Backfill Desk HTTP route selection changed"));
assert!(frontend.contains("backfill_options"));
@@ -251,3 +249,50 @@ fn pre_006_mainnet_default_and_http_route_selector_are_explicit_without_start_ru
assert!(!frontend.contains(forbidden), "pre.006 frontend opens deferred runtime marker {forbidden}");
}
}
+
+#[test]
+fn pre_007_campaign_dto_and_backend_mapping_exist_without_start_runtime() {
+ let root = app_root();
+ let manifest = read_text(root.join("Cargo.toml").as_path());
+ assert!(manifest.contains("ksp-job-api = { path = \"../ksp-job-api\" }"));
+ assert!(manifest.contains("ksp-job-backfill-lib = { path = \"../ksp-job-backfill-lib\" }"));
+ let dto = read_text(root.join("src/dto_backfill.rs").as_path());
+ for required in [
+ "BackfillStartRequestDto",
+ "BackfillRequestPreviewDto",
+ "MAX_BACKFILL_CANDIDATES",
+ "MAX_BACKFILL_HYDRATION_CONCURRENCY",
+ "MAX_BACKFILL_PAGES",
+ "MAX_BACKFILL_PAGE_SIZE",
+ ] {
+ assert!(dto.contains(required), "missing pre.007 DTO/request bound marker {required}");
+ }
+ let mapping = read_text(root.join("src/backfill_request.rs").as_path());
+ for required in ["BackfillRequest::new", "HttpRoleName::new", "latest_address", "before_address", "after_address", "explicit_signatures"] {
+ assert!(mapping.contains(required), "missing strict Backfill request mapping marker {required}");
+ }
+ let tauri = read_text(root.join("src/tauri.rs").as_path());
+ assert!(tauri.contains("backfill_validate_request"));
+ let html = read_text(root.join("frontend/main.html").as_path());
+ for required in [
+ "backfillCampaignForm",
+ "backfillScopeKind",
+ "backfillCommitment",
+ "backfillAddress",
+ "backfillAnchorSignature",
+ "backfillExplicitSignatures",
+ "backfillMinContextSlot",
+ "validateBackfillRequest",
+ "Valider la requête",
+ ] {
+ assert!(html.contains(required), "missing campaign form marker {required}");
+ }
+ let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
+ assert!(frontend.contains("backfill_validate_request"));
+ assert!(frontend.contains("Backfill Desk campaign validation requested"));
+ assert!(frontend.contains("Aucun job n'a été démarré"));
+ for forbidden in ["backfill_start", "BackfillJobRuntime", "BackfillJobHandle"] {
+ assert!(!frontend.contains(forbidden), "pre.007 frontend opens deferred Start/runtime marker {forbidden}");
+ assert!(!tauri.contains(forbidden), "pre.007 Tauri surface opens deferred Start/runtime marker {forbidden}");
+ }
+}
diff --git a/crates/ksp-app-backfill-desk/tests/desktop_security.rs b/crates/ksp-app-backfill-desk/tests/desktop_security.rs
index 1e2d279..0c1d700 100644
--- a/crates/ksp-app-backfill-desk/tests/desktop_security.rs
+++ b/crates/ksp-app-backfill-desk/tests/desktop_security.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_security.rs
-// version: 6
+// version: 7
//! Security and dependency-boundary checks for the Backfill Desk scaffold.
@@ -43,7 +43,7 @@ fn read_text(path: &std::path::Path) -> String {
}
#[test]
-fn capability_surface_remains_core_plus_tracing_while_transport_and_store_are_backend_only() {
+fn capability_surface_remains_core_plus_tracing_while_backfill_runtime_stays_backend_only() {
let root = app_root();
let capability = read_text(root.join("capabilities/default.json").as_path());
assert!(capability.contains("\"core:default\""));
@@ -53,10 +53,12 @@ fn capability_surface_remains_core_plus_tracing_while_transport_and_store_are_ba
}
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains("tauri-plugin-tracing.workspace = true"));
+ assert!(manifest.contains("ksp-job-api = { path = \"../ksp-job-api\" }"));
+ assert!(manifest.contains("ksp-job-backfill-lib = { path = \"../ksp-job-backfill-lib\" }"));
assert!(manifest.contains("ksp-onchain-transport-lib = { path = \"../ksp-onchain-transport-lib\" }"));
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\" }"));
- for forbidden in ["ksp-job-backfill-lib", "ksp-store-api", "ksp-store-postgres-lib", "reqwest", "tokio-postgres"] {
- assert!(!manifest.contains(forbidden), "current Backfill Desk opens a forbidden dependency: {forbidden}");
+ for forbidden in ["ksp-store-api", "ksp-store-postgres-lib", "reqwest", "tokio-postgres", "tonic", "yellowstone-grpc"] {
+ assert!(!manifest.contains(forbidden), "current Backfill Desk opens a forbidden direct dependency: {forbidden}");
}
}
@@ -89,7 +91,7 @@ fn pre_002_tauri_commands_remain_centralized() {
assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display());
}
}
- assert_eq!(command_count, 4);
+ assert_eq!(command_count, 5);
}
#[test]
@@ -109,21 +111,23 @@ fn pre_002_frontend_instrumentation_avoids_business_or_secret_payloads() {
assert!(main.contains(required));
}
}
- for forbidden in ["JSON.stringify(status)", "signature", "address", "apiKey", "authorization", "database_url"] {
- assert!(!main.contains(forbidden));
+ for forbidden in [
+ "JSON.stringify(status)",
+ "apiKey",
+ "authorization",
+ "database_url",
+ "campaign validation requested\", {\n address",
+ "campaign validation requested\", {\n anchorSignature",
+ "campaign validation requested\", {\n explicitSignatures",
+ "campaign validation requested\", {\n minContextSlot",
+ ] {
+ assert!(!main.contains(forbidden), "frontend tracing includes forbidden request payload marker {forbidden}");
}
}
#[test]
-fn pre_005_opens_store_facade_without_backend_or_job_dependencies() {
+fn pre_005_store_runtime_remains_backend_neutral_after_job_mapping_opens() {
let root = app_root();
- let manifest = read_text(root.join("Cargo.toml").as_path());
- assert!(manifest.contains("ksp-config-lib = { path = \"../ksp-config-lib\" }"));
- assert!(manifest.contains("ksp-onchain-transport-lib = { path = \"../ksp-onchain-transport-lib\" }"));
- assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\" }"));
- for forbidden in ["ksp-job-backfill-lib", "ksp-store-api", "ksp-store-postgres-lib", "reqwest", "tokio-postgres"] {
- assert!(!manifest.contains(forbidden), "pre.005 opens a forbidden direct dependency: {forbidden}");
- }
let bootstrap = read_text(root.join("src/bootstrap.rs").as_path());
assert!(bootstrap.contains("LogFilterLevel::Trace"));
assert!(!bootstrap.contains("HttpTransportPool"));
@@ -132,8 +136,8 @@ fn pre_005_opens_store_facade_without_backend_or_job_dependencies() {
assert!(store.contains("ksp_store_lib::Store::open"));
assert!(store.contains("store.health().await"));
assert!(store.contains("store.close().await"));
- for forbidden in ["connection_uri", "postgres", "database_url", "provider()", "endpoint_url"] {
- assert!(!store.contains(forbidden), "Store runtime leaks or depends on forbidden physical metadata marker {forbidden}");
+ for forbidden in ["ksp_job_", "BackfillRequest", "connection_uri", "postgres", "database_url", "provider()", "endpoint_url"] {
+ assert!(!store.contains(forbidden), "Store runtime leaks or absorbs another responsibility: {forbidden}");
}
let dto = read_text(root.join("src/dto_common.rs").as_path());
assert!(dto.contains("BackfillDeskOptionsDto"));
@@ -157,3 +161,46 @@ fn pre_006_frontend_receives_only_safe_http_route_metadata_and_no_endpoint_mater
assert!(!frontend.contains(forbidden), "frontend embeds forbidden provider material marker {forbidden}");
}
}
+
+#[test]
+fn pre_007_request_mapping_keeps_network_endpoint_and_payload_secrets_backend_owned() {
+ let root = app_root();
+ let dto = read_text(root.join("src/dto_backfill.rs").as_path());
+ let request_start = dto.find("pub(crate) struct BackfillStartRequestDto");
+ let preview_start = dto.find("pub(crate) struct BackfillRequestPreviewDto");
+ assert!(request_start.is_some());
+ assert!(preview_start.is_some());
+ let request_source = match (request_start, preview_start) {
+ (std::option::Option::Some(start), std::option::Option::Some(end)) if start < end => &dto[start..end],
+ _ => "",
+ };
+ for forbidden in [
+ "pub(crate) network:",
+ "pub(crate) provider",
+ "pub(crate) endpoint",
+ "pub(crate) url",
+ "pub(crate) job_id",
+ "pub(crate) credential",
+ "pub(crate) token",
+ ] {
+ assert!(!request_source.contains(forbidden), "frontend request DTO owns forbidden field marker {forbidden}");
+ }
+ let mapping = read_text(root.join("src/backfill_request.rs").as_path());
+ assert!(mapping.contains("options.store_network.as_deref()"));
+ assert!(mapping.contains("options.http_routes.iter().any"));
+ assert!(mapping.contains("BackfillRequest::new"));
+ assert!(!mapping.contains("HttpTransportPool::new"));
+ assert!(!mapping.contains("Store::open"));
+ let state = read_text(root.join("src/app_state.rs").as_path());
+ assert!(state.contains("backfill-desk-validation"));
+ assert!(state.contains("validated Backfill Desk campaign request without starting a Job"));
+ for forbidden in [
+ "preview.address",
+ "preview.anchor_signature",
+ "preview.explicit_signatures",
+ "request.address.as_str()",
+ "request.anchor_signature.as_str()",
+ ] {
+ assert!(!state.contains(forbidden), "request validation logging includes forbidden payload marker {forbidden}");
+ }
+}
diff --git a/crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs b/crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
new file mode 100644
index 0000000..3c8c08f
--- /dev/null
+++ b/crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
@@ -0,0 +1,198 @@
+// file: crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
+// version: 1
+
+fn ready_options() -> crate::BackfillDeskOptionsDto {
+ let limits = crate::backfill_request_limits();
+ assert!(limits.is_ok());
+ let limits = match limits {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => crate::BackfillRequestLimitsDto {
+ default_hydration_concurrency: 1,
+ default_max_candidates: 1,
+ default_max_pages: 1,
+ default_page_size: 1,
+ max_hydration_concurrency: 1,
+ max_candidates: 1,
+ max_pages: 1,
+ max_page_size: 1,
+ max_signature_text_bytes: 88,
+ min_signature_text_bytes: 64,
+ },
+ };
+ return crate::BackfillDeskOptionsDto {
+ commitments: crate::backfill_commitment_codes(),
+ composition_ready: true,
+ configured_networks: vec!["mainnet-beta".to_owned()],
+ http_routes: vec![
+ crate::BackfillHttpRouteOptionDto {
+ pooled: true,
+ providers: vec!["publicnode".to_owned(), "solana-public".to_owned()],
+ role: "backfill_pool".to_owned(),
+ },
+ crate::BackfillHttpRouteOptionDto { pooled: false, providers: vec!["publicnode".to_owned()], role: "backfill_publicnode".to_owned() },
+ ],
+ limits,
+ network_coherent: true,
+ scope_kinds: crate::backfill_scope_kind_codes(),
+ store_diagnostic: std::option::Option::None,
+ store_network: std::option::Option::Some("mainnet-beta".to_owned()),
+ store_ready: true,
+ transport_diagnostic: std::option::Option::None,
+ transport_ready: true,
+ };
+}
+
+fn request(scope_kind: &str) -> crate::BackfillStartRequestDto {
+ return crate::BackfillStartRequestDto {
+ address: std::option::Option::Some("11111111111111111111111111111111".to_owned()),
+ anchor_signature: std::option::Option::None,
+ commitment: "finalized".to_owned(),
+ explicit_signatures: std::vec::Vec::new(),
+ hydration_concurrency: 4,
+ http_role: "backfill_pool".to_owned(),
+ max_candidates: 100,
+ max_pages: 10,
+ min_context_slot: std::option::Option::Some("123456".to_owned()),
+ page_size: 100,
+ scope_kind: scope_kind.to_owned(),
+ };
+}
+
+fn job_id() -> std::option::Option {
+ let job_id = ksp_job_api::JobId::new("request-mapping-test");
+ assert!(job_id.is_ok());
+ return match job_id {
+ std::result::Result::Ok(value) => std::option::Option::Some(value),
+ std::result::Result::Err(_) => std::option::Option::None,
+ };
+}
+
+#[test]
+fn latest_address_maps_to_backend_network_role_commitment_and_bounds() {
+ let job_id = job_id();
+ let job_id = match job_id {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return,
+ };
+ let mapped = crate::map_backfill_request(request("latest_address"), &ready_options(), job_id);
+ assert!(mapped.is_ok());
+ let mapped = match mapped {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => return,
+ };
+ assert_eq!(mapped.network().as_str(), "mainnet-beta");
+ assert_eq!(mapped.role().as_str(), "backfill_pool");
+ assert_eq!(mapped.commitment(), ksp_job_backfill_lib::BackfillCommitment::Finalized);
+ assert_eq!(mapped.scope().kind(), ksp_job_backfill_lib::BackfillScopeKind::LatestAddress);
+ assert_eq!(mapped.page_size(), 100);
+ assert_eq!(mapped.max_pages(), 10);
+ assert_eq!(mapped.max_candidates(), 100);
+ assert_eq!(mapped.hydration_concurrency(), 4);
+ assert_eq!(mapped.min_context_slot(), std::option::Option::Some(123456));
+}
+
+#[test]
+fn before_and_after_scopes_require_an_exact_anchor_shape() {
+ for scope_kind in ["before_address", "after_address"] {
+ let job_id = job_id();
+ let job_id = match job_id {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return,
+ };
+ let mut input = request(scope_kind);
+ input.anchor_signature = std::option::Option::Some("1".repeat(64));
+ let mapped = crate::map_backfill_request(input, &ready_options(), job_id);
+ assert!(mapped.is_ok(), "scope {scope_kind} should map with a bounded anchor");
+ }
+ let job_id = job_id();
+ let job_id = match job_id {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return,
+ };
+ let missing_anchor = crate::map_backfill_request(request("before_address"), &ready_options(), job_id);
+ assert!(missing_anchor.is_err());
+}
+
+#[test]
+fn explicit_signatures_reject_address_context_and_deduplicate_in_job_contract() {
+ let job_id = job_id();
+ let job_id = match job_id {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return,
+ };
+ let mut input = request("explicit_signatures");
+ input.address = std::option::Option::None;
+ input.min_context_slot = std::option::Option::None;
+ input.explicit_signatures = vec!["1".repeat(64), "1".repeat(64), "2".repeat(64)];
+ let mapped = crate::map_backfill_request(input, &ready_options(), job_id);
+ assert!(mapped.is_ok());
+ let mapped = match mapped {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => return,
+ };
+ let signatures = mapped.scope().signatures();
+ assert!(signatures.is_some());
+ if let std::option::Option::Some(signatures) = signatures {
+ assert_eq!(signatures.len(), 2);
+ }
+ let mut hostile = request("explicit_signatures");
+ hostile.address = std::option::Option::None;
+ hostile.min_context_slot = std::option::Option::None;
+ hostile.explicit_signatures = vec![" 1".to_owned()];
+ let hostile_job_id = job_id();
+ let hostile_job_id = match hostile_job_id {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return,
+ };
+ assert!(crate::map_backfill_request(hostile, &ready_options(), hostile_job_id).is_err());
+}
+
+#[test]
+fn route_inventory_and_composition_are_revalidated_before_mapping() {
+ let first_job_id = job_id();
+ let first_job_id = match first_job_id {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return,
+ };
+ let mut unknown_route = request("latest_address");
+ unknown_route.http_role = "provider-from-frontend".to_owned();
+ let rejected = crate::map_backfill_request(unknown_route, &ready_options(), first_job_id);
+ assert!(rejected.is_err());
+ let mut unavailable = ready_options();
+ unavailable.composition_ready = false;
+ let second_job_id = job_id();
+ let second_job_id = match second_job_id {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return,
+ };
+ assert!(crate::map_backfill_request(request("latest_address"), &unavailable, second_job_id).is_err());
+}
+
+#[test]
+fn request_preview_never_contains_address_or_signature_values() {
+ let job_id = job_id();
+ let job_id = match job_id {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return,
+ };
+ let mapped = crate::map_backfill_request(request("latest_address"), &ready_options(), job_id);
+ assert!(mapped.is_ok());
+ let mapped = match mapped {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => return,
+ };
+ let preview = crate::project_backfill_request(&mapped);
+ assert!(preview.is_ok());
+ let preview = match preview {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => return,
+ };
+ let serialized = serde_json::to_string(&preview);
+ assert!(serialized.is_ok());
+ if let std::result::Result::Ok(serialized) = serialized {
+ assert!(!serialized.contains("11111111111111111111111111111111"));
+ assert!(!serialized.contains(&"1".repeat(64)));
+ assert!(serialized.contains("mainnet-beta"));
+ assert!(serialized.contains("backfill_pool"));
+ }
+}
diff --git a/crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs b/crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
new file mode 100644
index 0000000..4a4d4c5
--- /dev/null
+++ b/crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
@@ -0,0 +1,31 @@
+// file: crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
+// version: 1
+
+#[test]
+fn request_limits_are_derived_from_job_owned_public_bounds() {
+ let limits = crate::backfill_request_limits();
+ assert!(limits.is_ok());
+ let limits = match limits {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => return,
+ };
+ assert_eq!(usize::try_from(limits.max_page_size), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_PAGE_SIZE));
+ assert_eq!(usize::try_from(limits.max_pages), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_PAGES));
+ assert_eq!(usize::try_from(limits.max_candidates), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES));
+ assert_eq!(usize::try_from(limits.max_hydration_concurrency), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_HYDRATION_CONCURRENCY));
+ assert_eq!(usize::try_from(limits.min_signature_text_bytes), std::result::Result::Ok(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES));
+ assert_eq!(usize::try_from(limits.max_signature_text_bytes), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_SIGNATURE_TEXT_BYTES));
+ assert!(limits.default_page_size <= limits.max_page_size);
+ assert!(limits.default_max_pages <= limits.max_pages);
+ assert!(limits.default_max_candidates <= limits.max_candidates);
+ assert!(limits.default_hydration_concurrency <= limits.max_hydration_concurrency);
+}
+
+#[test]
+fn request_options_expose_exact_current_commitments_and_http_scopes() {
+ assert_eq!(crate::backfill_commitment_codes(), vec!["finalized".to_owned(), "confirmed".to_owned()]);
+ assert_eq!(
+ crate::backfill_scope_kind_codes(),
+ vec!["latest_address".to_owned(), "before_address".to_owned(), "after_address".to_owned(), "explicit_signatures".to_owned()]
+ );
+}
diff --git a/crates/ksp-app-backfill-desk/unit_tests/dto_common.rs b/crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
index 2f4ee9f..822efcd 100644
--- a/crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
+++ b/crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
-// version: 4
+// version: 5
#[test]
fn command_error_projection_excludes_arbitrary_context_and_source_values() {
@@ -15,11 +15,20 @@ fn command_error_projection_excludes_arbitrary_context_and_source_values() {
#[test]
fn transport_options_projection_contains_only_safe_transport_metadata() {
+ let limits = crate::backfill_request_limits();
+ assert!(limits.is_ok());
+ let limits = match limits {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => return,
+ };
let dto = crate::BackfillDeskOptionsDto {
+ commitments: crate::backfill_commitment_codes(),
http_routes: vec![crate::BackfillHttpRouteOptionDto { pooled: false, providers: vec!["solana-public".to_owned()], role: "default".to_owned() }],
+ limits,
composition_ready: true,
configured_networks: vec!["devnet".to_owned()],
network_coherent: true,
+ scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None,
store_network: std::option::Option::Some("devnet".to_owned()),
store_ready: true,
diff --git a/deltas/0.3.7/pre.007.md b/deltas/0.3.7/pre.007.md
new file mode 100644
index 0000000..5a1a370
--- /dev/null
+++ b/deltas/0.3.7/pre.007.md
@@ -0,0 +1,138 @@
+
+
+
+# Delta `0.3.7-pre.007` — DTO et mapping de requête Backfill
+
+## 1. Base requise
+
+```text
+0.3.7-pre.006-fix.001
+workspace.package.version = 0.3.7-pre.6.fix.1
+```
+
+## 2. Objectif
+
+Ouvrir le contrat Job Backfill dans la Desk uniquement pour construire, valider et prévisualiser une requête de campagne HTTP, sans créer ni démarrer de `BackfillJobRuntime`.
+
+## 3. Contrat applicatif
+
+Backfill Desk dépend désormais directement de `ksp-job-api` et `ksp-job-backfill-lib`. Le frontend reçoit les quatre scopes HTTP V1, les deux commitments, les maxima publics Job et des defaults applicatifs bornés.
+
+`BackfillStartRequestDto` reste possédé par l'application et ne contient ni réseau, provider, endpoint, URL, credential, checkpoint ou JobId. Le réseau est dérivé du Store composé ; `http_role` est revalidé contre l'inventaire sûr `BackfillDeskOptionsDto::http_routes` avant construction du contrat Job.
+
+`min_context_slot` traverse IPC sous forme de texte décimal et est parsé en `u64` côté Rust afin d'éviter toute perte de précision JavaScript.
+
+## 4. Mapping autoritaire
+
+Le mapping Rust :
+
+- valide la readiness Transport + Store et leur cohérence réseau ;
+- mappe explicitement `finalized` / `confirmed` ;
+- mappe explicitement `latest_address`, `before_address`, `after_address` et `explicit_signatures` ;
+- parse l'adresse et les signatures dans les types propriétaires ;
+- convertit les bornes `u32` vers `usize` ;
+- délègue la validation finale à `ksp-job-backfill-lib::BackfillRequest::new` ;
+- n'ouvre aucun runtime Job.
+
+La commande Tauri `backfill_validate_request` utilise exactement ce mapping avec un JobId interne réservé à la validation, puis renvoie une `BackfillRequestPreviewDto` sûre. La preview expose réseau/rôle/commitment/scope/bornes, mais réduit adresse, ancre, `min_context_slot` et signatures à des booléens ou comptages.
+
+## 5. Frontend
+
+Le formulaire Backfill expose :
+
+- route HTTP logique ;
+- scope ;
+- commitment ;
+- adresse et ancre selon le scope ;
+- signatures explicites ;
+- page size ;
+- max pages ;
+- max candidates ;
+- hydration concurrency ;
+- `min_context_slot`.
+
+Le bouton reste **Valider la requête**. Aucun Start, Cancel, Resume ou checkpoint n'est ouvert dans cette tranche. Les logs frontend ne contiennent aucune adresse, signature, ancre ou valeur de `min_context_slot`.
+
+## 6. Version
+
+```text
+workspace.package.version = 0.3.7-pre.7
+label = 0.3.7-pre.007
+```
+
+## 7. Fichiers ajoutés
+
+```text
+crates/ksp-app-backfill-desk/src/backfill_request.rs
+crates/ksp-app-backfill-desk/src/dto_backfill.rs
+crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
+crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
+deltas/0.3.7/pre.007.md
+```
+
+## 8. Fichiers modifiés
+
+```text
+Cargo.toml
+crates/ksp-app-backfill-desk/Cargo.toml
+crates/ksp-app-backfill-desk/frontend/main.html
+crates/ksp-app-backfill-desk/frontend/ts/main.ts
+crates/ksp-app-backfill-desk/src/app_state.rs
+crates/ksp-app-backfill-desk/src/constants.rs
+crates/ksp-app-backfill-desk/src/dto_common.rs
+crates/ksp-app-backfill-desk/src/errors.rs
+crates/ksp-app-backfill-desk/src/lib.rs
+crates/ksp-app-backfill-desk/src/tauri.rs
+crates/ksp-app-backfill-desk/src/transport_runtime.rs
+crates/ksp-app-backfill-desk/tests/desktop_contract.rs
+crates/ksp-app-backfill-desk/tests/desktop_security.rs
+crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
+docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md
+docs/validation/024-V0_3_7_BACKFILL_DESK.md
+```
+
+## 9. Fichiers supprimés
+
+Aucun.
+
+## 10. Gate précédent acquis
+
+Le replay opérateur de `0.3.7-pre.006-fix.001` est propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, `ksp-config-lib`, Backfill Desk et arbres Cargo passent sans anomalie rapportée.
+
+## 11. Validations exécutées dans l'environnement d'assemblage
+
+```text
+python3 scripts/audit_rust_workspace_rules.py
+python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.7
+```
+
+Contrôles ciblés :
+
+- syntaxe TypeScript par transpilation locale ;
+- cohérence des headers/version des fichiers modifiés ;
+- présence des quatre scopes et deux commitments à partir des contrats Job publics ;
+- absence de réseau/provider/URL/endpoint/JobId dans le DTO frontend ;
+- absence de `BackfillJobRuntime`, Start, Cancel, Resume et checkpoint dans le runtime de cette tranche ;
+- absence de valeur d'adresse/signature/ancre/`min_context_slot` dans la preview et les logs frontend ;
+- dépendances directes limitées aux façades/API prévues ;
+- contrôle différentiel et reconstruction par application du delta.
+
+## 12. Validations non exécutées dans l'environnement d'assemblage
+
+`cargo` et `rustfmt` ne sont pas disponibles dans l'environnement d'assemblage. Gate opérateur obligatoire avant `pre.008` :
+
+```text
+cargo fmt --all
+python3 scripts/audit_rust_workspace_rules.py
+python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.7
+cargo check --workspace
+cargo clippy --workspace --all-targets
+cargo test -p ksp-job-backfill-lib
+cargo test -p ksp-app-backfill-desk
+cargo tree -p ksp-app-backfill-desk --edges normal
+cargo tree -p ksp-app-backfill-desk -e features
+```
+
+## 13. Suite
+
+`pre.008` ouvrira le runtime et Start : état single-active-run, construction `BackfillJobRuntime`, admission atomique, spawn non bloquant et handle installé avant exécution. Le choix HTTP pool/provider et le mapping `BackfillRequest` de `pre.007` restent les entrées autoritaires.
diff --git a/docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md b/docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md
index 79852cb..ea2789f 100644
--- a/docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md
+++ b/docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md
@@ -1,5 +1,5 @@
-
+
# Plan v0.3.7 — Backfill Desk
@@ -259,7 +259,9 @@ Faire de `mainnet` le profil composite par défaut, conserver `supertrace` penda
### pre.007 — DTO/request mapping
-Ajouter `ksp-job-backfill-lib` et compléter les DTO TS-RS app-owned : quatre scopes HTTP existants, commitments, min context slot, bornes backend-owned, signatures explicites hostiles et rôle HTTP sélectionné. Mapper strictement vers `BackfillRequest`/`HttpRoleName` côté Rust sans Start effectif et refuser toute route qui n'appartient pas à l'inventaire courant.
+Ajouter `ksp-job-api` et `ksp-job-backfill-lib` comme dépendances backend directes de la Desk, sans ouvrir encore `BackfillJobRuntime`. Les DTO TS-RS app-owned exposent les quatre scopes HTTP existants, les commitments, `min_context_slot` sous forme de texte décimal, les bornes publiques Job et des defaults applicatifs bornés (`page_size=100`, `max_pages=10`, `max_candidates=1000`, `hydration_concurrency=4`). Le request DTO ne contient ni réseau, ni provider, ni endpoint, ni URL, ni JobId.
+
+Le mapping Rust dérive le réseau du Store configuré, revalide `http_role` contre `BackfillDeskOptionsDto::http_routes`, parse adresse/signatures et délègue l'autorité finale des bornes/cohérences à `BackfillRequest::new`. Une commande `backfill_validate_request` utilise exactement ce mapping pour produire une projection sûre réduite à présence/comptage et paramètres non sensibles ; elle ne lance aucun Job. Le frontend matérialise le formulaire de campagne et le bouton **Valider la requête** ; adresse, ancre, signatures explicites et `min_context_slot` ne sont jamais journalisés.
### pre.008 — runtime + Start
diff --git a/docs/validation/024-V0_3_7_BACKFILL_DESK.md b/docs/validation/024-V0_3_7_BACKFILL_DESK.md
index 37327de..2b4f0ec 100644
--- a/docs/validation/024-V0_3_7_BACKFILL_DESK.md
+++ b/docs/validation/024-V0_3_7_BACKFILL_DESK.md
@@ -1,5 +1,5 @@
-
+
# Validation v0.3.7 — Backfill Desk
@@ -61,10 +61,10 @@ Le journal opérateur fourni rapporte `cargo clean`, `cargo fmt`, audits Rust/Ma
- [X] Composite dédié `config/composite.ksp-app-backfill-desk.json` prévu.
- [X] Profils `devnet`, `mainnet`, `testnet` prévus avec couples Transport/Store de même réseau.
-- [X] Default prévu : `devnet`.
+- [X] Default final : `mainnet` depuis `pre.006`; Devnet/Testnet restent disponibles par reconfiguration du composite.
- [X] Aucun `std.job_backfill.json` en V1.
- [X] Les rôles sont dérivés de Transport et validés pour `getSignaturesForAddress` + `getTransaction`.
-- [X] Aucun rôle `history_backfill` n'est hardcodé ; la base standard actuelle expose `default`.
+- [X] Aucun rôle `history_backfill` n'est hardcodé ; `pre.006` expose les rôles logiques `backfill_pool`, `backfill_solana_public` et `backfill_publicnode` depuis Transport.
- [X] Une incohérence réseau est rejetée avant Start.
- [X] Le Desk ouvre Store via `ksp-store-lib`, jamais via backend direct.
@@ -97,8 +97,8 @@ Le journal opérateur fourni rapporte `cargo clean`, `cargo fmt`, audits Rust/Ma
- [X] Splash/main + tracing conformes.
- [X] Composite Backfill Desk packagé et valide.
- [X] Transport readiness et role inventory sûrs.
-- [ ] Store readiness et cohérence réseau.
-- [ ] Quatre scopes + commitments + bornes mappés.
+- [X] Store readiness et cohérence réseau.
+- [X] Quatre scopes + commitments + bornes mappés.
- [ ] Single active run + Start.
- [ ] Snapshot latest-value bridge.
- [ ] Cancel + races terminales.
@@ -207,7 +207,7 @@ cargo test -p ksp-app-backfill-desk FAIL — canari Ca
Backfill Desk dépend désormais directement de `ksp-onchain-transport-lib` au niveau backend uniquement. `initialize_transport()` résout le profil Transport déjà sélectionné par le composite, construit `HttpTransportPool`, dérive les clusters HTTP activés et déduplique les rôles logiques à partir des snapshots sûrs. Chaque rôle candidat doit réussir `select_for_method()` pour `getSignaturesForAddress` **et** `getTransaction` avant d'être exposé comme compatible.
-Le sous-contrat `BackfillDeskOptionsDto` expose uniquement : réseaux configurés, rôles compatibles, readiness Transport et diagnostic applicatif sûr. Il ne contient ni provider, URL, endpoint physique, token, checkpoint, payload RAW ou objet Transport. La commande Tauri `backfill_options` est centralisée dans `tauri.rs`. Les scopes, commitments et bornes restent hors de cette tranche et seront ajoutés en `pre.006`.
+Le sous-contrat `BackfillDeskOptionsDto` expose uniquement : réseaux configurés, rôles compatibles, readiness Transport et diagnostic applicatif sûr. Il ne contient ni provider, URL, endpoint physique, token, checkpoint, payload RAW ou objet Transport. La commande Tauri `backfill_options` est centralisée dans `tauri.rs`. Les scopes, commitments et bornes restent hors de cette tranche et seront ajoutés en `pre.007` après la stabilisation du routage HTTP de `pre.006`.
Une seule network distincte et au moins un rôle compatible sont requis pour `transport_ready = true`. Une erreur Config/Transport n'empêche pas le shell de démarrer : elle produit une options projection non-ready avec `CommandErrorDto`, tandis que le pool n'est pas conservé. La cohérence avec le réseau Store reste explicitement réservée à `pre.005`.
@@ -287,7 +287,7 @@ Le support Yellowstone gRPC de `ksp-job-backfill-lib`/Backfill Desk est retiré
- [X] le frontend charge/rend le sélecteur et trace son changement ;
- [X] aucun Start ni dépendance `ksp-job-backfill-lib` n'est ouvert par la Desk ;
- [X] audits statiques Rust/Markdown, JSON Schema et syntaxe TypeScript propres dans l'environnement d'assemblage ;
-- [ ] replay opérateur `cargo fmt/check/clippy/test` de `pre.006` à exécuter.
+- [X] replay opérateur `cargo fmt/check/clippy/test` de `pre.006` exécuté ; un fix Clippy strictement local a été requis puis traité.
## Correctif `pre.006-fix.001`
@@ -301,4 +301,30 @@ Le correctif reste strictement dans la responsabilité de `pre.006` : il ajoute
- [X] aucun contenu fonctionnel de `pre.006` n'est modifié ;
- [X] version Cargo synchronisée en `0.3.7-pre.6.fix.1` conformément à `VER-ID-007` et `VER-ID-010` ;
- [X] audits statiques Rust/Markdown rejoués dans l'environnement d'assemblage ;
-- [ ] replay opérateur `cargo fmt/check/clippy/test` du fix à exécuter.
+- [X] replay opérateur `cargo fmt/check/clippy/test` du fix propre : audits, check, Clippy, Config et toutes les suites Backfill Desk passent.
+
+## 15. `pre.007` — DTO et mapping de requête
+
+Le replay opérateur de `pre.006-fix.001` est acquis : audits Rust/Markdown propres, `cargo check --workspace` et Clippy propres, `ksp-config-lib` intégralement vert, puis Backfill Desk vert sur ses 21 tests unitaires, 5 tests de composition, 10 desktop contract, 6 desktop security et 1 public API. Les arbres Cargo ont également été exécutés sans anomalie rapportée.
+
+`pre.007` ouvre `ksp-job-api` et `ksp-job-backfill-lib` uniquement pour construire et valider le contrat de requête. `BackfillStartRequestDto` reste app-owned et ne transporte ni network, provider, endpoint, URL, credential, checkpoint ou JobId. Le backend dérive le réseau depuis le Store déjà composé et refuse tout `http_role` absent de l'inventaire sûr courant.
+
+Le formulaire expose les quatre scopes HTTP V1 (`latest_address`, `before_address`, `after_address`, `explicit_signatures`), `finalized`/`confirmed`, les quatre bornes numériques et `min_context_slot`. Ce dernier traverse l'IPC comme texte décimal afin d'éviter toute perte de précision JavaScript avant parsing `u64` côté Rust. Les maxima proviennent exclusivement des constantes publiques de `ksp-job-backfill-lib`; les defaults de Desk restent strictement inférieurs ou égaux à ces maxima.
+
+La commande `backfill_validate_request` construit un JobId interne uniquement pour satisfaire le contrat `BackfillRequest`, applique le même mapping qui sera réutilisé par Start, puis renvoie une `BackfillRequestPreviewDto` sûre. La preview contient le réseau, le rôle logique, commitment/scope, bornes et seulement présence/comptage pour les valeurs sensibles. Aucun Job n'est créé ni lancé dans cette tranche.
+
+### Gate statique local `pre.007`
+
+- [X] dépendances directes `ksp-job-api` et `ksp-job-backfill-lib` ajoutées à Backfill Desk ;
+- [X] aucune dépendance directe backend Store, `reqwest`, `tonic` ou Yellowstone ajoutée ;
+- [X] request DTO sans network/provider/endpoint/URL/JobId ;
+- [X] réseau dérivé du Store configuré et rôle HTTP revalidé contre l'inventaire courant ;
+- [X] quatre scopes et deux commitments mappés explicitement ;
+- [X] `min_context_slot` transporté comme texte puis parsé en `u64` côté Rust ;
+- [X] bornes maxima dérivées des constantes publiques Job et defaults applicatifs vérifiés ;
+- [X] preview sûre sans adresse ni signature ;
+- [X] formulaire frontend + validation backend disponibles, sans bouton Start ni `BackfillJobRuntime` ;
+- [X] instrumentation frontend limitée aux codes/rôle/scope et métadonnées de contrôle ;
+- [X] capabilities Tauri inchangées : `core:default + tracing:default` ;
+- [ ] `cargo fmt/check/clippy/test` de `pre.007` à rejouer par l'opérateur ; `cargo`/`rustfmt` restent absents du sandbox.
+