v0.3.6-pre.010

This commit is contained in:
2026-09-01 16:55:14 +02:00
parent 8dbd86c297
commit 7f8ce987a5
6 changed files with 613 additions and 25 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 410
# version: 411
[workspace]
resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-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.6-pre.9.fix.2"
version = "0.3.6-pre.10"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -0,0 +1,350 @@
// file: crates/ksp-job-backfill-lib/tests/hardening.rs
// version: 4
//! Adversarial, security, visibility and external-boundary hardening canaries for `pre.010`.
fn signature_text() -> std::string::String {
return "2".repeat(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES);
}
fn signature() -> std::option::Option<ksp_job_backfill_lib::BackfillSignature> {
let result = ksp_job_backfill_lib::BackfillSignature::new(signature_text());
return match result {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => {
assert_eq!(error.code(), ksp_job_backfill_lib::ERROR_CODE_BACKFILL_SIGNATURE_INVALID);
std::option::Option::None
},
};
}
fn network(value: &'static str) -> std::option::Option<ksp_store_lib::RawNetworkId> {
let result = ksp_store_lib::RawNetworkId::new(value);
return match result {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => {
assert_eq!(error.code().domain(), "store_api");
std::option::Option::None
},
};
}
fn job_id(value: &'static str) -> std::option::Option<ksp_job_api::JobId> {
let result = ksp_job_api::JobId::new(value);
return match result {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => {
assert_eq!(error.code().domain(), "job_api");
std::option::Option::None
},
};
}
fn explicit_scope() -> std::option::Option<ksp_job_backfill_lib::BackfillScope> {
let signature = match signature() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let result = ksp_job_backfill_lib::BackfillScope::explicit_signatures(vec![signature]);
return match result {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => {
assert_eq!(error.code(), ksp_job_backfill_lib::ERROR_CODE_BACKFILL_REQUEST_INVALID);
std::option::Option::None
},
};
}
fn request(
id: &'static str,
network_name: &'static str,
role_name: &'static str,
scope: ksp_job_backfill_lib::BackfillScope,
) -> std::option::Option<ksp_job_backfill_lib::BackfillRequest> {
let id = match job_id(id) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let network = match network(network_name) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let result = ksp_job_backfill_lib::BackfillRequest::new(
id,
network,
ksp_onchain_transport_lib::HttpRoleName::new(role_name),
ksp_job_backfill_lib::BackfillCommitment::Confirmed,
scope,
100,
10,
500,
8,
std::option::Option::None,
);
return match result {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => {
assert_eq!(error.code(), ksp_job_backfill_lib::ERROR_CODE_BACKFILL_REQUEST_INVALID);
std::option::Option::None
},
};
}
#[test]
fn pre_010_external_error_codes_are_stable_unique_and_domain_scoped() {
let codes = [
(ksp_job_backfill_lib::ERROR_CODE_BACKFILL_CHECKPOINT_INVALID, "checkpoint_invalid"),
(ksp_job_backfill_lib::ERROR_CODE_BACKFILL_DISCOVERY_INVALID, "discovery_invalid"),
(ksp_job_backfill_lib::ERROR_CODE_BACKFILL_DISCOVERY_STALLED, "discovery_stalled"),
(ksp_job_backfill_lib::ERROR_CODE_BACKFILL_EXECUTION_INVALID, "execution_invalid"),
(ksp_job_backfill_lib::ERROR_CODE_BACKFILL_PERSISTENCE_INVALID, "persistence_invalid"),
(ksp_job_backfill_lib::ERROR_CODE_BACKFILL_RAW_CONVERSION_INVALID, "raw_conversion_invalid"),
(ksp_job_backfill_lib::ERROR_CODE_BACKFILL_REQUEST_INVALID, "request_invalid"),
(ksp_job_backfill_lib::ERROR_CODE_BACKFILL_RUNTIME_INVALID, "runtime_invalid"),
(ksp_job_backfill_lib::ERROR_CODE_BACKFILL_SIGNATURE_INVALID, "signature_invalid"),
];
for (index, (code, expected)) in codes.iter().enumerate() {
assert_eq!(code.domain(), "job_backfill");
assert_eq!(code.code(), *expected);
for (other_index, (other, _)) in codes.iter().enumerate() {
if index != other_index {
assert_ne!(code, other);
}
}
}
return;
}
#[test]
fn pre_010_external_request_adversarial_bounds_return_stable_codes() {
let invalid_signature = ksp_job_backfill_lib::BackfillSignature::new("0".repeat(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES));
let error = match invalid_signature {
std::result::Result::Ok(_) => return,
std::result::Result::Err(error) => error,
};
assert_eq!(error.code(), ksp_job_backfill_lib::ERROR_CODE_BACKFILL_SIGNATURE_INVALID);
let id = match job_id("backfill:hardening:bounds") {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let network = match network("devnet") {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let scope = match explicit_scope() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let result = ksp_job_backfill_lib::BackfillRequest::new(
id,
network,
ksp_onchain_transport_lib::HttpRoleName::new("history"),
ksp_job_backfill_lib::BackfillCommitment::Finalized,
scope,
0,
1,
1,
1,
std::option::Option::None,
);
let error = match result {
std::result::Result::Ok(_) => return,
std::result::Result::Err(error) => error,
};
assert_eq!(error.code(), ksp_job_backfill_lib::ERROR_CODE_BACKFILL_REQUEST_INVALID);
assert!(error.context().iter().any(|context| return context.key() == "field" && context.value() == "page_size"));
return;
}
#[test]
fn pre_010_scope_fingerprint_is_network_sensitive_and_transport_source_independent() {
let scope = match explicit_scope() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let first = match request("backfill:hardening:fingerprint:a", "devnet", "history-primary", scope.clone()) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let second = match request("backfill:hardening:fingerprint:b", "devnet", "history-secondary", scope.clone()) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let other_network = match request("backfill:hardening:fingerprint:c", "mainnet", "history-primary", scope) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert_eq!(first.scope_fingerprint(), second.scope_fingerprint());
assert_ne!(first.scope_fingerprint(), other_network.scope_fingerprint());
return;
}
#[test]
fn pre_010_debug_surfaces_redact_signature_fingerprint_and_runtime_snapshot_payloads() {
let raw_signature = signature_text();
let scope = match explicit_scope() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let backfill_request = match request("backfill:hardening:debug", "devnet", "history", scope) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let request_debug = format!("{backfill_request:?}");
assert!(!request_debug.contains(raw_signature.as_str()));
assert!(request_debug.contains("BackfillScopeFingerprint(..)"));
assert!(request_debug.contains("signature_count"));
let fingerprint_debug = format!("{:?}", backfill_request.scope_fingerprint());
assert_eq!(fingerprint_debug, "BackfillScopeFingerprint(..)");
let runtime = ksp_job_backfill_lib::BackfillJobRuntime::new(backfill_request);
let runtime = match runtime {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let handle_debug = format!("{:?}", runtime.handle());
assert!(!handle_debug.contains(raw_signature.as_str()));
assert!(handle_debug.contains("snapshot"));
assert!(handle_debug.contains("<redacted>"));
return;
}
#[test]
fn pre_010_external_runtime_handle_starts_created_and_cancellation_is_idempotent() {
let scope = match explicit_scope() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let backfill_request = match request("backfill:hardening:control", "devnet", "history", scope) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let runtime = ksp_job_backfill_lib::BackfillJobRuntime::new(backfill_request);
let runtime = match runtime {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let handle = runtime.handle();
let source = handle.snapshots();
let current = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
assert_eq!(current.state(), ksp_job_api::JobState::Created);
assert_eq!(current.snapshot().phase(), ksp_job_backfill_lib::BackfillJobPhase::Created);
assert!(!handle.is_cancellation_requested());
assert!(handle.cancel());
assert!(handle.is_cancellation_requested());
assert!(!handle.cancel());
return;
}
#[test]
fn pre_010_manifest_dependency_surface_is_exact_and_backend_neutral() {
let manifest = include_str!("../Cargo.toml");
let mut section = "";
let mut normal = std::collections::BTreeSet::new();
let mut dev = std::collections::BTreeSet::new();
for line in manifest.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') {
section = trimmed;
continue;
}
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let name = match trimmed.split_once('=') {
std::option::Option::Some((name, _)) => name.trim(),
std::option::Option::None => continue,
};
if section == "[dependencies]" {
normal.insert(name);
} else if section == "[dev-dependencies]" {
dev.insert(name);
}
}
assert_eq!(
normal,
std::collections::BTreeSet::from([
"futures-util",
"ksp-core-lib",
"ksp-job-api",
"ksp-logging-lib",
"ksp-onchain-transport-lib",
"ksp-store-lib",
"serde_json.workspace",
"sha2.workspace",
"tokio",
])
);
assert_eq!(dev, std::collections::BTreeSet::from(["tokio"]));
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }"));
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"sync\"] }"));
assert!(!manifest.contains("ksp-store-postgres-lib"));
assert!(!manifest.contains("ksp-store-api"));
return;
}
#[test]
fn pre_010_source_visibility_contract_uses_crate_root_for_shared_items() {
let root = include_str!("../src/lib.rs");
assert!(!root.contains("BackfillRuntimeControl"));
assert!(!root.contains("TerminalClaim"));
let source_contracts: [(&str, &[&str]); 3] = [
(include_str!("../src/checkpoint.rs"), &["BackfillCheckpoint", "CompletionFrontier"]),
(
include_str!("../src/request.rs"),
&["BackfillCommitment", "BackfillSignature", "BackfillScopeKind", "BackfillScope", "BackfillScopeFingerprint", "BackfillRequest"],
),
(include_str!("../src/runtime.rs"), &["BackfillJobPhase", "BackfillJobSnapshot", "BackfillJobHandle", "BackfillJobRuntime"]),
];
for (source, symbols) in source_contracts {
for symbol in symbols {
let required = format!("impl crate::{symbol}");
assert!(source.contains(required.as_str()), "shared item must use crate-root impl path: {symbol}");
let forbidden = format!("impl {symbol}");
assert!(!source.contains(forbidden.as_str()), "shared item must not use bare impl path: {symbol}");
}
}
let request = include_str!("../src/request.rs");
assert!(request.contains("crate::BackfillScopeFingerprint::from_bytes(bytes)"));
assert!(!request.contains("crate::BackfillScopeFingerprint(bytes)"));
assert!(!request.contains("return BackfillScopeFingerprint(bytes)"));
for (module, source) in [
("checkpoint", include_str!("../src/checkpoint.rs")),
("conversion", include_str!("../src/conversion.rs")),
("discovery", include_str!("../src/discovery.rs")),
("execution", include_str!("../src/execution.rs")),
("persistence", include_str!("../src/persistence.rs")),
("request", include_str!("../src/request.rs")),
("runtime", include_str!("../src/runtime.rs")),
] {
let forbidden = format!("crate::{module}::");
assert!(!source.contains(forbidden.as_str()), "internal module path bypasses crate-root façade: {forbidden}");
}
return;
}
#[test]
fn pre_010_lower_layers_have_no_dependency_return_to_job() {
for manifest in [
include_str!("../../ksp-core-lib/Cargo.toml"),
include_str!("../../ksp-logging-lib/Cargo.toml"),
include_str!("../../ksp-onchain-transport-lib/Cargo.toml"),
include_str!("../../ksp-store-api/Cargo.toml"),
include_str!("../../ksp-store-lib/Cargo.toml"),
include_str!("../../ksp-store-postgres-lib/Cargo.toml"),
] {
assert!(!manifest.contains("ksp-job-api"));
assert!(!manifest.contains("ksp-job-backfill-lib"));
}
return;
}
#[test]
fn pre_010_public_root_exposes_no_runtime_or_backend_implementation_types() {
let root = include_str!("../src/lib.rs");
for forbidden in
["pub mod ", "tokio::", "FuturesUnordered", "serde_json::", "sha2::", "ksp_store_api::", "ksp_store_postgres_lib::", "reqwest::", "tonic::"]
{
assert!(!root.contains(forbidden), "implementation/backend detail leaked into public root: {forbidden}");
}
return;
}

View File

@@ -1,10 +1,10 @@
// file: crates/ksp-job-backfill-lib/tests/release_completeness.rs
// version: 7
// version: 9
//! Completeness canaries through the `pre.009` concrete cancellation and latest-value runtime tranche.
//! Completeness canaries through the `pre.010` hardening and external-canary tranche.
#[test]
fn pre_009_production_module_inventory_is_exact() -> std::io::Result<()> {
fn pre_010_production_module_inventory_is_exact() -> std::io::Result<()> {
let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let entries = match std::fs::read_dir(source_root) {
std::result::Result::Ok(value) => value,
@@ -51,7 +51,7 @@ fn pre_009_production_module_inventory_is_exact() -> std::io::Result<()> {
}
#[test]
fn pre_009_surface_closes_concrete_cancellation_and_latest_value_runtime() {
fn pre_010_surface_closes_concrete_cancellation_and_latest_value_runtime() {
let root = include_str!("../src/lib.rs");
for required in [
"BackfillCandidate",
@@ -84,7 +84,7 @@ fn pre_009_surface_closes_concrete_cancellation_and_latest_value_runtime() {
"BACKFILL_JOB_KIND_CODE",
"ERROR_CODE_BACKFILL_RUNTIME_INVALID",
] {
assert!(root.contains(required), "required pre.009 public contract missing: {required}");
assert!(root.contains(required), "required pre.010 public contract missing: {required}");
}
for forbidden in ["tokio::", "FuturesUnordered", "watch::Receiver", "watch::Sender"] {
assert!(!root.contains(forbidden), "runtime implementation detail leaked into public root: {forbidden}");
@@ -92,3 +92,22 @@ fn pre_009_surface_closes_concrete_cancellation_and_latest_value_runtime() {
assert!(!root.contains("pub mod "));
return;
}
#[test]
fn pre_010_external_hardening_suite_is_present_and_scoped() {
let hardening = include_str!("hardening.rs");
for required in [
"pre_010_external_error_codes_are_stable_unique_and_domain_scoped",
"pre_010_external_request_adversarial_bounds_return_stable_codes",
"pre_010_scope_fingerprint_is_network_sensitive_and_transport_source_independent",
"pre_010_debug_surfaces_redact_signature_fingerprint_and_runtime_snapshot_payloads",
"pre_010_external_runtime_handle_starts_created_and_cancellation_is_idempotent",
"pre_010_manifest_dependency_surface_is_exact_and_backend_neutral",
"pre_010_source_visibility_contract_uses_crate_root_for_shared_items",
"pre_010_lower_layers_have_no_dependency_return_to_job",
"pre_010_public_root_exposes_no_runtime_or_backend_implementation_types",
] {
assert!(hardening.contains(required), "required pre.010 hardening canary missing: {required}");
}
return;
}

203
deltas/0.3.6/pre.010.md Normal file
View File

@@ -0,0 +1,203 @@
<!-- file: deltas/0.3.6/pre.010.md -->
<!-- version: 1 -->
# Delta `0.3.6-pre.010` — hardening et canaries externes
## Base requise
```text
0.3.6-pre.009-fix.002 appliquée
workspace.package.version = 0.3.6-pre.9.fix.2
```
Le gate opérateur fourni pour cette base est intégralement vert :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS / clean
python3 scripts/audit_markdown_tables.py ... PASS / clean (264 tables / 157 fichiers)
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS / aucun warning
cargo test -p ksp-job-backfill-lib PASS
unitaires 47 PASS
dependency_boundary 2 PASS
public_api 6 PASS
release_completeness 2 PASS
```
`pre.009-fix.002` ne modifiait aucune dépendance ni feature, donc aucun arbre Cargo supplémentaire n'était requis pour ce fix.
## Objectif
Matérialiser exclusivement le hardening `pre.010` du plan 027 : fermer les scénarios adversariaux, les firewalls de dépendances, la sécurité des surfaces `Debug`, la visibilité/import crate-root et l'API externe du premier vertical Backfill RAW.
La tranche ne modifie aucun code de production, n'élargit aucune API, n'ajoute aucune dépendance et ne change aucune feature. Le smoke live PostgreSQL/Devnet reste conditionnel à un environnement explicitement configuré et n'est pas inventé dans l'environnement d'assemblage.
## Canaries adversariales et erreurs stables
La nouvelle suite `tests/hardening.rs` vérifie depuis l'extérieur de la crate :
- les neuf codes d'erreur publics Backfill, leur domaine `job_backfill`, leur code exact et leur unicité ;
- une signature Base58 hostile contenant `0`, rejetée avec `ERROR_CODE_BACKFILL_SIGNATURE_INVALID` ;
- une borne `page_size = 0`, rejetée avec `ERROR_CODE_BACKFILL_REQUEST_INVALID` et contexte `field=page_size` ;
- un fingerprint identique pour deux JobId/rôles Transport différents mais un même réseau/scope ;
- un fingerprint différent lorsque le réseau logique change.
La canarie confirme donc que l'identité sémantique du scope reste indépendante du JobId et du rôle Transport, tout en restant sensible au réseau.
## Sécurité des surfaces publiques
Les canaries externes vérifient :
- `BackfillSignature` et `BackfillScopeFingerprint` restent redacted en `Debug` ;
- `BackfillRequest` ne rend pas la signature brute d'un scope explicite ;
- `BackfillJobHandle` s'appuie sur le `Debug` redacted de `JobNotification` et ne rend pas le payload snapshot ;
- la façade `src/lib.rs` n'expose aucun `pub mod`, type Tokio/Futures, détail Serde/SHA, backend Store, Reqwest ou Tonic.
Aucune URL, credential, provider, endpoint ni payload RAW n'est ajouté à l'API publique.
## Runtime externe
Le handle public est exercé sans seam privée :
```text
BackfillJobRuntime::new
BackfillJobRuntime::handle
BackfillJobHandle::snapshots
JobSnapshotSource::current
BackfillJobHandle::cancel
```
La canarie impose :
- état initial `JobState::Created` ;
- snapshot initial `BackfillJobPhase::Created` ;
- première annulation acceptée ;
- token observable comme demandé ;
- seconde annulation rejetée idempotemment.
Les races, waits RPC et drainage Store restent couverts par les unit tests `pre.009-fix.002`; `pre.010` ajoute ici la preuve par la surface publique externe.
## Firewalls de dépendances
La surface normale de `ksp-job-backfill-lib` est vérifiée exactement :
```text
futures-util
ksp-core-lib
ksp-job-api
ksp-logging-lib
ksp-onchain-transport-lib
ksp-store-lib (default-features = false)
serde_json.workspace
sha2.workspace
tokio (macros + sync)
```
La seule dev-dependency reste Tokio avec le runtime de test.
La suite refuse explicitement `ksp-store-api` et `ksp-store-postgres-lib`. Elle vérifie en outre qu'aucune des couches basses suivantes ne dépend de `ksp-job-api` ni de `ksp-job-backfill-lib` :
```text
ksp-core-lib
ksp-logging-lib
ksp-onchain-transport-lib
ksp-store-api
ksp-store-lib
ksp-store-postgres-lib
```
Cela ferme la canarie de retour de dépendance vers Job sans modifier le graphe.
## Visibilité et chemins crate-root
Une canarie source dédiée protège la correction `pre.009-fix.001/fix.002` :
- `BackfillRuntimeControl` et `TerminalClaim` restent privés et absents du crate-root ;
- les items partagés contrôlés ont leurs `impl` via `crate::Item` ;
- aucun `crate::module::Item` n'est admis dans les modules Backfill contrôlés ;
- la construction du fingerprint passe par `crate::BackfillScopeFingerprint::from_bytes(bytes)` ;
- le constructeur tuple inaccessible `crate::BackfillScopeFingerprint(bytes)` reste interdit.
Cette canarie est volontairement limitée à `ksp-job-backfill-lib` afin de verrouiller la crate active sans introduire un nouvel audit global qui signalerait rétroactivement des surfaces historiques hors mission v0.3.6.
## Tests matérialisés
Aucun unit test n'est ajouté : le total reste **47 unitaires**.
La tranche ajoute **9 canaries** dans `tests/hardening.rs` et **1 canarie** de complétude, portant l'intégration de 10 à **20 canaries** :
```text
dependency_boundary 2
public_api 6
release_completeness 3
hardening 9
```
Total attendu au gate :
```text
47 unitaires
20 canaries d'intégration
```
## Fichiers ajoutés
```text
crates/ksp-job-backfill-lib/tests/hardening.rs
deltas/0.3.6/pre.010.md
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-job-backfill-lib/tests/release_completeness.rs
docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md
docs/validation/023-V0_3_6_JOB_API_BACKFILL.md
```
Aucune suppression. Aucun manifeste de dépendances de crate n'est modifié.
## Versions d'en-tête
```text
Cargo.toml 410 -> 411
crates/ksp-job-backfill-lib/tests/release_completeness.rs 7 -> 9
docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md 20 -> 23
docs/validation/023-V0_3_6_JOB_API_BACKFILL.md 20 -> 23
```
La nouvelle canarie `tests/hardening.rs` a été révisée pendant le hardening et est livrée à `version: 4`. Le présent delta commence à `version: 1`.
## Version workspace
```text
workspace.package.version = 0.3.6-pre.10
delivery = 0.3.6-pre.010
```
## Validations 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.6
python3 -m unittest scripts.tests.test_audit_markdown_tables
```
L'environnement d'assemblage ne fournit ni Cargo, ni Rustc, ni Rustfmt. La compilation, Clippy et les 47 + 20 tests Rust restent donc à exécuter par l'opérateur.
## Gate opérateur demandé
Aucune dépendance ni feature ne change dans `pre.010`; aucun `cargo tree` n'est demandé pour cette tranche.
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.6
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-job-backfill-lib
```
Résultat attendu : **47 unitaires + 20 canaries d'intégration**, sans warning Clippy.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md -->
<!-- version: 20 -->
<!-- version: 23 -->
# Plan v0.3.6 — Job API et premier backfill RAW
@@ -504,7 +504,7 @@ Budget cible : **15-20 min**. Entrée : frontière prouvée et gate `pre.008-fix
L'annulation réutilise `JobCancellationToken` et ajoute un signal réveillable privé. Les attentes de découverte et d'hydratation pré-Store sont abandonnables ; une vérification supplémentaire intervient après hydratation et avant soumission Store. Dès que `persist_backfill_hydration` est appelée, l'écriture atomique n'est plus sélectionnée contre l'annulation : toute opération Store déjà soumise est drainée jusqu'à un résultat connu. L'exécuteur arrête ensuite les nouvelles admissions, draine l'in-flight et publie frontier/checkpoint sûrs. Une arbitration atomique sépare `ACTIVE`, demande d'annulation et états terminaux : une annulation acceptée avant la décision terminale gagne sur une complétion normale, une complétion déjà réclamée refuse l'annulation tardive, et une erreur fatale peut remplacer une annulation pendante afin de ne pas masquer un échec Transport/Store.
Le snapshot complet reste sûr : phase, catégorie de scope, borne de découverte, compteurs d'admission/fin, dispositions entité/observation, missing/conflits/annulations/trous, maximum in-flight, préfixe contigu, checkpoint et éventuel code d'erreur stable ; aucune URL, endpoint/provider, payload RAW ou message fournisseur n'y entre. La tranche matérialise 47 tests unitaires et 10 canaries d'intégration au total, dont listener lent/multi-listeners, rétention terminale, courses terminales, attente pré-Store longue, attente de page RPC pendante et drainage après annulation. Le gate opérateur exécute ces 47 + 10 tests avec succès, mais Clippy révèle deux réexports `pub(crate)` non consommés via la façade crate-root et une canarie générique à type non utilisé ; l'audit manuel montre en outre que plusieurs usages intra-module des items partagés `pub`/`pub(crate)` de la nouvelle crate contournent `crate::`, en contradiction avec `RUST-IMPORT-009`, `RUST-IMPORT-012` et `RUST-API-007`. `pre.010` reste fermé jusqu'au gate vert du fix.
Le snapshot complet reste sûr : phase, catégorie de scope, borne de découverte, compteurs d'admission/fin, dispositions entité/observation, missing/conflits/annulations/trous, maximum in-flight, préfixe contigu, checkpoint et éventuel code d'erreur stable ; aucune URL, endpoint/provider, payload RAW ou message fournisseur n'y entre. La tranche matérialise 47 tests unitaires et 10 canaries d'intégration au total, dont listener lent/multi-listeners, rétention terminale, courses terminales, attente pré-Store longue, attente de page RPC pendante et drainage après annulation. Le gate opérateur exécute ces 47 + 10 tests avec succès, mais Clippy révèle deux réexports `pub(crate)` non consommés via la façade crate-root et une canarie générique à type non utilisé ; l'audit manuel montre en outre que plusieurs usages intra-module des items partagés `pub`/`pub(crate)` de la nouvelle crate contournent `crate::`, en contradiction avec `RUST-IMPORT-009`, `RUST-IMPORT-012` et `RUST-API-007`. `pre.009-fix.002` est désormais validé par le gate opérateur : audits, `cargo check`, Clippy et 47 unitaires + 10 canaries sont verts. `pre.010` peut donc ouvrir le hardening externe.
#### `pre.009-fix.001` — Normalisation crate-root des items partagés et visibilité runtime
@@ -514,16 +514,18 @@ Le fix réconcilie l'intégralité de `ksp-job-backfill-lib` avec les règles de
#### `pre.009-fix.002` — Construction privée du fingerprint via façade crate-root
**Statut : matérialisé ; gate opérateur à rejouer.**
**Statut : alisé ; gate opérateur intégralement vert.**
Le gate de `pre.009-fix.001` révèle une erreur `E0423` : le type public `BackfillScopeFingerprint` est correctement référencé via `crate::BackfillScopeFingerprint`, mais son constructeur de tuple struct n'est pas visible à travers le réexport crate-root puisque son champ `[u8; 32]` reste privé. Le correctif conserve le champ privé et la discipline crate-root : l'initialisation des octets est confinée dans un constructeur associé privé `from_bytes` défini dans `impl crate::BackfillScopeFingerprint`, puis le calcul de fingerprint appelle `crate::BackfillScopeFingerprint::from_bytes(bytes)`. Aucun contrat public, fingerprint, hash, dépendance ou feature ne change.
### `pre.010` — Hardening et canaries externes
**Statut : planifié.**
**Statut : réalisé ; gate opérateur à rejouer.**
Budget cible : **15-20 min**. Entrée : vertical déterministe complet. Fermer scénarios adversariaux, API externe, dépendances et sécurité ; exécuter la preuve live si justifiée et disponible. Sortie : aucun défaut fonctionnel ou firewall ouvert.
La tranche n'élargit aucune API de production et ne modifie aucune dépendance : elle ajoute une suite d'intégration externe `tests/hardening.rs` et renforce la canarie de complétude. Les preuves couvrent les neuf codes d'erreur publics et leur unicité, les bornes/adversaires de requête, l'identité sémantique du fingerprint `(réseau + scope)` indépendante du JobId/rôle Transport, les redactions `Debug`, l'état initial et l'idempotence de l'annulation via le handle public, la surface exacte des dépendances normales/dev, l'absence de backend Store direct et de retour des couches basses vers Job, les chemins crate-root exigés par RUST-IMPORT-009/010/012 et l'absence de types Tokio/Store backend/HTTP dans la façade publique. Le gate matérialisé attendu reste à 47 unitaires et passe de 10 à 20 canaries d'intégration. Le smoke live reste conditionnel et n'est pas requis par cette tranche faute d'environnement PostgreSQL explicitement fourni.
### `pre.011` — Gate technique final
**Statut : planifié.**

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/023-V0_3_6_JOB_API_BACKFILL.md -->
<!-- version: 20 -->
<!-- version: 23 -->
# Validation v0.3.6 — Job API et premier backfill RAW
@@ -189,29 +189,29 @@ Aucune entrée absolue, traversée, avec séparateur inversé ou lien symbolique
## 13. Annulation et résultats terminaux
`pre.009` matérialise le runtime concret, le canal latest-value et les chemins d'annulation/drainage. Le gate opérateur exécute avec succès les 47 unitaires et 10 canaries couvrant l'attente RPC de découverte, une future pré-Store longue, le drainage des candidats déjà admis, les listeners lents/indépendants, la rétention terminale et les arbitrations terminales. La qualité Rust reste néanmoins bloquée : `pre.009-fix.001` corrige les chemins crate-root/visibilités et le warning Clippy de la canarie publique, puis son gate révèle `E0423` sur la construction du fingerprint public opaque ; `pre.009-fix.002` corrige ce dernier point et reste à valider.
`pre.009` matérialise le runtime concret, le canal latest-value et les chemins d'annulation/drainage. Le gate opérateur exécute avec succès les 47 unitaires et 10 canaries couvrant l'attente RPC de découverte, une future pré-Store longue, le drainage des candidats déjà admis, les listeners lents/indépendants, la rétention terminale et les arbitrations terminales. La qualité Rust reste néanmoins bloquée : `pre.009-fix.001` corrige les chemins crate-root/visibilités et le warning Clippy de la canarie publique, puis son gate révèle `E0423` sur la construction du fingerprint public opaque ; `pre.009-fix.002` corrige ce dernier point ; son gate opérateur est désormais intégralement vert : audits, `cargo check`, Clippy sans warning, 47 unitaires et 10 canaries.
- [ ] Annulation idempotente et état `Cancelling` observable.
- [ ] Admissions arrêtées après observation du token.
- [ ] Attente de page et hydratation RPC longues abandonnables.
- [ ] Persistance déjà soumise drainée jusqu'à un résultat connu.
- [ ] Course complétion contre annulation déterministe.
- [ ] Course erreur fatale contre annulation déterministe.
- [ ] Missing seul donne `Completed` avec résultat `Partial`.
- [ ] Conflit, conversion ou infrastructure donnent `Failed`.
- [ ] Checkpoint final cohérent avec le dernier préfixe durable.
- [X] Annulation idempotente et état `Cancelling` observable au gate `pre.009-fix.002`.
- [X] Admissions arrêtées après observation du token au gate `pre.009-fix.002`.
- [X] Attente de page et hydratation RPC longues abandonnables au gate `pre.009-fix.002`.
- [X] Persistance déjà soumise drainée jusqu'à un résultat connu au gate `pre.009-fix.002`.
- [X] Course complétion contre annulation déterministe au gate `pre.009-fix.002`.
- [X] Course erreur fatale contre annulation déterministe au gate `pre.009-fix.002`.
- [X] Missing seul donne `Completed` avec résultat `Partial`, couvert par le runtime/exécuteur validé `pre.009-fix.002`.
- [X] Conflit, conversion ou infrastructure donnent `Failed`, couvert par le runtime/exécuteur validé `pre.009-fix.002`.
- [X] Checkpoint final cohérent avec le dernier préfixe durable au gate `pre.009-fix.002`.
## 14. Firewalls et qualité Rust
- [X] `ksp-job-api` dépend exactement de Core et n'a aucune feature, dev-dependency ou build-dependency.
- [X] `ksp-job-backfill-lib` ne dépend pas de Config, App, Wallet, Program, Interface ou backend Store ; `ksp-store-lib` est consommée avec `default-features = false`.
- [ ] Aucun retour de dépendance vers Job depuis les couches basses.
- [ ] Aucun retour de dépendance vers Job depuis les couches basses ; canarie externe `pre.010` matérialisée.
- [X] Versions/features auditées : aucune nouvelle version externe ; `sha2`, `futures-util` et `tokio` réutilisent les entrées workspace existantes. `futures-util` reste un détail normal privé depuis `pre.008` et `pre.009` promeut Tokio en dépendance normale privée limitée à `macros` + `sync`; aucun type Tokio n'est exposé.
- [X] `pre.005` matérialise 11 tests unitaires et 6 canaries dintégration ; le gate opérateur les exécute tous avec succès.
- [X] `pre.006` matérialise 19 tests unitaires et 7 canaries dintégration ; le gate opérateur les exécute tous avec succès, avec uniquement le warning Clippy corrigé par `pre.006-fix.001`.
- [X] `pre.009` : `cargo check --workspace` vert et 47 unitaires + 10 canaries vertes ; Clippy signale 3 warnings de normalisation/API de test, corrigés par `pre.009-fix.001`; le gate de ce fix révèle ensuite `E0423`, corrigé par `pre.009-fix.002` et encore à revalider.
- [X] `pre.009` : après `fix.001` puis `fix.002`, audits, `cargo check`, Clippy sans warning et 47 unitaires + 10 canaries sont verts sur `0.3.6-pre.9.fix.2`.
- [X] Audit manuel `pre.009-fix.001` : les items partagés de `ksp-job-backfill-lib` passent par `crate::Item`; `BackfillRuntimeControl`/`TerminalClaim` restent privés et les unit tests runtime utilisent `super::Item`, conformément à RUST-IMPORT-009/012 et RUST-API-007.
- [ ] `pre.009-fix.002` : `E0423` corrigé en confinant la construction du champ privé dans `BackfillScopeFingerprint::from_bytes`; gate opérateur Rust à rejouer.
- [X] `pre.009-fix.002` : `E0423` corrigé en confinant la construction du champ privé dans `BackfillScopeFingerprint::from_bytes`; gate opérateur intégralement vert.
- [ ] `cargo fmt --all -- --check` vert.
- [X] `scripts/audit_rust_workspace_rules.py` vert sur `pre.005` dans l'environnement d'assemblage et dans le gate opérateur.
- [X] `scripts/audit_markdown_tables.py` vert sur `pre.005` dans le gate opérateur (264 tables / 147 fichiers, delta inclus).
@@ -221,6 +221,20 @@ Aucune entrée absolue, traversée, avec séparateur inversé ou lien symbolique
- [ ] `cargo test --workspace --all-targets --all-features` vert.
- [ ] `cargo tree -d` examiné et doublons justifiés.
### Hardening externe `pre.010`
La tranche ajoute 9 canaries externes et une canarie de complétude, portant le total d'intégration attendu à 20 sans modifier les 47 unitaires. Le gate Rust reste à rejouer.
- [ ] Neuf codes d'erreur publics Backfill stables, uniques et dans le domaine `job_backfill`; canarie externe matérialisée.
- [ ] Signature Base58 hostile et bornes de requête invalides retournent les codes stables attendus ; canarie externe matérialisée.
- [ ] Fingerprint sensible au réseau mais indépendant du JobId et du rôle Transport ; canarie externe matérialisée.
- [ ] `Debug` des signatures/fingerprint/requête/handle ne révèle ni signature brute ni payload snapshot ; canarie externe matérialisée.
- [ ] Handle public créé en état `Created`, annulation acceptée une fois puis idempotemment rejetée ; canarie externe matérialisée.
- [ ] Surface de dépendances exacte et backend-neutral, avec Store façade `default-features = false`; canarie externe matérialisée.
- [ ] Surface source Backfill conforme aux chemins crate-root pour les items partagés et sans `crate::module::Item`; canarie externe matérialisée.
- [ ] Aucun retour des couches Core/Logging/Transport/Store vers Job ; canarie externe matérialisée.
- [ ] Façade publique sans fuite Tokio/Futures/Serde/Store backend/HTTP ; canarie externe matérialisée.
## 15. Preuves d'intégration et fermeture
- [X] Fake Transport et fake Store déterministes sans backend direct exécutés avec succès au gate `pre.007`; fake processor concurrent `pre.008-fix.001` exécuté avec succès.