0.0.2-alpha.2

This commit is contained in:
2026-09-28 16:16:55 +02:00
parent 84c01eb8e8
commit bdbd6a51a4
15 changed files with 1368 additions and 72 deletions

View File

@@ -1,11 +1,12 @@
# file: Cargo.toml
# version: 18
# version: 19
[workspace]
resolver = "3"
members = [
"crates/knowledge-domain-lib",
"crates/knowledge-git-api",
"crates/knowledge-git-lib",
"crates/knowledge-sync-api",
"crates/knowledge-web-lib",
"crates/knowledge-pgr-server",
@@ -14,7 +15,7 @@ members = [
]
[workspace.package]
version = "0.0.2-alpha.1.fix.1"
version = "0.0.2-alpha.2"
edition = "2024"
publish = false
repository = "https://git.sasedev.com/Sasedev/sasedev-knowledge"

View File

@@ -1,5 +1,5 @@
<!-- file: README.md -->
<!-- version: 10 -->
<!-- version: 11 -->
# SaseDev Knowledge
@@ -11,9 +11,9 @@ Le dépôt applicatif retenu est `sasedev-knowledge`. Le nom court du projet et
La version courante est définie par [`VERSION`](VERSION).
`0.0.1` est la première stable de fondation de SaseDev Knowledge. `0.0.2-alpha.1` ouvre maintenant la release Git/synchronisation : elle adopte explicitement PGR `v0.1.3`, ferme l'étude du backend Git et fixe le plan avant toute implémentation fonctionnelle.
`0.0.1` est la première stable de fondation de SaseDev Knowledge. `0.0.2-alpha.2` matérialise maintenant le premier backend Git réel après validation du cadrage et de son correctif documentaire.
Cette première tranche de `0.0.2` ne modifie ni les crates Rust ni la baseline Web. L'implémentation Git réelle commencera dans une tranche ultérieure après validation opérateur de ce cadrage.
`knowledge-git-lib` possède la configuration physique des sources, la racine d'état généré, la dérivation de clés de stockage sûres, l'exécution contrôlée du processus `git` sans shell et l'initialisation/inspection minimale des mirrors. Refresh, résolution de révision et snapshot restent réservés à la tranche suivante.
## Topologie produit de fondation
@@ -33,7 +33,7 @@ PostgreSQL est mutualisable et constitue la baseline retenue pour l'état transv
Chaque application Web doit pouvoir exposer sa propre surface d'administration protégée pour son domaine. Les chemins HTTP exacts ne sont pas encore figés. Une éventuelle quatrième application d'administration pourra agréger des fonctions transversales, mais elle ne remplacera pas les contrôles d'autorisation propres aux trois serveurs.
Le workspace Rust matérialise sept packages : les trois frontières de fondation `knowledge-domain-lib`, `knowledge-git-api` et `knowledge-sync-api`, la composition partagée `knowledge-web-lib`, puis les trois launchers `knowledge-pgr-server`, `knowledge-howtos-server` et `knowledge-snippets-server`. Les deux crates `-api` restent passives et indépendantes d'Actix, Gitea et PostgreSQL.
Le workspace Rust matérialise huit packages : les trois frontières de fondation `knowledge-domain-lib`, `knowledge-git-api` et `knowledge-sync-api`, le backend `knowledge-git-lib`, la composition partagée `knowledge-web-lib`, puis les trois launchers `knowledge-pgr-server`, `knowledge-howtos-server` et `knowledge-snippets-server`. Les deux crates `-api` restent passives et indépendantes d'Actix, Gitea et PostgreSQL.
## Frontend de référence et rendu Maud

View File

@@ -1,5 +1,5 @@
<!-- file: RULES.md -->
<!-- version: 10 -->
<!-- version: 11 -->
# Règles SaseDev Knowledge
@@ -127,7 +127,9 @@ La synchronisation générique MUST disposer d'une voie indépendante des webhoo
Pour `0.0.2`, la première implémentation générique retenue utilise un processus `git` contrôlé derrière `knowledge-git-api`. Cette décision est locale à Knowledge et reste remplaçable derrière le contrat.
La configuration physique des sources Git et la matérialisation locale appartiennent à `knowledge-git-lib` lorsqu'elle sera créée. Les copies générées MUST résider sous une racine d'état explicitement fournie au backend, hors du dépôt applicatif et des dépôts éditoriaux. Ni une URL remote ni un `SourceId` brut ne doivent devenir directement un chemin de stockage.
La configuration physique des sources Git et la matérialisation locale appartiennent à `knowledge-git-lib`, matérialisée en `0.0.2-alpha.2`. Les copies générées MUST résider sous une racine d'état explicitement fournie au backend, hors du dépôt applicatif et des dépôts éditoriaux. Ni une URL remote ni un `SourceId` brut ne doivent devenir directement un chemin de stockage.
`knowledge-git-lib` dérive une clé de stockage sûre à partir des octets UTF-8 du `SourceId` et ne réutilise jamais directement l'identifiant logique ou l'URL comme composant de chemin. Le backend MUST invoquer l'exécutable Git directement sans shell, avec arguments séparés et prompts interactifs désactivés. Les erreurs publiques du runner MUST NOT recopier les arguments de commande, l'URL source, stderr ou des credentials.
La racine d'état Git n'est pas un stockage métier. En `0.0.2`, elle est possédée exclusivement par une instance de backend/processus ; un partage concurrent inter-processus reste différé tant qu'un verrouillage dédié n'est pas matérialisé.

View File

@@ -1 +1 @@
0.0.2-alpha.1.fix.1
0.0.2-alpha.2

View File

@@ -0,0 +1,15 @@
# file: crates/knowledge-git-lib/Cargo.toml
# version: 1
[package]
name = "knowledge-git-lib"
version.workspace = true
edition.workspace = true
publish.workspace = true
repository.workspace = true
[dependencies]
knowledge-domain-lib.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,379 @@
// file: crates/knowledge-git-lib/src/backend.rs
// version: 1
//! Physical source configuration, generated-state layout and controlled Git process runner.
/// Explicit physical configuration for one controlled Git backend instance.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GitBackendConfig {
git_executable: std::path::PathBuf,
state_root: std::path::PathBuf,
}
impl crate::GitBackendConfig {
/// Build a backend configuration using `git` from the process environment.
pub fn new(state_root: std::path::PathBuf) -> Result<crate::GitBackendConfig, crate::GitBackendError> {
return crate::GitBackendConfig::with_git_executable(state_root, std::path::PathBuf::from("git"));
}
/// Build a backend configuration with an explicit Git executable path.
pub fn with_git_executable(
state_root: std::path::PathBuf,
git_executable: std::path::PathBuf,
) -> Result<crate::GitBackendConfig, crate::GitBackendError> {
if state_root.as_os_str().is_empty() {
return Err(crate::GitBackendError::EmptyStateRoot);
}
if git_executable.as_os_str().is_empty() {
return Err(crate::GitBackendError::EmptyGitExecutable);
}
return Ok(crate::GitBackendConfig { git_executable, state_root });
}
/// Return the configured Git executable path.
pub fn git_executable(&self) -> &std::path::Path {
return self.git_executable.as_path();
}
/// Return the exclusive generated-state root owned by this backend instance.
pub fn state_root(&self) -> &std::path::Path {
return self.state_root.as_path();
}
/// Return the empty controlled hooks directory used to disable repository hooks.
pub fn hooks_path(&self) -> std::path::PathBuf {
return self.state_root.join("hooks-disabled");
}
/// Return the directory that contains controlled bare mirrors.
pub fn repositories_path(&self) -> std::path::PathBuf {
return self.state_root.join("repositories");
}
/// Return the directory reserved for revision-addressed snapshots.
pub fn snapshots_path(&self) -> std::path::PathBuf {
return self.state_root.join("snapshots");
}
}
/// Coarse physical source kind used by the backend.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GitSourceKind {
/// Source is a path on the local filesystem.
Local,
/// Source is a Git remote location interpreted by the Git executable.
Remote,
}
/// Physical Git source configuration associated with one logical source identifier.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GitSourceConfig {
location: std::ffi::OsString,
source_id: knowledge_domain_lib::SourceId,
source_kind: crate::GitSourceKind,
}
impl crate::GitSourceConfig {
/// Build a local Git source configuration.
pub fn local(
source_id: knowledge_domain_lib::SourceId,
path: std::path::PathBuf,
) -> Result<crate::GitSourceConfig, crate::GitBackendError> {
if path.as_os_str().is_empty() {
return Err(crate::GitBackendError::EmptySourceLocation);
}
return Ok(crate::GitSourceConfig {
location: path.into_os_string(),
source_id,
source_kind: crate::GitSourceKind::Local,
});
}
/// Build a remote Git source configuration without interpreting provider-specific syntax.
pub fn remote(source_id: knowledge_domain_lib::SourceId, location: &str) -> Result<crate::GitSourceConfig, crate::GitBackendError> {
if location.trim().is_empty() {
return Err(crate::GitBackendError::EmptySourceLocation);
}
return Ok(crate::GitSourceConfig {
location: std::ffi::OsString::from(location),
source_id,
source_kind: crate::GitSourceKind::Remote,
});
}
/// Return the logical source identifier associated with this physical source.
pub fn source_id(&self) -> &knowledge_domain_lib::SourceId {
return &self.source_id;
}
/// Return whether the source is local or remote.
pub fn source_kind(&self) -> crate::GitSourceKind {
return self.source_kind;
}
}
/// Path-safe storage key derived from a logical Knowledge source identifier.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct GitSourceKey(String);
impl crate::GitSourceKey {
/// Derive a reversible collision-free key from the UTF-8 bytes of a logical source identifier.
pub fn from_source_id(source_id: &knowledge_domain_lib::SourceId) -> crate::GitSourceKey {
let mut value = String::from("source-");
for byte in source_id.as_str().as_bytes() {
value.push_str(format!("{:02x}", *byte).as_str());
}
return crate::GitSourceKey(value);
}
/// Return the path-safe key text.
pub fn as_str(&self) -> &str {
return self.0.as_str();
}
}
/// Result of ensuring that a controlled mirror exists.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GitMirrorStatus {
/// The mirror was created by the current call.
Created,
/// A valid existing mirror was reused.
Existing,
}
/// Description of one initialized or already existing controlled mirror.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GitMirror {
repository_path: std::path::PathBuf,
source_key: crate::GitSourceKey,
status: crate::GitMirrorStatus,
}
impl crate::GitMirror {
/// Return the controlled bare-repository path.
pub fn repository_path(&self) -> &std::path::Path {
return self.repository_path.as_path();
}
/// Return the path-safe key associated with the logical source identifier.
pub fn source_key(&self) -> &crate::GitSourceKey {
return &self.source_key;
}
/// Return whether the mirror was newly created or already existed.
pub fn status(&self) -> crate::GitMirrorStatus {
return self.status;
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct GitCommandSpec {
arguments: Vec<std::ffi::OsString>,
executable: std::path::PathBuf,
hooks_path: std::path::PathBuf,
operation: crate::GitOperation,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct GitCommandOutput {
stdout: Vec<u8>,
}
trait GitCommandRunner {
fn run(&self, specification: &GitCommandSpec) -> Result<GitCommandOutput, crate::GitBackendError>;
}
struct SystemGitRunner;
impl GitCommandRunner for SystemGitRunner {
fn run(&self, specification: &GitCommandSpec) -> Result<GitCommandOutput, crate::GitBackendError> {
let mut command = std::process::Command::new(&specification.executable);
command.args(&specification.arguments);
command.env("GIT_TERMINAL_PROMPT", "0");
command.env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES");
command.env_remove("GIT_COMMON_DIR");
command.env_remove("GIT_CONFIG_PARAMETERS");
command.env_remove("GIT_DIR");
command.env_remove("GIT_INDEX_FILE");
command.env_remove("GIT_OBJECT_DIRECTORY");
command.env_remove("GIT_WORK_TREE");
command.env("GIT_CONFIG_COUNT", "1");
command.env("GIT_CONFIG_KEY_0", "core.hooksPath");
command.env("GIT_CONFIG_VALUE_0", &specification.hooks_path);
let output_result = command.output();
let output = match output_result {
Ok(value) => value,
Err(_) => {
return Err(crate::GitBackendError::GitUnavailable {
operation: specification.operation,
});
}
};
if !output.status.success() {
return Err(crate::GitBackendError::GitCommandFailed {
operation: specification.operation,
exit_code: output.status.code(),
});
}
return Ok(GitCommandOutput { stdout: output.stdout });
}
}
/// Controlled Git backend that owns local mirror initialization and inspection.
pub struct GitBackend {
config: crate::GitBackendConfig,
runner: Box<dyn GitCommandRunner>,
}
impl crate::GitBackend {
/// Create a backend using the configured real Git executable.
pub fn new(config: crate::GitBackendConfig) -> crate::GitBackend {
return crate::GitBackend {
config,
runner: Box::new(SystemGitRunner),
};
}
#[cfg(test)]
fn with_runner(config: crate::GitBackendConfig, runner: Box<dyn GitCommandRunner>) -> crate::GitBackend {
return crate::GitBackend { config, runner };
}
/// Return the immutable backend configuration.
pub fn config(&self) -> &crate::GitBackendConfig {
return &self.config;
}
/// Derive the controlled mirror path without interpolating the raw source identifier.
pub fn repository_path(&self, source_id: &knowledge_domain_lib::SourceId) -> std::path::PathBuf {
let source_key = crate::GitSourceKey::from_source_id(source_id);
return self.config.repositories_path().join(format!("{}.git", source_key.as_str()));
}
/// Inspect one mirror when it already exists without creating it.
pub fn inspect_mirror(&self, source_id: &knowledge_domain_lib::SourceId) -> Result<Option<crate::GitMirror>, crate::GitBackendError> {
let source_key = crate::GitSourceKey::from_source_id(source_id);
let repository_path = self.repository_path(source_id);
if !repository_path.exists() {
return Ok(None);
}
let inspection_result = self.inspect_bare_repository(&repository_path);
match inspection_result {
Ok(()) => {
return Ok(Some(crate::GitMirror {
repository_path,
source_key,
status: crate::GitMirrorStatus::Existing,
}));
}
Err(error) => return Err(error),
}
}
/// Ensure that one configured source has a valid controlled bare mirror.
pub fn ensure_mirror(&self, source: &crate::GitSourceConfig) -> Result<crate::GitMirror, crate::GitBackendError> {
let preparation_result = self.prepare_state_directories();
match preparation_result {
Ok(()) => (),
Err(error) => return Err(error),
}
let source_key = crate::GitSourceKey::from_source_id(source.source_id());
let repository_path = self.repository_path(source.source_id());
if repository_path.exists() {
let inspection_result = self.inspect_bare_repository(&repository_path);
match inspection_result {
Ok(()) => {
return Ok(crate::GitMirror {
repository_path,
source_key,
status: crate::GitMirrorStatus::Existing,
});
}
Err(error) => return Err(error),
}
}
let clone_result = self.clone_mirror(source, &repository_path);
match clone_result {
Ok(()) => (),
Err(error) => return Err(error),
}
let inspection_result = self.inspect_bare_repository(&repository_path);
match inspection_result {
Ok(()) => {
return Ok(crate::GitMirror {
repository_path,
source_key,
status: crate::GitMirrorStatus::Created,
});
}
Err(error) => return Err(error),
}
}
fn prepare_state_directories(&self) -> Result<(), crate::GitBackendError> {
for path in [self.config.hooks_path(), self.config.repositories_path(), self.config.snapshots_path()] {
let create_result = std::fs::create_dir_all(path);
if create_result.is_err() {
return Err(crate::GitBackendError::StatePreparationFailed);
}
}
return Ok(());
}
fn clone_mirror(&self, source: &crate::GitSourceConfig, repository_path: &std::path::Path) -> Result<(), crate::GitBackendError> {
let mut arguments = vec![std::ffi::OsString::from("clone"), std::ffi::OsString::from("--mirror")];
if source.source_kind == crate::GitSourceKind::Local {
arguments.push(std::ffi::OsString::from("--no-local"));
}
arguments.push(std::ffi::OsString::from("--"));
arguments.push(source.location.clone());
arguments.push(repository_path.as_os_str().to_os_string());
let specification = GitCommandSpec {
arguments,
executable: self.config.git_executable().to_path_buf(),
hooks_path: self.config.hooks_path(),
operation: crate::GitOperation::CloneMirror,
};
let run_result = self.runner.run(&specification);
match run_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
fn inspect_bare_repository(&self, repository_path: &std::path::Path) -> Result<(), crate::GitBackendError> {
let specification = GitCommandSpec {
arguments: vec![
std::ffi::OsString::from("--git-dir"),
repository_path.as_os_str().to_os_string(),
std::ffi::OsString::from("rev-parse"),
std::ffi::OsString::from("--is-bare-repository"),
],
executable: self.config.git_executable().to_path_buf(),
hooks_path: self.config.hooks_path(),
operation: crate::GitOperation::InspectMirror,
};
let run_result = self.runner.run(&specification);
let output = match run_result {
Ok(value) => value,
Err(error) => return Err(error),
};
let stdout_result = std::str::from_utf8(&output.stdout);
let stdout = match stdout_result {
Ok(value) => value.trim(),
Err(_) => {
return Err(crate::GitBackendError::UnexpectedGitOutput {
operation: crate::GitOperation::InspectMirror,
});
}
};
if stdout != "true" {
return Err(crate::GitBackendError::InvalidMirror);
}
return Ok(());
}
}
#[cfg(test)]
#[path = "../unit_tests/backend.rs"]
mod tests;

View File

@@ -0,0 +1,78 @@
// file: crates/knowledge-git-lib/src/error.rs
// version: 1
//! Recoverable errors for controlled Git process execution.
/// Git operation category safe to expose in diagnostics.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GitOperation {
/// Clone one configured source into a controlled mirror.
CloneMirror,
/// Inspect whether a controlled repository is a valid bare mirror.
InspectMirror,
}
impl std::fmt::Display for crate::GitOperation {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
crate::GitOperation::CloneMirror => return formatter.write_str("clone mirror"),
crate::GitOperation::InspectMirror => return formatter.write_str("inspect mirror"),
}
}
}
/// Recoverable error returned by the controlled Git backend.
///
/// Process failures intentionally retain only an operation category and exit code. Source URLs,
/// command arguments, stderr and credentials are not embedded in this public error.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GitBackendError {
/// The configured Git executable path is empty.
EmptyGitExecutable,
/// The configured state root is empty.
EmptyStateRoot,
/// A local source path or remote source string is empty.
EmptySourceLocation,
/// The backend could not prepare its generated-state directories.
StatePreparationFailed,
/// The Git executable could not be started for the requested operation.
GitUnavailable {
/// Operation that could not start.
operation: crate::GitOperation,
},
/// Git returned a non-success status for the requested operation.
GitCommandFailed {
/// Operation that failed.
operation: crate::GitOperation,
/// Process exit code when the operating system exposed one.
exit_code: Option<i32>,
},
/// An existing controlled repository is not a valid bare mirror.
InvalidMirror,
/// Git returned output that does not satisfy the backend contract.
UnexpectedGitOutput {
/// Operation whose output could not be validated.
operation: crate::GitOperation,
},
}
impl std::fmt::Display for crate::GitBackendError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
crate::GitBackendError::EmptyGitExecutable => return formatter.write_str("git executable must not be empty"),
crate::GitBackendError::EmptyStateRoot => return formatter.write_str("git state root must not be empty"),
crate::GitBackendError::EmptySourceLocation => return formatter.write_str("git source location must not be empty"),
crate::GitBackendError::StatePreparationFailed => return formatter.write_str("git state directories could not be prepared"),
crate::GitBackendError::GitUnavailable { operation } => return write!(formatter, "git is unavailable while attempting to {operation}"),
crate::GitBackendError::GitCommandFailed { operation, exit_code } => {
return write!(formatter, "git failed while attempting to {operation}; exit code {exit_code:?}");
}
crate::GitBackendError::InvalidMirror => return formatter.write_str("controlled git repository is not a valid bare mirror"),
crate::GitBackendError::UnexpectedGitOutput { operation } => {
return write!(formatter, "git returned unexpected output while attempting to {operation}");
}
}
}
}
impl std::error::Error for crate::GitBackendError {}

View File

@@ -0,0 +1,29 @@
// file: crates/knowledge-git-lib/src/lib.rs
// version: 1
//! Controlled Git process backend for Knowledge content sources.
//!
//! This crate owns physical Git source configuration, generated-state layout and process execution.
//! It deliberately contains no forge API, persistence, scheduler or application orchestration.
mod backend;
mod error;
/// Controlled Git backend that owns local mirror initialization and inspection.
pub use self::backend::GitBackend;
/// Explicit physical configuration for one controlled Git backend instance.
pub use self::backend::GitBackendConfig;
/// Description of one initialized or already existing controlled mirror.
pub use self::backend::GitMirror;
/// Result of ensuring that a controlled mirror exists.
pub use self::backend::GitMirrorStatus;
/// Path-safe storage key derived from a logical Knowledge source identifier.
pub use self::backend::GitSourceKey;
/// Physical Git source configuration associated with one logical source identifier.
pub use self::backend::GitSourceConfig;
/// Coarse physical source kind used by the backend.
pub use self::backend::GitSourceKind;
/// Recoverable error returned by the controlled Git backend.
pub use self::error::GitBackendError;
/// Git operation category exposed by recoverable process errors without leaking command arguments.
pub use self::error::GitOperation;

View File

@@ -0,0 +1,283 @@
// file: crates/knowledge-git-lib/unit_tests/backend.rs
// version: 1
#[derive(Clone)]
struct RecordingRunner {
calls: std::rc::Rc<std::cell::RefCell<Vec<super::GitCommandSpec>>>,
failure: Option<crate::GitBackendError>,
inspection_stdout: Vec<u8>,
}
impl RecordingRunner {
fn successful() -> RecordingRunner {
return RecordingRunner {
calls: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
failure: None,
inspection_stdout: b"true\n".to_vec(),
};
}
fn failing(error: crate::GitBackendError) -> RecordingRunner {
return RecordingRunner {
calls: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
failure: Some(error),
inspection_stdout: b"true\n".to_vec(),
};
}
fn with_inspection_stdout(value: &[u8]) -> RecordingRunner {
return RecordingRunner {
calls: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
failure: None,
inspection_stdout: value.to_vec(),
};
}
}
impl super::GitCommandRunner for RecordingRunner {
fn run(&self, specification: &super::GitCommandSpec) -> Result<super::GitCommandOutput, crate::GitBackendError> {
self.calls.borrow_mut().push(specification.clone());
if let Some(error) = self.failure {
return Err(error);
}
if specification.operation == crate::GitOperation::InspectMirror {
return Ok(super::GitCommandOutput {
stdout: self.inspection_stdout.clone(),
});
}
return Ok(super::GitCommandOutput { stdout: Vec::new() });
}
}
fn source_id(value: &str) -> Option<knowledge_domain_lib::SourceId> {
let result = knowledge_domain_lib::SourceId::new(value);
match result {
Ok(source_id) => return Some(source_id),
Err(_) => return None,
}
}
fn test_root(label: &str) -> std::path::PathBuf {
let mut path = std::env::temp_dir();
path.push(format!("sasedev-knowledge-git-lib-{label}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&path);
return path;
}
#[test]
fn backend_configuration_requires_non_empty_paths() {
assert_eq!(crate::GitBackendConfig::new(std::path::PathBuf::new()), Err(crate::GitBackendError::EmptyStateRoot));
assert_eq!(
crate::GitBackendConfig::with_git_executable(test_root("empty-executable"), std::path::PathBuf::new()),
Err(crate::GitBackendError::EmptyGitExecutable)
);
return;
}
#[test]
fn source_key_never_interpolates_raw_source_identifier_as_a_path() {
let source_result = source_id("../pgr/credential:value");
assert!(source_result.is_some());
let source_id = match source_result {
Some(value) => value,
None => return,
};
let key = crate::GitSourceKey::from_source_id(&source_id);
assert!(key.as_str().starts_with("source-"));
assert!(key.as_str().chars().all(|character| character.is_ascii_alphanumeric() || character == '-'));
assert!(!key.as_str().contains("pgr"));
assert!(!key.as_str().contains(".."));
return;
}
#[test]
fn local_source_clone_uses_mirror_no_local_and_argument_separator() {
let root = test_root("local-arguments");
let config_result = crate::GitBackendConfig::new(root.clone());
assert!(config_result.is_ok());
let config = match config_result {
Ok(value) => value,
Err(_) => return,
};
let runner = RecordingRunner::successful();
let backend = crate::GitBackend::with_runner(config, Box::new(runner.clone()));
let source_id_result = source_id("pgr");
assert!(source_id_result.is_some());
let source_id = match source_id_result {
Some(value) => value,
None => return,
};
let source_result = crate::GitSourceConfig::local(source_id, std::path::PathBuf::from("/controlled/source"));
assert!(source_result.is_ok());
let source = match source_result {
Ok(value) => value,
Err(_) => return,
};
let mirror_result = backend.ensure_mirror(&source);
assert!(mirror_result.is_ok());
let calls = runner.calls.borrow();
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].operation, crate::GitOperation::CloneMirror);
assert_eq!(
calls[0].arguments,
vec![
std::ffi::OsString::from("clone"),
std::ffi::OsString::from("--mirror"),
std::ffi::OsString::from("--no-local"),
std::ffi::OsString::from("--"),
std::ffi::OsString::from("/controlled/source"),
backend.repository_path(source.source_id()).into_os_string(),
]
);
assert_eq!(calls[1].operation, crate::GitOperation::InspectMirror);
assert!(backend.config().hooks_path().is_dir());
assert!(backend.config().repositories_path().is_dir());
assert!(backend.config().snapshots_path().is_dir());
let _ = std::fs::remove_dir_all(root);
return;
}
#[test]
fn remote_source_clone_does_not_add_no_local() {
let root = test_root("remote-arguments");
let config_result = crate::GitBackendConfig::new(root.clone());
assert!(config_result.is_ok());
let config = match config_result {
Ok(value) => value,
Err(_) => return,
};
let runner = RecordingRunner::successful();
let backend = crate::GitBackend::with_runner(config, Box::new(runner.clone()));
let source_id_result = source_id("howtos");
assert!(source_id_result.is_some());
let source_id = match source_id_result {
Some(value) => value,
None => return,
};
let source_result = crate::GitSourceConfig::remote(source_id, "ssh://git@example.invalid/howtos.git");
assert!(source_result.is_ok());
let source = match source_result {
Ok(value) => value,
Err(_) => return,
};
assert!(backend.ensure_mirror(&source).is_ok());
let calls = runner.calls.borrow();
assert_eq!(calls[0].operation, crate::GitOperation::CloneMirror);
assert_eq!(calls[0].arguments[0], std::ffi::OsString::from("clone"));
assert_eq!(calls[0].arguments[1], std::ffi::OsString::from("--mirror"));
assert_eq!(calls[0].arguments[2], std::ffi::OsString::from("--"));
assert!(!calls[0].arguments.contains(&std::ffi::OsString::from("--no-local")));
let _ = std::fs::remove_dir_all(root);
return;
}
#[test]
fn process_errors_do_not_expose_remote_location_or_credentials() {
let root = test_root("redaction");
let config_result = crate::GitBackendConfig::new(root.clone());
assert!(config_result.is_ok());
let config = match config_result {
Ok(value) => value,
Err(_) => return,
};
let runner = RecordingRunner::failing(crate::GitBackendError::GitCommandFailed {
operation: crate::GitOperation::CloneMirror,
exit_code: Some(128),
});
let backend = crate::GitBackend::with_runner(config, Box::new(runner));
let source_id_result = source_id("snippets");
assert!(source_id_result.is_some());
let source_id = match source_id_result {
Some(value) => value,
None => return,
};
let source_result = crate::GitSourceConfig::remote(source_id, "https://user:secret@example.invalid/snippets.git");
assert!(source_result.is_ok());
let source = match source_result {
Ok(value) => value,
Err(_) => return,
};
let result = backend.ensure_mirror(&source);
assert!(result.is_err());
let error = match result {
Ok(_) => return,
Err(value) => value,
};
let display = error.to_string();
assert!(!display.contains("secret"));
assert!(!display.contains("example.invalid"));
assert_eq!(
error,
crate::GitBackendError::GitCommandFailed {
operation: crate::GitOperation::CloneMirror,
exit_code: Some(128),
}
);
let _ = std::fs::remove_dir_all(root);
return;
}
#[test]
fn existing_mirror_is_inspected_without_clone() {
let root = test_root("existing-mirror");
let config_result = crate::GitBackendConfig::new(root.clone());
assert!(config_result.is_ok());
let config = match config_result {
Ok(value) => value,
Err(_) => return,
};
let runner = RecordingRunner::successful();
let backend = crate::GitBackend::with_runner(config, Box::new(runner.clone()));
let source_result = source_id("pgr");
assert!(source_result.is_some());
let source_id = match source_result {
Some(value) => value,
None => return,
};
let repository_path = backend.repository_path(&source_id);
assert!(std::fs::create_dir_all(&repository_path).is_ok());
let source_result = crate::GitSourceConfig::remote(source_id, "https://example.invalid/pgr.git");
assert!(source_result.is_ok());
let source = match source_result {
Ok(value) => value,
Err(_) => return,
};
let result = backend.ensure_mirror(&source);
assert!(result.is_ok());
let mirror = match result {
Ok(value) => value,
Err(_) => return,
};
assert_eq!(mirror.status(), crate::GitMirrorStatus::Existing);
let calls = runner.calls.borrow();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].operation, crate::GitOperation::InspectMirror);
let _ = std::fs::remove_dir_all(root);
return;
}
#[test]
fn invalid_existing_mirror_is_rejected_without_deleting_it() {
let root = test_root("invalid-mirror");
let config_result = crate::GitBackendConfig::new(root.clone());
assert!(config_result.is_ok());
let config = match config_result {
Ok(value) => value,
Err(_) => return,
};
let runner = RecordingRunner::with_inspection_stdout(b"false\n");
let backend = crate::GitBackend::with_runner(config, Box::new(runner));
let source_result = source_id("pgr");
assert!(source_result.is_some());
let source_id = match source_result {
Some(value) => value,
None => return,
};
let repository_path = backend.repository_path(&source_id);
assert!(std::fs::create_dir_all(&repository_path).is_ok());
let result = backend.inspect_mirror(&source_id);
assert_eq!(result, Err(crate::GitBackendError::InvalidMirror));
assert!(repository_path.is_dir());
let _ = std::fs::remove_dir_all(root);
return;
}

324
deltas/0.0.2/alpha.2.md Normal file
View File

@@ -0,0 +1,324 @@
<!-- file: deltas/0.0.2/alpha.2.md -->
<!-- version: 1 -->
# Delta 0.0.2-alpha.2 — contrats minimaux et backend process Git
## Base requise
Partir de `0.0.2-alpha.1.fix.1` après application de son history `PASS`, commit et push :
```text
history/0.0.2/alpha.1.md FAIL
history/0.0.2/alpha.1.fix.1.md PASS
```
Le workspace doit être propre avant application du présent delta.
La version devient :
```text
0.0.2-alpha.2
```
## Objet
Matérialiser la première responsabilité runtime Git réelle sans anticiper la tranche suivante :
- créer `knowledge-git-lib` comme crate d'implémentation réutilisable ;
- posséder une racine d'état explicitement fournie au backend ;
- posséder la configuration physique d'une source locale ou remote ;
- dériver une `source-key` sûre sans interpoler directement `SourceId` ou URL dans le filesystem ;
- encapsuler `std::process::Command` derrière un runner interne testable, sans shell ;
- préparer les répertoires `hooks-disabled/`, `repositories/` et `snapshots/` ;
- initialiser un mirror avec `git clone --mirror`, et `--no-local` pour une source locale ;
- inspecter un mirror existant comme repository bare avant réutilisation ;
- ne pas supprimer/recréer silencieusement un mirror invalide ;
- ne pas exposer URL, arguments, stderr ou credentials dans les erreurs publiques du runner.
`refresh`, résolution vers commit et snapshot/worktree fonctionnel restent réservés à `alpha.3`.
## Scope
ADD :
```text
crates/knowledge-git-lib/Cargo.toml
crates/knowledge-git-lib/src/backend.rs
crates/knowledge-git-lib/src/error.rs
crates/knowledge-git-lib/src/lib.rs
crates/knowledge-git-lib/unit_tests/backend.rs
deltas/0.0.2/alpha.2.md
```
REPLACE :
```text
Cargo.toml
README.md
RULES.md
VERSION
docs/architecture/001-PROJECT_PROFILE.md
docs/architecture/002-KNOWLEDGE_ARCHITECTURE.md
docs/plans/002-V0_0_2_GIT_SYNC_PLAN.md
docs/validation/001-VALIDATION_MATRIX.md
```
DELETE : aucun.
Le history `history/0.0.2/alpha.1.fix.1.md` appartient à la base requise mais n'est pas inclus dans ce delta.
Aucun fichier sous `Web/` n'est modifié.
## Contrat de `knowledge-git-lib`
La crate dépend uniquement de `knowledge-domain-lib` dans cette tranche.
Elle expose :
```text
GitBackend
GitBackendConfig
GitSourceConfig
GitSourceKind
GitSourceKey
GitMirror
GitMirrorStatus
GitBackendError
GitOperation
```
`knowledge-git-api` reste inchangée : son trait `GitRepository` possède déjà `refresh` et `resolve_revision`, qui ne doivent pas être implémentés artificiellement avant la tranche qui matérialise ces comportements.
La dépendance et l'implémentation explicite de ce trait seront introduites lorsque le backend possédera effectivement les opérations requises.
## Layout d'état
Une configuration valide possède obligatoirement une racine d'état non vide :
```text
<git-state-root>/
hooks-disabled/
repositories/
<source-key>.git/
snapshots/
```
Le répertoire `snapshots/` est réservé dès maintenant par le layout mais aucun snapshot n'est créé dans `alpha.2`.
La `source-key` est obtenue par encodage hexadécimal déterministe des octets UTF-8 du `SourceId`, préfixé par `source-`. Cette stratégie évite les collisions entre octets distincts et interdit qu'un séparateur de chemin, `..`, une URL ou un credential provenant de l'entrée logique devienne un composant de chemin brut.
## Runner Git contrôlé
Le runner système :
- utilise `std::process::Command` directement ;
- passe chaque argument séparément ;
- pose `GIT_TERMINAL_PROMPT=0` ;
- retire les variables `GIT_*` qui peuvent rediriger implicitement repository, worktree, index ou object database ;
- force `core.hooksPath` vers le répertoire vide contrôlé via l'environnement de configuration Git ;
- transforme un échec de spawn ou exit non nul en erreur récupérable ;
- ne copie jamais arguments, source, stdout/stderr ou credentials dans l'erreur publique.
Les sources locales utilisent :
```text
git clone --mirror --no-local -- <source> <mirror>
```
Les sources remote utilisent :
```text
git clone --mirror -- <source> <mirror>
```
Un mirror existant est inspecté avec :
```text
git --git-dir <mirror> rev-parse --is-bare-repository
```
Une réponse différente de `true` produit une erreur sans suppression destructive.
## Tests de la tranche
Les tests unitaires de `knowledge-git-lib` utilisent un runner enregistré injecté uniquement sous `cfg(test)` pour vérifier sans réseau :
- rejet des chemins de configuration vides ;
- dérivation sûre de `source-key` ;
- arguments exacts de clone local ;
- absence de `--no-local` pour un remote ;
- redaction des erreurs contenant potentiellement une URL avec credentials ;
- inspection d'un mirror existant sans clone ;
- refus d'un mirror invalide sans suppression.
Les tests réels sur repositories Git temporaires appartiennent à `alpha.3`, avec refresh, résolution et snapshot.
## Gate opérateur
Après application du history `alpha.1.fix.1`, commit/push de cet état, puis application du présent delta :
```bash
test "$(cat VERSION)" = '0.0.2-alpha.2'
grep -F 'version = "0.0.2-alpha.2"' Cargo.toml
grep -A2 '^## Statut$' history/0.0.2/alpha.1.fix.1.md | grep -Fx 'PASS'
git diff --check
python3 - <<'PY'
from pathlib import Path
import re
import tomllib
required = (
"crates/knowledge-git-lib/Cargo.toml",
"crates/knowledge-git-lib/src/backend.rs",
"crates/knowledge-git-lib/src/error.rs",
"crates/knowledge-git-lib/src/lib.rs",
"crates/knowledge-git-lib/unit_tests/backend.rs",
"deltas/0.0.2/alpha.2.md",
)
for value in required:
assert Path(value).is_file(), value
root = tomllib.loads(Path("Cargo.toml").read_text(encoding="utf-8"))
manifest = tomllib.loads(Path("crates/knowledge-git-lib/Cargo.toml").read_text(encoding="utf-8"))
assert "crates/knowledge-git-lib" in root["workspace"]["members"]
assert set(manifest["dependencies"]) == {"knowledge-domain-lib"}, manifest["dependencies"]
backend = Path("crates/knowledge-git-lib/src/backend.rs").read_text(encoding="utf-8")
error = Path("crates/knowledge-git-lib/src/error.rs").read_text(encoding="utf-8")
for phrase in (
"std::process::Command::new",
'command.env("GIT_TERMINAL_PROMPT", "0")',
'command.env_remove("GIT_DIR")',
'command.env_remove("GIT_WORK_TREE")',
'command.env_remove("GIT_INDEX_FILE")',
'command.env_remove("GIT_OBJECT_DIRECTORY")',
'command.env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")',
'command.env("GIT_CONFIG_KEY_0", "core.hooksPath")',
'std::ffi::OsString::from("--mirror")',
'std::ffi::OsString::from("--no-local")',
'std::ffi::OsString::from("--")',
'std::ffi::OsString::from("--is-bare-repository")',
):
assert phrase in backend, phrase
for forbidden in (
'Command::new("sh")',
'Command::new("bash")',
'Command::new("cmd")',
'Command::new("powershell")',
"stderr: String",
"arguments: Vec<String>",
):
assert forbidden not in backend + error, forbidden
for forbidden in ("refresh(", "resolve_revision(", "worktree add", "knowledge-app-lib"):
assert forbidden not in backend, forbidden
for path in Path("crates/knowledge-git-lib").rglob("*.rs"):
text = path.read_text(encoding="utf-8")
for forbidden in ("unwrap()", "expect(", "panic!("):
assert forbidden not in text, (path, forbidden)
for path in Path(".").rglob("*.md"):
if ".git" in path.parts:
continue
lines = path.read_text(encoding="utf-8").splitlines()
assert lines[0] == f"<!-- file: {path.as_posix()} -->", path
assert re.fullmatch(r"<!-- version: [1-9][0-9]* -->", lines[1]), path
print("Knowledge 0.0.2-alpha.2 structural audit: clean")
PY
python3 -B scripts/audit_rust_test_layout.py crates
python3 -B scripts/audit_rust_blank_lines.py crates
python3 -B scripts/audit_rust_item_spacing.py crates
python3 -B scripts/audit_rust_module_order.py crates
python3 -B scripts/audit_rust_trait_import_blocks.py crates
python3 -B scripts/audit_rust_reexport_blocks.py crates
python3 -B scripts/audit_rust_export_completeness.py --root . --summary-only
cargo fmt --all -- --check
cargo metadata --format-version 1 --no-deps > /tmp/sasedev-knowledge-0.0.2-alpha.2-metadata.json
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p knowledge-git-lib --all-targets --all-features
cargo test --workspace --all-targets --all-features
python3 - <<'PY'
import json
from pathlib import Path
metadata = json.loads(Path("/tmp/sasedev-knowledge-0.0.2-alpha.2-metadata.json").read_text(encoding="utf-8"))
packages = {package["name"] for package in metadata["packages"]}
expected = {
"knowledge-domain-lib",
"knowledge-git-api",
"knowledge-git-lib",
"knowledge-sync-api",
"knowledge-web-lib",
"knowledge-pgr-server",
"knowledge-howtos-server",
"knowledge-snippets-server",
}
assert packages == expected, packages
assert len(metadata["workspace_members"]) == 8, metadata["workspace_members"]
print("Knowledge 0.0.2-alpha.2 Cargo metadata audit: clean")
PY
test -f Cargo.lock
git check-ignore -q Cargo.lock
if git ls-files --error-unmatch Cargo.lock >/dev/null 2>&1; then
echo 'Cargo.lock must remain untracked for Knowledge' >&2
exit 1
fi
test -z "$(git diff --name-only -- Web)"
test -z "$(git diff --name-only -- crates/knowledge-git-api crates/knowledge-sync-api)"
test ! -e crates/knowledge-app-lib
test ! -e crates/knowledge-forge-gitea-lib
test ! -e crates/knowledge-store-postgres-lib
git add -A
git diff --cached --check
git status --short
```
## Résultats attendus
La gate doit notamment produire :
```text
Knowledge 0.0.2-alpha.2 structural audit: clean
Rust test-layout audit: clean (8 crate(s))
Rust export completeness audit: 0 candidate(s) across 8 package(s)
Knowledge 0.0.2-alpha.2 Cargo metadata audit: clean
```
La suite ciblée `knowledge-git-lib` doit exécuter `7` tests unitaires `PASS`.
`git diff --name-only -- Web` et le diff ciblant les deux crates `-api` doivent rester vides.
Le staging final doit contenir uniquement le scope annoncé par ce delta. Aucun `history/0.0.2/alpha.2.md` ne doit exister avant réception des résultats opérateur.
## Revue opérateur complémentaire
Vérifier que :
- `knowledge-git-lib` possède bien une responsabilité d'implémentation et n'est pas une crate placeholder ;
- le runner ne passe jamais par un shell ;
- les erreurs process restent exploitables sans recopier des informations sensibles ;
- le layout généré reste reconstructible et distinct du stockage métier ;
- `knowledge-git-api` n'est pas artificiellement étendue pour des opérations qui n'existent pas encore ;
- `alpha.3` reste propriétaire du refresh, de la résolution et du snapshot réel.
## Transition
### PASS
Produire `history/0.0.2/alpha.2.md`, appliquer ce history, committer/pousser `0.0.2-alpha.2`, puis seulement préparer `0.0.2-alpha.3`.
### FAIL / PARTIAL / BLOCKED
Conserver le résultat réel dans `history/0.0.2/alpha.2.md` et rester dans le scope `alpha.2` avec `alpha.2.fix.1` si nécessaire, sauf déclencheur de REPLAN explicitement satisfait.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/001-PROJECT_PROFILE.md -->
<!-- version: 10 -->
<!-- version: 11 -->
# Profil du projet Knowledge
@@ -79,7 +79,7 @@ Aucun `OVERRIDE` n'est requis par l'adoption `v0.1.3`.
## Matrice des décisions importantes
| Décision | Classification | Contrat retenu / condition de reprise |
|----------------------------------------|-----------------------------------|----------------------------------------------------------------------------------------------------------|
|----------------------------------------|-----------------------------------|------------------------------------------------------------------------------------------------------------------|
| Dépôt applicatif | `KEEP-LOCAL` | `sasedev-knowledge` ; workspace unique de la plateforme |
| Nom court / préfixe crates | `KEEP-LOCAL` | `knowledge` / `knowledge-` |
| Dépôt normatif | `KEEP-LOCAL` | dépôt existant `pgr`, jamais fusionné dans le dépôt applicatif |
@@ -88,7 +88,7 @@ Aucun `OVERRIDE` n'est requis par l'adoption `v0.1.3`.
| Nombre de sources Git | `KEEP-LOCAL` | au moins trois sources initiales, sans limite codée à trois |
| Git comme backend du contenu | `KEEP-LOCAL` | invariant d'architecture Knowledge |
| Implémentation Git de `0.0.2` | `KEEP-LOCAL` | processus `git` contrôlé derrière `knowledge-git-api` ; décision réévaluable derrière le contrat |
| Configuration physique Git | `KEEP-LOCAL` | future `knowledge-git-lib` ; source locale/remote + racine d'état ; pas dans le domaine |
| Configuration physique Git | `KEEP-LOCAL` | `knowledge-git-lib` matérialisée en `0.0.2-alpha.2` ; source locale/remote + racine d'état ; pas dans le domaine |
| Copies Git générées | `KEEP-LOCAL` | racine d'état explicite hors dépôt source, mirrors et snapshots adressés par clé sûre/révision |
| Partage du cache Git entre processus | `DEFER` | état root exclusif à une instance backend en `0.0.2` ; reprendre avec verrouillage inter-processus réel |
| Forge | `KEEP-LOCAL` | capacité optionnelle derrière adapter ; aucune API de forge dans le domaine |
@@ -122,11 +122,12 @@ Aucun `OVERRIDE` n'est requis par l'adoption `v0.1.3`.
## Nomenclature de crates
Les packages matérialisés dans la stable `0.0.1` sont :
Les packages matérialisés sont :
```text
knowledge-domain-lib types métier stables de famille/source
knowledge-git-api contrat passif GitRepository provider-neutral
knowledge-git-lib implémentation Git générique + configuration physique + état local contrôlé
knowledge-sync-api contrat passif RepositoryEventSource
knowledge-web-lib composition Actix/Maud/Vite partagée
knowledge-pgr-server launcher PGR mince
@@ -134,12 +135,7 @@ knowledge-howtos-server launcher HOWTOs mince
knowledge-snippets-server launcher snippets mince
```
`0.0.2-alpha.1` ferme deux décisions d'ownership sans créer de placeholder :
```text
knowledge-git-lib implémentation Git générique + configuration physique + état local contrôlé
knowledge-app-lib owner candidat du cas d'usage de synchronisation partagé, à matérialiser seulement quand l'orchestration réelle le démontre
```
`knowledge-git-lib` devient réelle en `0.0.2-alpha.2`. `knowledge-app-lib` reste seulement l'owner candidat du cas d'usage de synchronisation partagé et ne sera matérialisée que si l'orchestration réelle démontre cette responsabilité.
Les responsabilités suivantes restent candidates et non matérialisées :

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/002-KNOWLEDGE_ARCHITECTURE.md -->
<!-- version: 10 -->
<!-- version: 11 -->
# Architecture de fondation Knowledge
@@ -105,7 +105,7 @@ Le domaine ne suppose pas qu'une famille possède exactement un repository. Il m
## 3. Git générique
Le contrat `GitRepository` reste provider-neutral. `0.0.2-alpha.1` sélectionne comme première implémentation un **processus `git` contrôlé** dans la future crate :
Le contrat `GitRepository` reste provider-neutral. `0.0.2-alpha.1` sélectionne comme première implémentation un **processus `git` contrôlé** et `0.0.2-alpha.2` matérialise son premier owner runtime :
```text
knowledge-git-lib
@@ -117,7 +117,7 @@ Cette sélection est une décision locale Knowledge, pas un couplage du contrat
`knowledge-domain-lib` continue de posséder `SourceId` et `ContentSource`. Les URLs, chemins locaux, exécutable Git et répertoires de travail ne remontent pas dans le domaine.
`knowledge-git-lib` possédera la configuration physique :
`knowledge-git-lib` possède la configuration physique :
```text
source_id -> local path ou remote Git générique
@@ -143,13 +143,13 @@ Les données générées résident hors du dépôt applicatif et hors des dépô
Le backend reçoit explicitement `<git-state-root>` ; il n'utilise aucun défaut implicite sous le workspace. Une variable de déploiement telle que `KNOWLEDGE_GIT_STATE_ROOT` pourra alimenter la composition, mais la librairie reçoit un chemin et ne lit pas elle-même l'environnement.
`source-key` est une clé sûre dérivée par l'implémentation. Un `SourceId` ou une URL remote ne sont jamais interpolés directement comme chemin de filesystem.
`source-key` est une clé sûre dérivée par l'implémentation. En `alpha.2`, elle encode de manière déterministe les octets UTF-8 du `SourceId` sous une forme hexadécimale préfixée ; un `SourceId` ou une URL remote ne sont jamais interpolés directement comme chemin de filesystem.
Pendant `0.0.2`, une racine d'état appartient exclusivement à une instance backend/processus. Le partage simultané d'une même racine entre plusieurs processus reste `DEFER` jusqu'à matérialisation d'un verrouillage inter-processus.
### Mirror et refresh
Une source locale ou remote converge vers un mirror Git contrôlé.
Une source locale ou remote converge vers un mirror Git contrôlé. `alpha.2` implémente l'initialisation et l'inspection bare minimale ; refresh, résolution et snapshot restent dans les tranches suivantes.
L'initialisation utilise conceptuellement :

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/002-V0_0_2_GIT_SYNC_PLAN.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# Plan 0.0.2 — Git générique et synchronisation de lecture
@@ -28,7 +28,7 @@ PGR publish report pgr-0.1.3-publish.001-report.md = PASS
Le passage de PGR `v0.1.1` à `v0.1.3` est traité comme adoption d'un projet existant via `230-ADOPTION`. Les artefacts historiques de `0.0.1` ne sont pas réécrits.
`0.0.2-alpha.1` est `FAIL` pour défaut de structure du présent plan malgré des contrôles techniques propres. `alpha.1.fix.1` corrige uniquement ce cadrage documentaire avant toute implémentation Git.
`0.0.2-alpha.1` est `FAIL` pour défaut de structure du présent plan malgré des contrôles techniques propres. `alpha.1.fix.1` a corrigé uniquement ce cadrage documentaire et sa gate opérateur est `PASS`. `alpha.2` peut donc matérialiser le premier backend Git réel.
## Mission
@@ -65,10 +65,6 @@ Décisions déjà matérialisées par le cadrage `alpha.1` et non remises en cau
Questions laissées ouvertes jusqu'à la tranche qui les consomme :
- forme exacte des types de configuration publics de `knowledge-git-lib` ;
- frontière exacte entre runner de processus interne et API publique ;
- modèle d'erreur précis du backend Git, notamment redaction des arguments sensibles ;
- politique de collision/normalisation de `source-key` et validation des chemins ;
- politique de nettoyage/rétention des snapshots ;
- besoin réel de supporter submodules ou Git LFS dans cette release ;
- besoin réel d'une crate `knowledge-app-lib` pour l'orchestration partagée ;
@@ -139,11 +135,15 @@ Le résultat opérateur initial est **FAIL** : les contrôles techniques transmi
`alpha.1.fix.1` corrige uniquement cette structure documentaire, ajoute la gate de non-régression correspondante et ne modifie aucune décision technique de l'étude Git. Aucun fichier sous `crates/` ou `Web/` n'est modifié dans `alpha.1` ou son fix.
La trajectoire normale ne reprend vers `alpha.2` qu'après `PASS` opérateur du fix.
La gate opérateur de `alpha.1.fix.1` est **PASS**.
### alpha.2 — contrats minimaux et backend process
Matérialiser le premier backend réel sans encore prétendre couvrir tout le cycle de synchronisation :
Statut opérateur : **à valider**.
Le delta matérialise le premier backend réel sans encore prétendre couvrir tout le cycle de synchronisation. Les choix concrets de la tranche sont : configuration publique `GitBackendConfig`/`GitSourceConfig`, runner process strictement interne, erreurs publiques redacted par construction et `source-key` hexadécimale déterministe dérivée du `SourceId`.
Le scope reste :
- créer `knowledge-git-lib` avec responsabilité réelle ;
- introduire uniquement les types/configurations nécessaires aux sources et à la racine d'état ;

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/001-VALIDATION_MATRIX.md -->
<!-- version: 19 -->
<!-- version: 20 -->
# Matrice de validation Knowledge
@@ -543,3 +543,32 @@ Le fix reste documentaire et ne change aucune décision technique Git. Il doit p
- `git diff --cached --check` propre avant commit.
Le `PASS` du fix ferme `alpha.1` et autorise uniquement la préparation de `0.0.2-alpha.2`.
## Gate de `0.0.2-alpha.2`
La première tranche fonctionnelle Git doit prouver :
- `VERSION = 0.0.2-alpha.2` et `[workspace.package].version` identique ;
- history `alpha.1.fix.1` `PASS` committé avant application ;
- huit packages Cargo exactement, avec `knowledge-git-lib` comme nouvelle crate `-lib` réelle ;
- `knowledge-git-api` et `knowledge-sync-api` toujours passives et inchangées ;
- `knowledge-git-lib` dépendant uniquement des contrats/domaines réellement nécessaires, sans Actix, forge, PostgreSQL, scheduler ou runtime async ;
- racine d'état obligatoire et explicite, avec sous-répertoires `hooks-disabled/`, `repositories/` et `snapshots/` ;
- dérivation de `source-key` ne réutilisant ni `SourceId` brut ni URL remote comme chemin ;
- source locale utilisant `git clone --mirror --no-local -- ...` ;
- source remote utilisant `git clone --mirror -- ...` sans `--no-local` ;
- runner process interne utilisant `std::process::Command` directement, sans shell ;
- prompts Git interactifs désactivés et variables Git capables de rediriger implicitement repository/worktree retirées ;
- hooks Git désactivés via un répertoire contrôlé ;
- erreurs de processus publiques ne contenant ni URL source, ni arguments, ni stderr, ni credentials ;
- inspection d'un mirror existant sans suppression/recréation silencieuse ;
- tests unitaires ciblant configuration, layout, arguments, redaction et erreurs ;
- aucun refresh, résolution de révision ou snapshot fonctionnel anticipé avant `alpha.3` ;
- aucun adapter Gitea/GitHub/GitLab, PostgreSQL, scheduler ou `knowledge-app-lib` ajouté ;
- `cargo fmt --all -- --check`, audits Rust, `cargo metadata`, `cargo check`, Clippy strict et tests ciblés/workspace propres ;
- baseline Web inchangée et absence de diff sous `Web/` ;
- `Cargo.lock` toujours ignoré/non suivi ;
- `git diff --cached --check` propre avant commit.
Le `PASS` d'`alpha.2` autorise uniquement la préparation de `0.0.2-alpha.3`, consacrée au refresh réel, à la résolution et au snapshot sur repositories Git temporaires.

160
history/0.0.2/alpha.2.md Normal file
View File

@@ -0,0 +1,160 @@
<!-- file: history/0.0.2/alpha.2.md -->
<!-- version: 1 -->
# History 0.0.2-alpha.2 — contrats minimaux et backend process Git
## Statut
FAIL
La gate opérateur de `0.0.2-alpha.2` exécutée le `2026-09-28` échoue avant validation fonctionnelle du nouveau backend Git.
## Baseline opérateur
`alpha.1.fix.1` a été historisé `PASS`, committé et poussé avant application d'`alpha.2` :
```text
history/0.0.2/alpha.1.fix.1.md PASS
commit 84c01eb
message 0.0.2-alpha.1.fix.1
branch master
push origin/master PASS
```
Le push observé a avancé `origin/master` de `ab1e5fd` à `84c01eb`.
## Version et contrôles initiaux
Les contrôles de version passent :
```text
VERSION 0.0.2-alpha.2
Cargo workspace version 0.0.2-alpha.2
history alpha.1.fix.1 PASS
```
En revanche, l'hygiène Git échoue immédiatement :
```text
git diff --check
docs/validation/001-VALIDATION_MATRIX.md:574: new blank line at EOF.
```
Ce défaut est bloquant pour la tranche.
## Audit structurel du delta
L'audit structurel embarqué échoue sur un fichier généré sous `node_modules` :
```text
AssertionError: Web/knowledge-template-lab/node_modules/sync-child-process/README.md
```
Le défaut vient de la gate elle-même : elle parcourt récursivement tous les `*.md` du workspace et n'exclut que `.git`, alors que `node_modules` ne fait pas partie du corpus Markdown gouverné par Knowledge.
L'intention de contrôle des headers Markdown reste valide, mais la gate doit ignorer les arbres générés/non gouvernés tels que `node_modules`.
## Audits Rust structurels
Les audits structurels passent :
```text
Rust test-layout audit: clean (8 crate(s))
Rust blank-line audit: clean (22 file(s))
Rust item-spacing audit: clean (22 file(s))
Rust module-order audit: clean (22 file(s))
Rust trait-import-block audit: clean (22 file(s))
Rust re-export-block audit: clean (22 file(s))
Rust export completeness audit: 0 candidate(s) across 8 package(s)
```
Ils confirment la présence cohérente de la huitième crate, mais ne remplacent pas `cargo fmt`, la compilation, Clippy et les tests.
## Formatage Rust
`cargo fmt --all -- --check` échoue sur les nouveaux fichiers de `knowledge-git-lib`.
Les écarts sont uniquement des différences de formatage rustfmt dans :
```text
crates/knowledge-git-lib/src/backend.rs
crates/knowledge-git-lib/src/error.rs
crates/knowledge-git-lib/src/lib.rs
crates/knowledge-git-lib/unit_tests/backend.rs
```
La tranche n'est donc pas formatée conformément à la gate Rust du projet.
## Compilation et tests
`cargo metadata --format-version 1 --no-deps` produit correctement les métadonnées attendues, mais `cargo check --workspace` échoue avec `E0423` :
```text
crates/knowledge-git-lib/src/backend.rs:126
return crate::GitSourceKey(value);
```
`GitSourceKey` est réexporté publiquement depuis `crate`, mais son champ tuple reste privé au module `backend`. La construction doit donc être effectuée depuis le module propriétaire, par exemple via `crate::backend::GitSourceKey(value)`, ou par un constructeur privé équivalent.
Le même défaut bloque :
```text
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p knowledge-git-lib --all-targets --all-features
cargo test --workspace --all-targets --all-features
```
Aucun résultat de test `knowledge-git-lib` n'est donc présenté comme exécuté avec succès.
## Métadonnées et limites de scope
L'audit des métadonnées Cargo passe :
```text
Knowledge 0.0.2-alpha.2 Cargo metadata audit: clean
```
Il confirme huit packages exactement, dont `knowledge-git-lib`.
Les contrôles de frontière passent également :
```text
Web/ sans diff
crates/knowledge-git-api sans diff
crates/knowledge-sync-api sans diff
crates/knowledge-app-lib absent
crates/knowledge-forge-gitea-lib absent
crates/knowledge-store-postgres-lib absent
Cargo.lock ignoré et non suivi
```
Le `git status --short` final n'a pas été exécuté à cause d'une faute de frappe opérateur (`it status --short`). Cette faute de frappe n'influence pas la décision : `git diff --cached --check` venait déjà d'échouer sur la ligne vide finale de la matrice de validation.
## Diagnostic
`alpha.2` reste dans son scope prévu ; aucun REPLAN n'est nécessaire.
Le correctif doit uniquement :
- supprimer la ligne vide terminale fautive de la matrice de validation ;
- corriger la gate Markdown pour exclure les arbres générés/non gouvernés ;
- appliquer le formatage rustfmt attendu aux quatre fichiers Rust signalés ;
- corriger la construction privée de `GitSourceKey` sans élargir l'API publique ;
- conserver inchangés le scope, les dépendances et les responsabilités du backend ;
- ne pas introduire `refresh`, résolution de révision ou snapshot fonctionnel avant `alpha.3`.
## Décision
`0.0.2-alpha.2` est `FAIL`.
La transition autorisée est :
```text
appliquer ce history
-> git add / diff --cached --check / status
-> commit exact "0.0.2-alpha.2"
-> push origin master
-> vérifier le workspace propre
-> appliquer seulement ensuite 0.0.2-alpha.2.fix.1
```