0.0.2-alpha.3

This commit is contained in:
2026-09-28 19:23:12 +02:00
parent 38025266b9
commit 721e8a6d9c
18 changed files with 1103 additions and 38 deletions

View File

@@ -1,5 +1,5 @@
# file: Cargo.toml
# version: 21
# version: 22
[workspace]
resolver = "3"
@@ -15,13 +15,14 @@ members = [
]
[workspace.package]
version = "0.0.2-alpha.2.fix.2"
version = "0.0.2-alpha.3"
edition = "2024"
publish = false
repository = "https://git.sasedev.com/Sasedev/sasedev-knowledge"
[workspace.dependencies]
knowledge-domain-lib = { path = "crates/knowledge-domain-lib" }
knowledge-git-api = { path = "crates/knowledge-git-api" }
knowledge-web-lib = { path = "crates/knowledge-web-lib" }
[workspace.lints.rust]

View File

@@ -1,5 +1,5 @@
<!-- file: README.md -->
<!-- version: 11 -->
<!-- version: 12 -->
# 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.2` matérialise maintenant le premier backend Git réel après validation du cadrage et de son correctif documentaire.
`0.0.1` est la première stable de fondation de SaseDev Knowledge. `0.0.2-alpha.3` complète maintenant le chemin Git générique de lecture après fermeture de `alpha.2` par `alpha.2.fix.2` `PASS`.
`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.
`knowledge-git-lib` implémente désormais `knowledge-git-api::GitRepository` pour des sources physiques enregistrées : mirror contrôlé, refresh avec pruning, résolution d'une référence vers un commit et snapshot/worktree détaché adressé par révision. Les preuves fonctionnelles reposent sur des repositories Git temporaires locaux et n'exigent aucune API de forge.
## Topologie produit de fondation

View File

@@ -1,5 +1,5 @@
<!-- file: RULES.md -->
<!-- version: 11 -->
<!-- version: 12 -->
# Règles SaseDev Knowledge
@@ -131,6 +131,8 @@ La configuration physique des sources Git et la matérialisation locale appartie
`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.
À partir de `0.0.2-alpha.3`, le backend enregistre explicitement les configurations physiques associées aux `SourceId` puis implémente `knowledge-git-api::GitRepository`. Le refresh MUST converger par le remote du mirror avec pruning ; une référence MUST être résolue avec vérification vers un commit avant d'être utilisée comme identité autoritaire. Les snapshots MUST être adressés par la révision résolue et un snapshot existant MUST être vérifié sur son `HEAD` et son état propre avant réutilisation. Une divergence ou un worktree sale est une erreur récupérable et ne déclenche aucune suppression silencieuse.
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é.
## PostgreSQL

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/knowledge-git-api/src/lib.rs
// version: 2
// version: 3
//! Provider-neutral contracts for Git-backed Knowledge sources.
//!
@@ -38,6 +38,30 @@ impl crate::GitRevision {
}
}
/// Read-only local materialization of one authoritative Git revision.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GitSnapshot {
path: std::path::PathBuf,
revision: crate::GitRevision,
}
impl crate::GitSnapshot {
/// Build a snapshot contract from a resolved revision and its local read root.
pub fn new(revision: crate::GitRevision, path: std::path::PathBuf) -> crate::GitSnapshot {
return crate::GitSnapshot { path, revision };
}
/// Return the local root containing the materialized repository content.
pub fn path(&self) -> &std::path::Path {
return self.path.as_path();
}
/// Return the authoritative revision represented by the snapshot.
pub fn revision(&self) -> &crate::GitRevision {
return &self.revision;
}
}
/// Provider-neutral operations required from a Git repository backend.
///
/// Implementations may use a local Git executable, a Rust Git library or another generic Git
@@ -51,6 +75,9 @@ pub trait GitRepository {
/// Resolve one opaque reference to the authoritative revision used by synchronization.
fn resolve_revision(&self, source_id: &knowledge_domain_lib::SourceId, reference: &crate::GitReference) -> Result<crate::GitRevision, Self::Error>;
/// Prepare or validate a read-only local snapshot for one resolved revision.
fn prepare_snapshot(&self, source_id: &knowledge_domain_lib::SourceId, revision: &crate::GitRevision) -> Result<crate::GitSnapshot, Self::Error>;
}
#[cfg(test)]

View File

@@ -1,5 +1,5 @@
// file: crates/knowledge-git-api/unit_tests/lib.rs
// version: 1
// version: 2
#[derive(Debug, Eq, PartialEq)]
struct FakeGitError;
@@ -16,6 +16,10 @@ impl crate::GitRepository for FakeGitRepository {
fn resolve_revision(&self, source_id: &knowledge_domain_lib::SourceId, reference: &crate::GitReference) -> Result<crate::GitRevision, Self::Error> {
return Ok(crate::GitRevision::new(&format!("{}:{}", source_id.as_str(), reference.as_str())));
}
fn prepare_snapshot(&self, _source_id: &knowledge_domain_lib::SourceId, revision: &crate::GitRevision) -> Result<crate::GitSnapshot, Self::Error> {
return Ok(crate::GitSnapshot::new(revision.clone(), std::path::PathBuf::from("/controlled/snapshot")));
}
}
#[test]
@@ -31,5 +35,14 @@ fn generic_repository_contract_does_not_require_a_forge() {
assert!(crate::GitRepository::refresh(&mut repository, &source_id).is_ok());
let revision_result = crate::GitRepository::resolve_revision(&repository, &source_id, &reference);
assert_eq!(revision_result, Ok(crate::GitRevision::new("local-pgr:refs/heads/master")));
let revision = match revision_result {
Ok(value) => value,
Err(_) => return,
};
let snapshot_result = crate::GitRepository::prepare_snapshot(&repository, &source_id, &revision);
assert_eq!(
snapshot_result,
Ok(crate::GitSnapshot::new(revision, std::path::PathBuf::from("/controlled/snapshot")))
);
return;
}

View File

@@ -1,5 +1,5 @@
# file: crates/knowledge-git-lib/Cargo.toml
# version: 1
# version: 2
[package]
name = "knowledge-git-lib"
@@ -10,6 +10,7 @@ repository.workspace = true
[dependencies]
knowledge-domain-lib.workspace = true
knowledge-git-api.workspace = true
[lints]
workspace = true

View File

@@ -1,5 +1,5 @@
// file: crates/knowledge-git-lib/src/backend.rs
// version: 2
// version: 3
//! Physical source configuration, generated-state layout and controlled Git process runner.
@@ -200,21 +200,22 @@ impl GitCommandRunner for SystemGitRunner {
}
}
/// Controlled Git backend that owns local mirror initialization and inspection.
/// Controlled Git backend that owns configured sources and local read materialization.
pub struct GitBackend {
config: crate::GitBackendConfig,
runner: Box<dyn GitCommandRunner>,
sources: Vec<crate::GitSourceConfig>,
}
impl crate::GitBackend {
/// Create a backend using the configured real Git executable.
/// Create a backend using the configured real Git executable and no registered source yet.
pub fn new(config: crate::GitBackendConfig) -> crate::GitBackend {
return crate::GitBackend { config, runner: Box::new(SystemGitRunner) };
return crate::GitBackend { config, runner: Box::new(SystemGitRunner), sources: Vec::new() };
}
#[cfg(test)]
fn with_runner(config: crate::GitBackendConfig, runner: Box<dyn GitCommandRunner>) -> crate::GitBackend {
return crate::GitBackend { config, runner };
return crate::GitBackend { config, runner, sources: Vec::new() };
}
/// Return the immutable backend configuration.
@@ -222,12 +223,36 @@ impl crate::GitBackend {
return &self.config;
}
/// Register one physical source configuration for provider-neutral operations.
pub fn register_source(&mut self, source: crate::GitSourceConfig) -> Result<(), crate::GitBackendError> {
for existing in &self.sources {
if existing.source_id() == source.source_id() {
return Err(crate::GitBackendError::DuplicateSource);
}
}
self.sources.push(source);
return Ok(());
}
/// 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()));
}
/// Derive the revision-addressed snapshot path after validating the revision as a safe object identity.
pub fn snapshot_path(
&self,
source_id: &knowledge_domain_lib::SourceId,
revision: &knowledge_git_api::GitRevision,
) -> Result<std::path::PathBuf, crate::GitBackendError> {
if !is_safe_revision(revision.as_str()) {
return Err(crate::GitBackendError::InvalidRevision);
}
let source_key = crate::GitSourceKey::from_source_id(source_id);
return Ok(self.config.snapshots_path().join(source_key.as_str()).join(revision.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);
@@ -276,6 +301,15 @@ impl crate::GitBackend {
}
}
fn configured_source(&self, source_id: &knowledge_domain_lib::SourceId) -> Result<&crate::GitSourceConfig, crate::GitBackendError> {
for source in &self.sources {
if source.source_id() == source_id {
return Ok(source);
}
}
return Err(crate::GitBackendError::SourceNotConfigured);
}
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);
@@ -336,6 +370,238 @@ impl crate::GitBackend {
}
return Ok(());
}
fn refresh_registered_source(&self, source_id: &knowledge_domain_lib::SourceId) -> Result<(), crate::GitBackendError> {
let source_result = self.configured_source(source_id);
let source = match source_result {
Ok(value) => value,
Err(error) => return Err(error),
};
let mirror_result = self.ensure_mirror(source);
let mirror = match mirror_result {
Ok(value) => value,
Err(error) => return Err(error),
};
let specification = GitCommandSpec {
arguments: vec![
std::ffi::OsString::from("--git-dir"),
mirror.repository_path().as_os_str().to_os_string(),
std::ffi::OsString::from("remote"),
std::ffi::OsString::from("update"),
std::ffi::OsString::from("--prune"),
],
executable: self.config.git_executable().to_path_buf(),
hooks_path: self.config.hooks_path(),
operation: crate::GitOperation::RefreshMirror,
};
let run_result = self.runner.run(&specification);
match run_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
fn resolve_registered_revision(
&self,
source_id: &knowledge_domain_lib::SourceId,
reference: &knowledge_git_api::GitReference,
) -> Result<knowledge_git_api::GitRevision, crate::GitBackendError> {
let configured_result = self.configured_source(source_id);
match configured_result {
Ok(_) => (),
Err(error) => return Err(error),
}
let mirror_result = self.inspect_mirror(source_id);
let mirror = match mirror_result {
Ok(Some(value)) => value,
Ok(None) => return Err(crate::GitBackendError::MirrorMissing),
Err(error) => return Err(error),
};
let revision_expression = format!("{}^{{commit}}", reference.as_str());
let specification = GitCommandSpec {
arguments: vec![
std::ffi::OsString::from("--git-dir"),
mirror.repository_path().as_os_str().to_os_string(),
std::ffi::OsString::from("rev-parse"),
std::ffi::OsString::from("--verify"),
std::ffi::OsString::from("--end-of-options"),
std::ffi::OsString::from(revision_expression),
],
executable: self.config.git_executable().to_path_buf(),
hooks_path: self.config.hooks_path(),
operation: crate::GitOperation::ResolveRevision,
};
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::ResolveRevision });
},
};
if !is_safe_revision(stdout) {
return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::ResolveRevision });
}
return Ok(knowledge_git_api::GitRevision::new(stdout));
}
fn prepare_registered_snapshot(
&self,
source_id: &knowledge_domain_lib::SourceId,
revision: &knowledge_git_api::GitRevision,
) -> Result<knowledge_git_api::GitSnapshot, crate::GitBackendError> {
let configured_result = self.configured_source(source_id);
match configured_result {
Ok(_) => (),
Err(error) => return Err(error),
}
if !is_safe_revision(revision.as_str()) {
return Err(crate::GitBackendError::InvalidRevision);
}
let mirror_result = self.inspect_mirror(source_id);
let mirror = match mirror_result {
Ok(Some(value)) => value,
Ok(None) => return Err(crate::GitBackendError::MirrorMissing),
Err(error) => return Err(error),
};
let snapshot_path_result = self.snapshot_path(source_id, revision);
let snapshot_path = match snapshot_path_result {
Ok(value) => value,
Err(error) => return Err(error),
};
if snapshot_path.exists() {
let validation_result = self.validate_snapshot(&snapshot_path, revision);
match validation_result {
Ok(()) => return Ok(knowledge_git_api::GitSnapshot::new(revision.clone(), snapshot_path)),
Err(error) => return Err(error),
}
}
let parent = match snapshot_path.parent() {
Some(value) => value,
None => return Err(crate::GitBackendError::StatePreparationFailed),
};
let create_result = std::fs::create_dir_all(parent);
if create_result.is_err() {
return Err(crate::GitBackendError::StatePreparationFailed);
}
let specification = GitCommandSpec {
arguments: vec![
std::ffi::OsString::from("--git-dir"),
mirror.repository_path().as_os_str().to_os_string(),
std::ffi::OsString::from("worktree"),
std::ffi::OsString::from("add"),
std::ffi::OsString::from("--detach"),
std::ffi::OsString::from("--"),
snapshot_path.as_os_str().to_os_string(),
std::ffi::OsString::from(revision.as_str()),
],
executable: self.config.git_executable().to_path_buf(),
hooks_path: self.config.hooks_path(),
operation: crate::GitOperation::CreateSnapshot,
};
let run_result = self.runner.run(&specification);
match run_result {
Ok(_) => (),
Err(error) => return Err(error),
}
let validation_result = self.validate_snapshot(&snapshot_path, revision);
match validation_result {
Ok(()) => return Ok(knowledge_git_api::GitSnapshot::new(revision.clone(), snapshot_path)),
Err(error) => return Err(error),
}
}
fn validate_snapshot(&self, snapshot_path: &std::path::Path, revision: &knowledge_git_api::GitRevision) -> Result<(), crate::GitBackendError> {
if !snapshot_path.is_dir() {
return Err(crate::GitBackendError::InvalidSnapshot);
}
let head_specification = GitCommandSpec {
arguments: vec![
std::ffi::OsString::from("-C"),
snapshot_path.as_os_str().to_os_string(),
std::ffi::OsString::from("rev-parse"),
std::ffi::OsString::from("--verify"),
std::ffi::OsString::from("HEAD"),
],
executable: self.config.git_executable().to_path_buf(),
hooks_path: self.config.hooks_path(),
operation: crate::GitOperation::InspectSnapshot,
};
let head_result = self.runner.run(&head_specification);
let head_output = match head_result {
Ok(value) => value,
Err(_) => return Err(crate::GitBackendError::InvalidSnapshot),
};
let head_text_result = std::str::from_utf8(&head_output.stdout);
let head_text = match head_text_result {
Ok(value) => value.trim(),
Err(_) => return Err(crate::GitBackendError::InvalidSnapshot),
};
if head_text != revision.as_str() {
return Err(crate::GitBackendError::InvalidSnapshot);
}
let status_specification = GitCommandSpec {
arguments: vec![
std::ffi::OsString::from("-C"),
snapshot_path.as_os_str().to_os_string(),
std::ffi::OsString::from("status"),
std::ffi::OsString::from("--porcelain=v1"),
std::ffi::OsString::from("--untracked-files=all"),
],
executable: self.config.git_executable().to_path_buf(),
hooks_path: self.config.hooks_path(),
operation: crate::GitOperation::InspectSnapshot,
};
let status_result = self.runner.run(&status_specification);
let status_output = match status_result {
Ok(value) => value,
Err(_) => return Err(crate::GitBackendError::InvalidSnapshot),
};
if !status_output.stdout.is_empty() {
return Err(crate::GitBackendError::InvalidSnapshot);
}
return Ok(());
}
}
impl knowledge_git_api::GitRepository for crate::GitBackend {
type Error = crate::GitBackendError;
fn refresh(&mut self, source_id: &knowledge_domain_lib::SourceId) -> Result<(), Self::Error> {
return self.refresh_registered_source(source_id);
}
fn resolve_revision(
&self,
source_id: &knowledge_domain_lib::SourceId,
reference: &knowledge_git_api::GitReference,
) -> Result<knowledge_git_api::GitRevision, Self::Error> {
return self.resolve_registered_revision(source_id, reference);
}
fn prepare_snapshot(
&self,
source_id: &knowledge_domain_lib::SourceId,
revision: &knowledge_git_api::GitRevision,
) -> Result<knowledge_git_api::GitSnapshot, Self::Error> {
return self.prepare_registered_snapshot(source_id, revision);
}
}
fn is_safe_revision(value: &str) -> bool {
if value.is_empty() {
return false;
}
for character in value.chars() {
if !character.is_ascii_hexdigit() {
return false;
}
}
return true;
}
#[cfg(test)]

View File

@@ -1,5 +1,5 @@
// file: crates/knowledge-git-lib/src/error.rs
// version: 2
// version: 3
//! Recoverable errors for controlled Git process execution.
@@ -10,6 +10,14 @@ pub enum GitOperation {
CloneMirror,
/// Inspect whether a controlled repository is a valid bare mirror.
InspectMirror,
/// Refresh refs in one controlled mirror.
RefreshMirror,
/// Resolve one configured reference to a commit revision.
ResolveRevision,
/// Materialize one detached revision-addressed snapshot.
CreateSnapshot,
/// Validate an existing detached snapshot before reuse.
InspectSnapshot,
}
impl std::fmt::Display for crate::GitOperation {
@@ -17,6 +25,10 @@ impl std::fmt::Display for crate::GitOperation {
match self {
crate::GitOperation::CloneMirror => return formatter.write_str("clone mirror"),
crate::GitOperation::InspectMirror => return formatter.write_str("inspect mirror"),
crate::GitOperation::RefreshMirror => return formatter.write_str("refresh mirror"),
crate::GitOperation::ResolveRevision => return formatter.write_str("resolve revision"),
crate::GitOperation::CreateSnapshot => return formatter.write_str("create snapshot"),
crate::GitOperation::InspectSnapshot => return formatter.write_str("inspect snapshot"),
}
}
}
@@ -33,6 +45,10 @@ pub enum GitBackendError {
EmptyStateRoot,
/// A local source path or remote source string is empty.
EmptySourceLocation,
/// One logical source identifier was registered more than once in the same backend.
DuplicateSource,
/// No physical source configuration is registered for the requested logical source.
SourceNotConfigured,
/// The backend could not prepare its generated-state directories.
StatePreparationFailed,
/// The Git executable could not be started for the requested operation.
@@ -49,6 +65,12 @@ pub enum GitBackendError {
},
/// An existing controlled repository is not a valid bare mirror.
InvalidMirror,
/// A controlled mirror required by a read operation does not exist yet.
MirrorMissing,
/// A revision supplied to snapshot materialization is not a safe Git object identity.
InvalidRevision,
/// An existing snapshot does not match the requested revision or is not clean.
InvalidSnapshot,
/// Git returned output that does not satisfy the backend contract.
UnexpectedGitOutput {
/// Operation whose output could not be validated.
@@ -62,12 +84,17 @@ impl std::fmt::Display for crate::GitBackendError {
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::DuplicateSource => return formatter.write_str("git source identifier is already configured"),
crate::GitBackendError::SourceNotConfigured => return formatter.write_str("git source identifier is not configured"),
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::MirrorMissing => return formatter.write_str("controlled git mirror does not exist"),
crate::GitBackendError::InvalidRevision => return formatter.write_str("git revision is not a safe object identity"),
crate::GitBackendError::InvalidSnapshot => return formatter.write_str("controlled git snapshot is invalid or dirty"),
crate::GitBackendError::UnexpectedGitOutput { operation } => {
return write!(formatter, "git returned unexpected output while attempting to {operation}");
},

View File

@@ -1,15 +1,16 @@
// file: crates/knowledge-git-lib/src/lib.rs
// version: 2
// version: 3
//! 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.
//! This crate owns physical Git source configuration, generated-state layout, provider-neutral read
//! materialization 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.
/// Controlled Git backend that owns configured sources and local read materialization.
pub use self::backend::GitBackend;
/// Explicit physical configuration for one controlled Git backend instance.
pub use self::backend::GitBackendConfig;

View File

@@ -0,0 +1,198 @@
// file: crates/knowledge-git-lib/tests/git_read_path.rs
// version: 1
fn test_root(label: &str) -> std::path::PathBuf {
let nanos_result = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
let nanos = match nanos_result {
Ok(value) => value.as_nanos(),
Err(_) => 0,
};
let mut path = std::env::temp_dir();
path.push(format!("sasedev-knowledge-git-integration-{label}-{}-{nanos}", std::process::id()));
let _ = std::fs::remove_dir_all(&path);
return path;
}
fn run_git(repository: &std::path::Path, arguments: &[&str]) -> bool {
let mut command = std::process::Command::new("git");
command.current_dir(repository);
command.args(arguments);
command.env("GIT_TERMINAL_PROMPT", "0");
let status_result = command.status();
match status_result {
Ok(status) => return status.success(),
Err(_) => return false,
}
}
fn prepare_source(root: &std::path::Path) -> Option<std::path::PathBuf> {
let source = root.join("source");
if std::fs::create_dir_all(&source).is_err() {
return None;
}
if !run_git(&source, &["init", "-b", "main"]) {
return None;
}
if !run_git(&source, &["config", "user.name", "Knowledge Test"]) {
return None;
}
if !run_git(&source, &["config", "user.email", "knowledge@example.invalid"]) {
return None;
}
if !run_git(&source, &["config", "commit.gpgsign", "false"]) {
return None;
}
if std::fs::write(source.join("content.txt"), "first\n").is_err() {
return None;
}
if !run_git(&source, &["add", "content.txt"]) {
return None;
}
if !run_git(&source, &["commit", "-m", "first"]) {
return None;
}
return Some(source);
}
fn source_id() -> Option<knowledge_domain_lib::SourceId> {
let result = knowledge_domain_lib::SourceId::new("integration-source");
match result {
Ok(value) => return Some(value),
Err(_) => return None,
}
}
fn configured_backend(root: &std::path::Path, source: &std::path::Path) -> Option<(knowledge_git_lib::GitBackend, knowledge_domain_lib::SourceId)> {
let source_id_result = source_id();
let source_id = match source_id_result {
Some(value) => value,
None => return None,
};
let config_result = knowledge_git_lib::GitBackendConfig::new(root.join("state"));
let config = match config_result {
Ok(value) => value,
Err(_) => return None,
};
let source_result = knowledge_git_lib::GitSourceConfig::local(source_id.clone(), source.to_path_buf());
let source_config = match source_result {
Ok(value) => value,
Err(_) => return None,
};
let mut backend = knowledge_git_lib::GitBackend::new(config);
if backend.register_source(source_config).is_err() {
return None;
}
return Some((backend, source_id));
}
#[test]
fn real_local_repository_refreshes_resolves_and_materializes_revision_snapshots() {
let root = test_root("refresh-resolve-snapshot");
let source_result = prepare_source(&root);
assert!(source_result.is_some());
let source = match source_result {
Some(value) => value,
None => return,
};
let backend_result = configured_backend(&root, &source);
assert!(backend_result.is_some());
let (mut backend, source_id) = match backend_result {
Some(value) => value,
None => return,
};
let reference = knowledge_git_api::GitReference::new("refs/heads/main");
assert!(knowledge_git_api::GitRepository::refresh(&mut backend, &source_id).is_ok());
let first_revision_result = knowledge_git_api::GitRepository::resolve_revision(&backend, &source_id, &reference);
assert!(first_revision_result.is_ok());
let first_revision = match first_revision_result {
Ok(value) => value,
Err(_) => return,
};
let first_snapshot_result = knowledge_git_api::GitRepository::prepare_snapshot(&backend, &source_id, &first_revision);
assert!(first_snapshot_result.is_ok());
let first_snapshot = match first_snapshot_result {
Ok(value) => value,
Err(_) => return,
};
assert!(first_snapshot.path().is_dir());
let first_content_result = std::fs::read_to_string(first_snapshot.path().join("content.txt"));
assert!(first_content_result.is_ok());
let first_content = match first_content_result {
Ok(value) => value,
Err(_) => return,
};
assert_eq!(first_content, "first\n");
let repeated_snapshot_result = knowledge_git_api::GitRepository::prepare_snapshot(&backend, &source_id, &first_revision);
assert!(repeated_snapshot_result.is_ok());
let repeated_snapshot = match repeated_snapshot_result {
Ok(value) => value,
Err(_) => return,
};
assert_eq!(first_snapshot.path(), repeated_snapshot.path());
assert!(std::fs::write(source.join("content.txt"), "second\n").is_ok());
assert!(run_git(&source, &["add", "content.txt"]));
assert!(run_git(&source, &["commit", "-m", "second"]));
assert!(knowledge_git_api::GitRepository::refresh(&mut backend, &source_id).is_ok());
let second_revision_result = knowledge_git_api::GitRepository::resolve_revision(&backend, &source_id, &reference);
assert!(second_revision_result.is_ok());
let second_revision = match second_revision_result {
Ok(value) => value,
Err(_) => return,
};
assert_ne!(first_revision, second_revision);
let second_snapshot_result = knowledge_git_api::GitRepository::prepare_snapshot(&backend, &source_id, &second_revision);
assert!(second_snapshot_result.is_ok());
let second_snapshot = match second_snapshot_result {
Ok(value) => value,
Err(_) => return,
};
assert_ne!(first_snapshot.path(), second_snapshot.path());
let second_content_result = std::fs::read_to_string(second_snapshot.path().join("content.txt"));
assert!(second_content_result.is_ok());
let second_content = match second_content_result {
Ok(value) => value,
Err(_) => return,
};
assert_eq!(second_content, "second\n");
let _ = std::fs::remove_dir_all(&root);
return;
}
#[test]
fn invalid_reference_and_dirty_existing_snapshot_are_rejected() {
let root = test_root("invalid-and-dirty");
let source_result = prepare_source(&root);
assert!(source_result.is_some());
let source = match source_result {
Some(value) => value,
None => return,
};
let backend_result = configured_backend(&root, &source);
assert!(backend_result.is_some());
let (mut backend, source_id) = match backend_result {
Some(value) => value,
None => return,
};
assert!(knowledge_git_api::GitRepository::refresh(&mut backend, &source_id).is_ok());
let missing_reference = knowledge_git_api::GitReference::new("refs/heads/does-not-exist");
let missing_result = knowledge_git_api::GitRepository::resolve_revision(&backend, &source_id, &missing_reference);
assert!(missing_result.is_err());
let reference = knowledge_git_api::GitReference::new("refs/heads/main");
let revision_result = knowledge_git_api::GitRepository::resolve_revision(&backend, &source_id, &reference);
assert!(revision_result.is_ok());
let revision = match revision_result {
Ok(value) => value,
Err(_) => return,
};
let snapshot_result = knowledge_git_api::GitRepository::prepare_snapshot(&backend, &source_id, &revision);
assert!(snapshot_result.is_ok());
let snapshot = match snapshot_result {
Ok(value) => value,
Err(_) => return,
};
assert!(std::fs::write(snapshot.path().join("content.txt"), "dirty\n").is_ok());
let repeated_result = knowledge_git_api::GitRepository::prepare_snapshot(&backend, &source_id, &revision);
assert_eq!(repeated_result, Err(knowledge_git_lib::GitBackendError::InvalidSnapshot));
let _ = std::fs::remove_dir_all(&root);
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/knowledge-git-lib/unit_tests/backend.rs
// version: 3
// version: 4
#[derive(Clone)]
struct RecordingRunner {
@@ -72,6 +72,34 @@ fn backend_configuration_requires_non_empty_paths() {
return;
}
#[test]
fn duplicate_source_registration_is_rejected() {
let root = test_root("duplicate-source");
let config_result = crate::GitBackendConfig::new(root.clone());
assert!(config_result.is_ok());
let config = match config_result {
Ok(value) => value,
Err(_) => return,
};
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 mut backend = crate::GitBackend::new(config);
assert_eq!(backend.register_source(source.clone()), Ok(()));
assert_eq!(backend.register_source(source), Err(crate::GitBackendError::DuplicateSource));
let _ = std::fs::remove_dir_all(root);
return;
}
#[test]
fn source_key_never_interpolates_raw_source_identifier_as_a_path() {
let source_result = source_id("../pgr/credential:value");

298
deltas/0.0.2/alpha.3.md Normal file
View File

@@ -0,0 +1,298 @@
<!-- file: deltas/0.0.2/alpha.3.md -->
<!-- version: 1 -->
# Delta 0.0.2-alpha.3 — refresh, résolution et snapshot Git réels
## Base requise
Partir de `0.0.2-alpha.2.fix.2` après application, validation, history `PASS`, commit et push :
```text
deltas/0.0.2/alpha.2.md présent et historique
deltas/0.0.2/alpha.2.fix.1.md présent et historique
deltas/0.0.2/alpha.2.fix.2.md présent et historique
history/0.0.2/alpha.2.md FAIL
history/0.0.2/alpha.2.fix.1.md FAIL
history/0.0.2/alpha.2.fix.2.md PASS
```
Les deltas et histories antérieurs ne doivent pas être réécrits.
La version devient :
```text
0.0.2-alpha.3
```
## Scope
Cette tranche complète la matérialisation Git de lecture déjà prévue par le plan :
- faire implémenter réellement `knowledge-git-api::GitRepository` par `knowledge-git-lib` ;
- enregistrer explicitement les configurations physiques des sources dans `GitBackend` sans les faire remonter dans le domaine ou l'API provider-neutral ;
- ajouter au contrat uniquement `GitSnapshot` et `prepare_snapshot`, maintenant nécessaires ;
- initialiser ou rafraîchir le mirror avec pruning ;
- résoudre une référence opaque vers un commit réel ;
- matérialiser un worktree détaché adressé par la révision résolue ;
- valider un snapshot existant avant réutilisation ;
- prouver le chemin avec de vrais repositories Git temporaires locaux.
Cette tranche ne matérialise pas encore le cas d'usage `Changed` / `Unchanged` d'`alpha.4`.
## Changements apportés
### Contrat provider-neutral
`knowledge-git-api` ajoute `GitSnapshot`, contenant uniquement :
```text
GitRevision
PathBuf de lecture locale
```
`GitRepository` gagne :
```text
prepare_snapshot(source_id, revision)
```
Aucune API de forge, notion de remote, commande Git ou configuration physique n'entre dans le contrat.
### Backend configuré
`GitBackend` conserve sa configuration globale puis enregistre explicitement des `GitSourceConfig` par `SourceId`.
Un doublon est refusé par une erreur récupérable. Une opération provider-neutral sur une source inconnue échoue également sans exposer son URL ou son chemin.
### Refresh
Le refresh :
1. vérifie que la source logique possède une configuration physique enregistrée ;
2. initialise le mirror s'il n'existe pas encore ;
3. exécute ensuite :
```text
git --git-dir <mirror> remote update --prune
```
Aucune API de forge n'est nécessaire.
### Résolution
La résolution exige un mirror existant et utilise :
```text
git --git-dir <mirror> rev-parse --verify --end-of-options <reference>^{commit}
```
La sortie produite par le backend doit être non vide et hexadécimale avant de devenir `GitRevision`. Aucune longueur SHA particulière n'est figée.
### Snapshot
Le chemin de snapshot reste :
```text
<state-root>/snapshots/<source-key>/<resolved-revision>/
```
Un snapshot absent est créé par :
```text
git --git-dir <mirror> worktree add --detach -- <snapshot-path> <resolved-revision>
```
Un snapshot existant n'est réutilisé qu'après :
```text
git -C <snapshot> rev-parse --verify HEAD
git -C <snapshot> status --porcelain=v1 --untracked-files=all
```
Le `HEAD` doit correspondre exactement à la révision demandée et le statut doit être vide. Une divergence ou un snapshot sale est rejeté sans suppression automatique.
### Tests réels
`crates/knowledge-git-lib/tests/git_read_path.rs` crée un repository Git local temporaire réel et vérifie notamment :
- initialisation du mirror via la voie `refresh` ;
- résolution de `refs/heads/main` ;
- création puis réutilisation idempotente d'un snapshot ;
- ajout d'un second commit source, refresh et nouvelle révision ;
- snapshot distinct pour la nouvelle révision ;
- rejet d'une référence inexistante ;
- rejet d'un snapshot existant rendu sale.
Ces tests exercent la frontière `knowledge_git_api::GitRepository` et le client Git réel. Les mocks existants restent uniquement des tests unitaires des arguments/processus.
## Hors scope confirmé
La tranche n'ajoute pas :
- `knowledge-app-lib` ;
- scheduler ;
- persistance de dernière révision ;
- PostgreSQL ou migrations ;
- adapter Gitea/GitHub/GitLab ;
- webhook ;
- status/diff public, branch, commit, merge, rebase ou push ;
- dépendance Git Rust secondaire ;
- changement sous `Web/` ;
- changement de rendu ou de route HTTP.
## Gate opérateur
Exécuter depuis la racine du workspace après application du delta :
```bash
test "$(cat VERSION)" = '0.0.2-alpha.3'
grep -F 'version = "0.0.2-alpha.3"' Cargo.toml
grep -A2 '^## Statut$' history/0.0.2/alpha.2.fix.2.md | grep -Fx 'PASS'
git diff --check
python3 - <<'PY'
from pathlib import Path
import tomllib
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 len(root["workspace"]["members"]) == 8
assert set(manifest["dependencies"]) == {"knowledge-domain-lib", "knowledge-git-api"}
api = Path("crates/knowledge-git-api/src/lib.rs").read_text(encoding="utf-8")
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")
integration = Path("crates/knowledge-git-lib/tests/git_read_path.rs").read_text(encoding="utf-8")
for phrase in (
"pub struct GitSnapshot",
"fn prepare_snapshot(",
):
assert phrase in api, phrase
for phrase in (
"impl knowledge_git_api::GitRepository for crate::GitBackend",
"pub fn register_source(",
'std::ffi::OsString::from("remote")',
'std::ffi::OsString::from("update")',
'std::ffi::OsString::from("--prune")',
'std::ffi::OsString::from("rev-parse")',
'std::ffi::OsString::from("--verify")',
'std::ffi::OsString::from("--end-of-options")',
'std::ffi::OsString::from("worktree")',
'std::ffi::OsString::from("add")',
'std::ffi::OsString::from("--detach")',
'std::ffi::OsString::from("status")',
'std::ffi::OsString::from("--porcelain=v1")',
'std::ffi::OsString::from("--untracked-files=all")',
):
assert phrase in backend, phrase
for phrase in (
"DuplicateSource",
"SourceNotConfigured",
"MirrorMissing",
"InvalidRevision",
"InvalidSnapshot",
"RefreshMirror",
"ResolveRevision",
"CreateSnapshot",
"InspectSnapshot",
):
assert phrase in error, phrase
for phrase in (
"real_local_repository_refreshes_resolves_and_materializes_revision_snapshots",
"invalid_reference_and_dirty_existing_snapshot_are_rejected",
"knowledge_git_api::GitRepository::refresh",
"knowledge_git_api::GitRepository::resolve_revision",
"knowledge_git_api::GitRepository::prepare_snapshot",
):
assert phrase in integration, phrase
for forbidden in (
'Command::new("sh")',
'Command::new("bash")',
'Command::new("cmd")',
'Command::new("powershell")',
"knowledge-app-lib",
"knowledge-forge-gitea-lib",
"knowledge-store-postgres-lib",
):
assert forbidden not in backend + error, forbidden
print("Knowledge 0.0.2-alpha.3 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
git --version
cargo fmt --all -- --check
cargo metadata --format-version 1 --no-deps > /tmp/sasedev-knowledge-0.0.2-alpha.3-metadata.json
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p knowledge-git-api --all-targets --all-features
cargo test -p knowledge-git-lib --all-targets --all-features
cargo test -p knowledge-git-lib --test git_read_path
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.3-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.3 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-sync-api)"
test ! -e crates/knowledge-app-lib
test ! -e crates/knowledge-forge-gitea-lib
test ! -e crates/knowledge-store-postgres-lib
test ! -e history/0.0.2/alpha.3.md
git add -A
git diff --cached --check
git status --short
```
## Résultat attendu
La tranche est `PASS` uniquement si :
- le formatage, la compilation et Clippy strict sont propres ;
- le test `knowledge-git-api` passe ;
- les tests unitaires de `knowledge-git-lib` passent ;
- les deux tests Git réels de `git_read_path` passent ;
- les tests workspace passent ;
- aucun changement hors scope n'est observé ;
- aucun history `alpha.3` n'est créé avant réception des résultats opérateur.
Un `FAIL`, `PARTIAL` ou `BLOCKED` doit être historisé avant tout fix. Un `PASS` autorise uniquement `0.0.2-alpha.4`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/001-PROJECT_PROFILE.md -->
<!-- version: 11 -->
<!-- version: 12 -->
# Profil du projet Knowledge
@@ -127,7 +127,7 @@ 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-git-lib implémentation Git générique + mirror/refresh/resolve/snapshot + é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
@@ -135,7 +135,7 @@ knowledge-howtos-server launcher HOWTOs mince
knowledge-snippets-server launcher snippets mince
```
`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é.
`knowledge-git-lib` devient réelle en `0.0.2-alpha.2` puis implémente le chemin de lecture Git dans `alpha.3`. `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: 11 -->
<!-- version: 12 -->
# Architecture de fondation Knowledge
@@ -149,7 +149,7 @@ Pendant `0.0.2`, une racine d'état appartient exclusivement à une instance bac
### Mirror et refresh
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.
Une source locale ou remote converge vers un mirror Git contrôlé. `alpha.2` implémente l'initialisation et l'inspection bare minimale ; `alpha.3` ajoute l'enregistrement explicite des sources physiques, le refresh réel, la résolution et le snapshot derrière `GitRepository`.
L'initialisation utilise conceptuellement :
@@ -159,7 +159,7 @@ git clone --mirror [--no-local pour une source locale] -- <source> <mirror>
Pour une source locale, `--no-local` évite de créer une dépendance par hardlinks/optimisation locale sur le repository éditorial.
Le refresh réutilise la configuration du remote du mirror et met les refs à jour avec pruning. L'opération doit être idempotente et ne dépend d'aucune API de forge.
Le refresh réutilise la configuration du remote du mirror et exécute `remote update --prune`. L'opération est idempotente, initialise le mirror s'il n'existe pas encore et ne dépend d'aucune API de forge.
### Résolution et snapshot
@@ -169,15 +169,15 @@ Une référence cible n'est jamais considérée comme identité autoritaire avan
git --git-dir=<mirror> rev-parse --verify --end-of-options <reference>^{commit}
```
La valeur obtenue devient `GitRevision`.
La valeur obtenue devient `GitRevision`. Le backend valide que l'identité produite est une représentation hexadécimale non vide avant de l'utiliser comme composant de chemin, sans figer une longueur SHA particulière.
Le snapshot de lecture est ensuite créé comme worktree détaché adressé par cette révision. Un snapshot déjà présent n'est réutilisé qu'après vérification de son `HEAD` et d'un état propre ; une divergence produit une erreur récupérable plutôt qu'un nettoyage destructif silencieux.
Le snapshot de lecture est ensuite créé comme worktree détaché adressé par cette révision. `GitSnapshot` expose uniquement la révision autoritaire et le chemin local de lecture. Un snapshot déjà présent n'est réutilisé qu'après vérification de son `HEAD` et d'un état propre ; une divergence produit une erreur récupérable plutôt qu'un nettoyage destructif silencieux.
Les hooks Git sont désactivés pour les opérations de checkout/worktree appartenant au backend. Les commandes sont exécutées directement sans shell, avec prompts interactifs désactivés et sans journaliser de credential.
### Scope futur du contrat Git
Les opérations génériques à plus long terme peuvent inclure `status/diff`, branch, commit, merge/rebase et push. Elles ne sont pas ajoutées en `0.0.2` : la release n'étend le contrat que pour la matérialisation de lecture réellement nécessaire.
`alpha.3` étend `GitRepository` uniquement avec `prepare_snapshot`, parce que la matérialisation locale devient effectivement nécessaire au cas d'usage de lecture. Les opérations génériques à plus long terme peuvent inclure `status/diff`, branch, commit, merge/rebase et push. Elles ne sont pas ajoutées en `0.0.2`.
La baseline conserve au moins trois sources indépendantes (`pgr`, `howtos`, `snippets`) mais le mapping reste configurable et extensible ; aucun `match` codé en dur n'impose exactement trois repositories.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/002-V0_0_2_GIT_SYNC_PLAN.md -->
<!-- version: 5 -->
<!-- version: 6 -->
# Plan 0.0.2 — Git générique et synchronisation de lecture
@@ -143,7 +143,7 @@ Statut opérateur initial : **FAIL**.
`alpha.2.fix.1` corrige l'hygiène de fin de fichier, la portée de la gate Markdown, le formatage rustfmt et la construction interne de `GitSourceKey`, mais sa gate reste **FAIL** sur une unique violation `clippy::implicit_return` dans une closure de test.
`alpha.2.fix.2` corrige exclusivement ce retour implicite, sans modification du backend, de l'API, des dépendances ou du comportement Git.
`alpha.2.fix.2` corrige exclusivement ce retour implicite, sans modification du backend, de l'API, des dépendances ou du comportement Git. Sa gate opérateur est **PASS** et ferme `alpha.2`.
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`.
@@ -164,17 +164,22 @@ Si la surface « configuration + runner + mirror » ne peut plus être validée
### alpha.3 — refresh, résolution et snapshot réel
Compléter le chemin de lecture Git sur de vrais repositories temporaires :
Statut opérateur : **à valider**.
- refresh idempotent du mirror ;
- résolution typée d'une référence vers un commit ;
La tranche complète le chemin de lecture Git sur de vrais repositories temporaires :
- enregistrer explicitement les configurations physiques des sources dans `GitBackend` afin d'implémenter réellement le contrat provider-neutral ;
- faire implémenter `knowledge-git-api::GitRepository` par `knowledge-git-lib` ;
- étendre le contrat uniquement avec `GitSnapshot` / `prepare_snapshot`, maintenant nécessaires à la matérialisation de lecture ;
- refresh idempotent du mirror via `remote update --prune` ;
- résolution typée d'une référence vers un commit avec `rev-parse --verify --end-of-options ...^{commit}` ;
- refus des références invalides ou non résolubles ;
- préparation d'un worktree/snapshot détaché adressé par révision ;
- validation d'un snapshot existant avant réutilisation ;
- validation du `HEAD` et de l'état propre d'un snapshot existant avant réutilisation ;
- preuve d'un changement de révision après ajout d'un commit source ;
- preuve qu'aucune API de forge n'est nécessaire.
Les tests doivent exercer la frontière publique du backend et les effets Git réels. Une imitation complète de `git` par mocks ne constitue pas la preuve principale de cette tranche.
Les tests doivent exercer la frontière `GitRepository` et les effets Git réels. Une imitation complète de `git` par mocks ne constitue pas la preuve principale de cette tranche. Aucun `knowledge-app-lib`, scheduler ou état persistant n'entre dans `alpha.3`.
### alpha.4 — cas d'usage de synchronisation

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/001-VALIDATION_MATRIX.md -->
<!-- version: 22 -->
<!-- version: 23 -->
# Matrice de validation Knowledge
@@ -610,3 +610,30 @@ Le second fix reste strictement dans le scope `alpha.2` et doit prouver :
- `git diff --cached --check` propre avant commit.
Le `PASS` de `alpha.2.fix.2` ferme `alpha.2` et autorise uniquement la préparation de `0.0.2-alpha.3`.
## Gate de `0.0.2-alpha.3`
La tranche de lecture Git réelle doit prouver :
- `VERSION = 0.0.2-alpha.3` et `[workspace.package].version` identique ;
- history `history/0.0.2/alpha.2.fix.2.md` présent avec statut `PASS` et committé avant application ;
- huit packages Cargo exactement ;
- `knowledge-git-lib` dépend désormais de `knowledge-domain-lib` et `knowledge-git-api`, sans autre dépendance ;
- `knowledge-git-api` étendu uniquement avec le contrat `GitSnapshot` / `prepare_snapshot` nécessaire à la lecture ;
- `GitBackend` implémente `knowledge_git_api::GitRepository` et possède l'enregistrement explicite des configurations physiques de source ;
- doublons de `SourceId` refusés et source non configurée signalée sans exposer sa localisation ;
- refresh du mirror par `git --git-dir <mirror> remote update --prune` ;
- résolution par `rev-parse --verify --end-of-options <reference>^{commit}` ;
- révision backend validée comme identité hexadécimale non vide avant usage dans un chemin, sans longueur SHA figée ;
- snapshot créé par `worktree add --detach -- <path> <revision>` sous `snapshots/<source-key>/<revision>` ;
- snapshot existant réutilisé seulement si son `HEAD` correspond exactement à la révision et si `status --porcelain=v1 --untracked-files=all` est vide ;
- snapshot divergent ou sale refusé sans suppression silencieuse ;
- tests avec vrais repositories Git temporaires prouvant initialisation/refresh/résolution/snapshot, réutilisation idempotente, changement de commit, référence invalide et snapshot sale ;
- aucune API de forge, dépendance réseau Rust, PostgreSQL, scheduler, `knowledge-app-lib` ou écriture/push Git ajoutée ;
- `cargo fmt --all -- --check`, audits Rust, `cargo metadata`, `cargo check`, Clippy strict, tests ciblés et tests workspace propres ;
- `Web/` et `knowledge-sync-api` sans diff ;
- `Cargo.lock` toujours ignoré/non suivi ;
- `git diff --cached --check` propre avant commit.
Le `PASS` de `alpha.3` autorise uniquement la préparation de `0.0.2-alpha.4`, consacrée au cas d'usage de synchronisation `Changed` / `Unchanged`.

171
history/0.0.2/alpha.3.md Normal file
View File

@@ -0,0 +1,171 @@
<!-- file: history/0.0.2/alpha.3.md -->
<!-- version: 1 -->
# History 0.0.2-alpha.3 — refresh, résolution et snapshot Git réels
## Statut
FAIL
La gate opérateur de `0.0.2-alpha.3` exécutée le `2026-09-28` confirme le comportement fonctionnel du chemin Git réel, mais la tranche reste bloquée par trois défauts de conformité : hygiène Git, formatage rustfmt et documentation de la crate de test d'intégration sous Clippy strict.
## Baseline opérateur
`alpha.2.fix.2` a été historisé `PASS`, committé et poussé avant application d'`alpha.3` :
```text
history/0.0.2/alpha.2.fix.2.md PASS
commit 3802526
message 0.0.2-alpha.2.fix.2
branch master
push origin/master PASS
workspace après push propre
```
Le push observé a avancé `origin/master` de `ffb4721` à `3802526`.
## Version et audit structurel
Les contrôles initiaux passent :
```text
VERSION 0.0.2-alpha.3
Cargo workspace version 0.0.2-alpha.3
history alpha.2.fix.2 PASS
Knowledge 0.0.2-alpha.3 structural audit: clean
```
Les audits Rust structurels passent également :
```text
Rust test-layout audit: clean (8 crate(s))
Rust blank-line audit: clean (23 file(s))
Rust item-spacing audit: clean (23 file(s))
Rust module-order audit: clean (23 file(s))
Rust trait-import-block audit: clean (23 file(s))
Rust re-export-block audit: clean (23 file(s))
Rust export completeness audit: 0 candidate(s) across 8 package(s)
```
## Hygiène Git et formatage
`git diff --check` échoue sur :
```text
docs/validation/001-VALIDATION_MATRIX.md:639: new blank line at EOF.
```
`cargo fmt --all -- --check` échoue uniquement sur `crates/knowledge-git-api/unit_tests/lib.rs`, où rustfmt compacte l'assertion finale `GitSnapshot` sur une seule ligne.
Ces deux défauts sont bloquants même si la compilation réussit ensuite.
## Compilation et Clippy
`cargo metadata --format-version 1 --no-deps` et `cargo check --workspace` passent.
Clippy strict échoue sur :
```text
crates/knowledge-git-lib/tests/git_read_path.rs
missing documentation for the crate
-D missing-docs
```
Le test d'intégration constitue une crate Cargo indépendante ; il doit donc posséder une documentation de crate explicite compatible avec la politique `missing_docs` du workspace.
## Tests fonctionnels Git
Le contrat provider-neutral compile et son test passe :
```text
cargo test -p knowledge-git-api --all-targets --all-features
1 passed; 0 failed
```
`knowledge-git-lib` exécute huit tests unitaires et deux tests d'intégration avec de vrais repositories Git temporaires :
```text
8 unit tests PASS
2 integration tests PASS
```
Les preuves observées couvrent notamment :
- enregistrement de source et rejet des doublons ;
- refresh du mirror réel ;
- résolution de `refs/heads/main` ;
- matérialisation d'un snapshot par révision ;
- second commit source puis nouvelle révision/snapshot ;
- rejet d'une référence inexistante ;
- rejet d'un snapshot existant rendu sale.
Le test d'intégration ciblé `cargo test -p knowledge-git-lib --test git_read_path` passe également avec `2 passed; 0 failed`.
Les tests workspace passent intégralement malgré le warning `missing_docs` non promu en erreur par `cargo test` :
```text
knowledge-domain-lib : 3 PASS
knowledge-git-api : 1 PASS
knowledge-git-lib : 8 unit PASS + 2 integration PASS
knowledge-sync-api : 1 PASS
knowledge-web-lib : 4 PASS
launchers : 0 test chacun
```
## Métadonnées et frontières
L'audit Cargo passe :
```text
Knowledge 0.0.2-alpha.3 Cargo metadata audit: clean
```
Les contrôles de frontière passent :
```text
Web/ 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
history/0.0.2/alpha.3.md absent avant clôture
Cargo.lock présent localement, ignoré et non suivi
```
Le `git diff --cached --check` final échoue pour la même ligne vide terminale déjà signalée dans la matrice. La commande finale `git status --short` n'a pas été exécutée en raison d'une faute de frappe opérateur (`it status --short`), sans effet sur la décision déjà acquise.
## Replanification décidée
Pendant la revue de la tranche, deux besoins structurels ont été confirmés sans remettre en cause les primitives Git d'`alpha.3` :
1. Knowledge doit devenir autonome après son adoption PGR : seules les règles réellement retenues doivent être adaptées et matérialisées localement ; PGR `v0.1.3` reste une provenance d'adoption et ne doit pas être rechargé automatiquement au début de chaque version, session ou tranche.
2. Les crates Rust doivent être auditées afin que `lib.rs` reste une façade de crate et que les définitions soient possédées par des modules nommés selon leur responsabilité lorsque la taille/structure le justifie, en particulier pour les crates `-api`.
Ces sujets ne corrigent pas le comportement Git d'`alpha.3` et ne doivent donc pas être absorbés dans son implémentation. Le plan `0.0.2` est replanifié pour insérer une tranche `alpha.4` dédiée à cette autonomie normative et à l'organisation interne des crates ; l'ancien cas d'usage de synchronisation devient `alpha.5`.
## Diagnostic
`alpha.3` est fonctionnellement probante mais ne satisfait pas la gate complète.
Le correctif doit uniquement :
- supprimer la ligne vide terminale fautive de la matrice ;
- appliquer le formatage rustfmt attendu au test de `knowledge-git-api` ;
- ajouter la documentation de crate requise au test d'intégration `git_read_path` ;
- enregistrer explicitement le REPLAN `alpha.4` / `alpha.5` et la règle d'autonomie PGR ;
- ne pas élargir le comportement Git déjà validé par les tests.
## Décision
`0.0.2-alpha.3` est `FAIL`.
La transition autorisée est :
```text
appliquer ce history
-> git add / diff --cached --check / status
-> commit exact "0.0.2-alpha.3"
-> push origin master
-> vérifier le workspace propre
-> appliquer seulement ensuite 0.0.2-alpha.3.fix.1
```