0.0.2-alpha.2
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 18
|
# version: 19
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = [
|
members = [
|
||||||
"crates/knowledge-domain-lib",
|
"crates/knowledge-domain-lib",
|
||||||
"crates/knowledge-git-api",
|
"crates/knowledge-git-api",
|
||||||
|
"crates/knowledge-git-lib",
|
||||||
"crates/knowledge-sync-api",
|
"crates/knowledge-sync-api",
|
||||||
"crates/knowledge-web-lib",
|
"crates/knowledge-web-lib",
|
||||||
"crates/knowledge-pgr-server",
|
"crates/knowledge-pgr-server",
|
||||||
@@ -14,7 +15,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.2-alpha.1.fix.1"
|
version = "0.0.2-alpha.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = false
|
publish = false
|
||||||
repository = "https://git.sasedev.com/Sasedev/sasedev-knowledge"
|
repository = "https://git.sasedev.com/Sasedev/sasedev-knowledge"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: README.md -->
|
<!-- file: README.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# SaseDev Knowledge
|
# 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).
|
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
|
## 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.
|
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
|
## Frontend de référence et rendu Maud
|
||||||
|
|
||||||
|
|||||||
6
RULES.md
6
RULES.md
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: RULES.md -->
|
<!-- file: RULES.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# Règles SaseDev Knowledge
|
# 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.
|
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é.
|
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é.
|
||||||
|
|
||||||
|
|||||||
15
crates/knowledge-git-lib/Cargo.toml
Normal file
15
crates/knowledge-git-lib/Cargo.toml
Normal 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
|
||||||
379
crates/knowledge-git-lib/src/backend.rs
Normal file
379
crates/knowledge-git-lib/src/backend.rs
Normal 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;
|
||||||
78
crates/knowledge-git-lib/src/error.rs
Normal file
78
crates/knowledge-git-lib/src/error.rs
Normal 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 {}
|
||||||
29
crates/knowledge-git-lib/src/lib.rs
Normal file
29
crates/knowledge-git-lib/src/lib.rs
Normal 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;
|
||||||
283
crates/knowledge-git-lib/unit_tests/backend.rs
Normal file
283
crates/knowledge-git-lib/unit_tests/backend.rs
Normal 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
324
deltas/0.0.2/alpha.2.md
Normal 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.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/001-PROJECT_PROFILE.md -->
|
<!-- file: docs/architecture/001-PROJECT_PROFILE.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# Profil du projet Knowledge
|
# Profil du projet Knowledge
|
||||||
|
|
||||||
@@ -78,55 +78,56 @@ Aucun `OVERRIDE` n'est requis par l'adoption `v0.1.3`.
|
|||||||
|
|
||||||
## Matrice des décisions importantes
|
## Matrice des décisions importantes
|
||||||
|
|
||||||
| Décision | Classification | Contrat retenu / condition de reprise |
|
| Décision | Classification | Contrat retenu / condition de reprise |
|
||||||
|----------------------------------------|-----------------------------------|----------------------------------------------------------------------------------------------------------|
|
|----------------------------------------|-----------------------------------|------------------------------------------------------------------------------------------------------------------|
|
||||||
| Dépôt applicatif | `KEEP-LOCAL` | `sasedev-knowledge` ; workspace unique de la plateforme |
|
| Dépôt applicatif | `KEEP-LOCAL` | `sasedev-knowledge` ; workspace unique de la plateforme |
|
||||||
| Nom court / préfixe crates | `KEEP-LOCAL` | `knowledge` / `knowledge-` |
|
| 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 |
|
| Dépôt normatif | `KEEP-LOCAL` | dépôt existant `pgr`, jamais fusionné dans le dépôt applicatif |
|
||||||
| Dépôt HOWTO | `KEEP-LOCAL` | dépôt séparé `howtos` |
|
| Dépôt HOWTO | `KEEP-LOCAL` | dépôt séparé `howtos` |
|
||||||
| Dépôt snippets | `KEEP-LOCAL` | dépôt séparé `snippets` |
|
| Dépôt snippets | `KEEP-LOCAL` | dépôt séparé `snippets` |
|
||||||
| Nombre de sources Git | `KEEP-LOCAL` | au moins trois sources initiales, sans limite codée à trois |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| Forge | `KEEP-LOCAL` | capacité optionnelle derrière adapter ; aucune API de forge dans le domaine |
|
||||||
| Gitea | `DEFER` | premier adapter candidat ; reprendre lorsque des capacités forge-only sont implémentées |
|
| Gitea | `DEFER` | premier adapter candidat ; reprendre lorsque des capacités forge-only sont implémentées |
|
||||||
| Synchronisation sans webhook | `KEEP-LOCAL` | explicite/périodique convergent vers le même cas d'usage ; hooks/webhooks restent accélérateurs |
|
| Synchronisation sans webhook | `KEEP-LOCAL` | explicite/périodique convergent vers le même cas d'usage ; hooks/webhooks restent accélérateurs |
|
||||||
| Scheduler périodique concret | `DEFER` | créer seulement lorsque son owner runtime/lifecycle est démontré |
|
| Scheduler périodique concret | `DEFER` | créer seulement lorsque son owner runtime/lifecycle est démontré |
|
||||||
| Dernière révision observée en `0.0.2` | `KEEP-LOCAL` | fournie par l'appelant et retournée par le cas d'usage ; pas de pseudo-persistance locale |
|
| Dernière révision observée en `0.0.2` | `KEEP-LOCAL` | fournie par l'appelant et retournée par le cas d'usage ; pas de pseudo-persistance locale |
|
||||||
| PostgreSQL | `KEEP-LOCAL` | une base logique partagée future pour état transversal/projections ; jamais vérité du contenu Git publié |
|
| PostgreSQL | `KEEP-LOCAL` | une base logique partagée future pour état transversal/projections ; jamais vérité du contenu Git publié |
|
||||||
| Rust stable / édition 2024 | `ADOPT` | profil Rust/Cargo PGR |
|
| Rust stable / édition 2024 | `ADOPT` | profil Rust/Cargo PGR |
|
||||||
| Workspace Cargo | `ADOPT` | skeleton Rust/Cargo déjà matérialisé |
|
| Workspace Cargo | `ADOPT` | skeleton Rust/Cargo déjà matérialisé |
|
||||||
| `Cargo.lock` | `KEEP-LOCAL` | généré localement et ignoré ; manifests Cargo versionnés |
|
| `Cargo.lock` | `KEEP-LOCAL` | généré localement et ignoré ; manifests Cargo versionnés |
|
||||||
| Actix Web | `KEEP-LOCAL` | trois launchers existants et composition partagée dans `knowledge-web-lib` |
|
| Actix Web | `KEEP-LOCAL` | trois launchers existants et composition partagée dans `knowledge-web-lib` |
|
||||||
| Maud | `KEEP-LOCAL` | rendu SSR commun dans `knowledge-web-lib` |
|
| Maud | `KEEP-LOCAL` | rendu SSR commun dans `knowledge-web-lib` |
|
||||||
| TypeScript | `ADOPT` | owner `530` |
|
| TypeScript | `ADOPT` | owner `530` |
|
||||||
| Package manager frontend | `KEEP-LOCAL` | npm ; `package-lock.json` versionné |
|
| Package manager frontend | `KEEP-LOCAL` | npm ; `package-lock.json` versionné |
|
||||||
| Vite | `KEEP-LOCAL` + owner `550` adopté | build/preview de références et manifest consommé par Maud |
|
| Vite | `KEEP-LOCAL` + owner `550` adopté | build/preview de références et manifest consommé par Maud |
|
||||||
| Sass | `KEEP-LOCAL` + owner `560` adopté | styles source et assets réellement servis |
|
| Sass | `KEEP-LOCAL` + owner `560` adopté | styles source et assets réellement servis |
|
||||||
| Bootstrap | `KEEP-LOCAL` | framework CSS local Knowledge, jamais règle PGR universelle |
|
| Bootstrap | `KEEP-LOCAL` | framework CSS local Knowledge, jamais règle PGR universelle |
|
||||||
| Racine frontend | `KEEP-LOCAL` | package partagé `Web/knowledge-template-lab/` |
|
| Racine frontend | `KEEP-LOCAL` | package partagé `Web/knowledge-template-lab/` |
|
||||||
| Templates statiques Web | `KEEP-LOCAL` | harness de conception/validation ; rendu runtime Maud |
|
| Templates statiques Web | `KEEP-LOCAL` | harness de conception/validation ; rendu runtime Maud |
|
||||||
| Intégration manifest Vite / Maud | `KEEP-LOCAL` | matérialisée dans `knowledge-web-lib` |
|
| Intégration manifest Vite / Maud | `KEEP-LOCAL` | matérialisée dans `knowledge-web-lib` |
|
||||||
| Driver/ORM PostgreSQL | `DEFER` | reprendre à la première implémentation persistante réelle |
|
| Driver/ORM PostgreSQL | `DEFER` | reprendre à la première implémentation persistante réelle |
|
||||||
| Migrations PostgreSQL | `DEFER` | reprendre avec le premier schéma réel |
|
| Migrations PostgreSQL | `DEFER` | reprendre avec le premier schéma réel |
|
||||||
| Moteur de recherche | `DEFER` | commencer par capacités PostgreSQL/index applicatif lorsque ce scope sera engagé |
|
| Moteur de recherche | `DEFER` | commencer par capacités PostgreSQL/index applicatif lorsque ce scope sera engagé |
|
||||||
| Fournisseur d'auth externe | `DEFER` | reprendre lorsque le workflow utilisateur l'exige |
|
| Fournisseur d'auth externe | `DEFER` | reprendre lorsque le workflow utilisateur l'exige |
|
||||||
| Surface admin par application | `KEEP-LOCAL` | chaque serveur protège ses opérations privilégiées ; URL exacte différée |
|
| Surface admin par application | `KEEP-LOCAL` | chaque serveur protège ses opérations privilégiées ; URL exacte différée |
|
||||||
| Quatrième application d'administration | `DEFER` | agréger seulement si une responsabilité transverse apparaît |
|
| Quatrième application d'administration | `DEFER` | agréger seulement si une responsabilité transverse apparaît |
|
||||||
| Modèle rôles/capabilities | `DEFER` | reprendre avec les cas d'usage d'autorisation réels |
|
| Modèle rôles/capabilities | `DEFER` | reprendre avec les cas d'usage d'autorisation réels |
|
||||||
| Formats finaux HOWTO/snippet | `DEFER` | définir avec corpus réel |
|
| Formats finaux HOWTO/snippet | `DEFER` | définir avec corpus réel |
|
||||||
| Autres noms de crates | `DEFER` | créer uniquement lorsque l'ownership est démontré |
|
| Autres noms de crates | `DEFER` | créer uniquement lorsque l'ownership est démontré |
|
||||||
|
|
||||||
## Nomenclature de crates
|
## Nomenclature de crates
|
||||||
|
|
||||||
Les packages matérialisés dans la stable `0.0.1` sont :
|
Les packages matérialisés sont :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
knowledge-domain-lib types métier stables de famille/source
|
knowledge-domain-lib types métier stables de famille/source
|
||||||
knowledge-git-api contrat passif GitRepository provider-neutral
|
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-sync-api contrat passif RepositoryEventSource
|
||||||
knowledge-web-lib composition Actix/Maud/Vite partagée
|
knowledge-web-lib composition Actix/Maud/Vite partagée
|
||||||
knowledge-pgr-server launcher PGR mince
|
knowledge-pgr-server launcher PGR mince
|
||||||
@@ -134,12 +135,7 @@ knowledge-howtos-server launcher HOWTOs mince
|
|||||||
knowledge-snippets-server launcher snippets mince
|
knowledge-snippets-server launcher snippets mince
|
||||||
```
|
```
|
||||||
|
|
||||||
`0.0.2-alpha.1` ferme deux décisions d'ownership sans créer de placeholder :
|
`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é.
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
Les responsabilités suivantes restent candidates et non matérialisées :
|
Les responsabilités suivantes restent candidates et non matérialisées :
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/002-KNOWLEDGE_ARCHITECTURE.md -->
|
<!-- file: docs/architecture/002-KNOWLEDGE_ARCHITECTURE.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# Architecture de fondation Knowledge
|
# 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
|
## 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
|
```text
|
||||||
knowledge-git-lib
|
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-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
|
```text
|
||||||
source_id -> local path ou remote Git générique
|
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.
|
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.
|
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
|
### 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 :
|
L'initialisation utilise conceptuellement :
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/002-V0_0_2_GIT_SYNC_PLAN.md -->
|
<!-- 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
|
# 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.
|
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
|
## 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 :
|
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 ;
|
- politique de nettoyage/rétention des snapshots ;
|
||||||
- besoin réel de supporter submodules ou Git LFS dans cette release ;
|
- 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 ;
|
- 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.
|
`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
|
### 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 ;
|
- créer `knowledge-git-lib` avec responsabilité réelle ;
|
||||||
- introduire uniquement les types/configurations nécessaires aux sources et à la racine d'état ;
|
- introduire uniquement les types/configurations nécessaires aux sources et à la racine d'état ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/001-VALIDATION_MATRIX.md -->
|
<!-- file: docs/validation/001-VALIDATION_MATRIX.md -->
|
||||||
<!-- version: 19 -->
|
<!-- version: 20 -->
|
||||||
|
|
||||||
# Matrice de validation Knowledge
|
# 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.
|
- `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`.
|
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
160
history/0.0.2/alpha.2.md
Normal 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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user