0.0.3-alpha.3
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 33
|
# version: 34
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
@@ -16,7 +16,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.3-alpha.2"
|
version = "0.0.3-alpha.3"
|
||||||
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: 19 -->
|
<!-- version: 20 -->
|
||||||
|
|
||||||
# SaseDev Knowledge
|
# SaseDev Knowledge
|
||||||
|
|
||||||
@@ -13,9 +13,9 @@ 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` est la stable du chemin Git générique de lecture, de l'autonomie normative locale et du cas d'usage partagé de synchronisation `Changed` / `Unchanged`.
|
`0.0.1` est la première stable de fondation de SaseDev Knowledge. `0.0.2` est la stable du chemin Git générique de lecture, de l'autonomie normative locale et du cas d'usage partagé de synchronisation `Changed` / `Unchanged`.
|
||||||
|
|
||||||
`0.0.3-alpha.2` matérialise le contrat d'écriture Git provider-neutral dans `knowledge-git-api` : chemins de mutation validés, create/update/delete, identité de commit explicite, branche `refs/heads/...` validée, état distant attendu, requêtes candidate/push et conflit explicite. Le contrat reste distinct de `GitRepository` afin de préserver les consommateurs lecture seule. Le worktree d'écriture et le push Git réels restent réservés aux tranches suivantes ; PostgreSQL, forge, workflow complet de proposition, rebase/merge automatique et auth/admin restent hors scope.
|
`0.0.3-alpha.3` matérialise maintenant la création réelle d'un commit candidat dans `knowledge-git-lib` à partir du contrat provider-neutral acquis en `alpha.2` : worktree d'écriture isolé, mutations confinées avec refus des symlinks, configuration Git globale neutralisée, hooks désactivés, identité auteur/committer explicite, vérification du parent de base, rétention interne de la révision candidate et nettoyage du worktree temporaire. Aucun push distant n'est encore effectué ; la comparaison-et-échange vers `refs/heads/...` reste réservée à `alpha.4`. PostgreSQL, forge, workflow complet de proposition, rebase/merge automatique et auth/admin restent hors scope.
|
||||||
|
|
||||||
`knowledge-git-lib` implémente `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. `alpha.4` ne change pas cette sémantique. Les règles nécessaires au développement courant sont désormais possédées localement sous `docs/rules/`, et les futurs prompts Knowledge ne chargent pas PGR sauf réévaluation explicitement engagée. `knowledge-app-lib` compose maintenant refresh, résolution, comparaison avec la révision précédente et préparation de snapshot en un résultat explicite `Changed` / `Unchanged`, sans persister la dernière révision ni matérialiser de scheduler.
|
`knowledge-git-lib` implémente `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. La création locale du candidat d'`alpha.3` reste séparée de cette sémantique de lecture. Les règles nécessaires au développement courant sont désormais possédées localement sous `docs/rules/`, et les futurs prompts Knowledge ne chargent pas PGR sauf réévaluation explicitement engagée. `knowledge-app-lib` compose maintenant refresh, résolution, comparaison avec la révision précédente et préparation de snapshot en un résultat explicite `Changed` / `Unchanged`, sans persister la dernière révision ni matérialiser de scheduler.
|
||||||
|
|
||||||
## Topologie produit de fondation
|
## Topologie produit de fondation
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/knowledge-git-lib/src/backend.rs
|
// file: crates/knowledge-git-lib/src/backend.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
//! Physical source configuration, generated-state layout and controlled Git process runner.
|
//! Physical source configuration, generated-state layout and controlled Git process runner.
|
||||||
|
|
||||||
@@ -51,6 +51,16 @@ impl crate::GitBackendConfig {
|
|||||||
pub fn snapshots_path(&self) -> std::path::PathBuf {
|
pub fn snapshots_path(&self) -> std::path::PathBuf {
|
||||||
return self.state_root.join("snapshots");
|
return self.state_root.join("snapshots");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the directory reserved for temporary isolated write worktrees.
|
||||||
|
pub fn write_worktrees_path(&self) -> std::path::PathBuf {
|
||||||
|
return self.state_root.join("write-worktrees");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the controlled empty global Git configuration path.
|
||||||
|
pub fn empty_global_config_path(&self) -> std::path::PathBuf {
|
||||||
|
return self.state_root.join("empty-global.gitconfig");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Coarse physical source kind used by the backend.
|
/// Coarse physical source kind used by the backend.
|
||||||
@@ -155,7 +165,9 @@ impl crate::GitMirror {
|
|||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
struct GitCommandSpec {
|
struct GitCommandSpec {
|
||||||
arguments: Vec<std::ffi::OsString>,
|
arguments: Vec<std::ffi::OsString>,
|
||||||
|
environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
|
||||||
executable: std::path::PathBuf,
|
executable: std::path::PathBuf,
|
||||||
|
global_config_path: std::path::PathBuf,
|
||||||
hooks_path: std::path::PathBuf,
|
hooks_path: std::path::PathBuf,
|
||||||
operation: crate::GitOperation,
|
operation: crate::GitOperation,
|
||||||
}
|
}
|
||||||
@@ -183,9 +195,20 @@ impl GitCommandRunner for SystemGitRunner {
|
|||||||
command.env_remove("GIT_INDEX_FILE");
|
command.env_remove("GIT_INDEX_FILE");
|
||||||
command.env_remove("GIT_OBJECT_DIRECTORY");
|
command.env_remove("GIT_OBJECT_DIRECTORY");
|
||||||
command.env_remove("GIT_WORK_TREE");
|
command.env_remove("GIT_WORK_TREE");
|
||||||
|
command.env_remove("GIT_AUTHOR_NAME");
|
||||||
|
command.env_remove("GIT_AUTHOR_EMAIL");
|
||||||
|
command.env_remove("GIT_COMMITTER_NAME");
|
||||||
|
command.env_remove("GIT_COMMITTER_EMAIL");
|
||||||
|
command.env_remove("GIT_CONFIG_GLOBAL");
|
||||||
|
command.env_remove("GIT_CONFIG_NOSYSTEM");
|
||||||
|
command.env("GIT_CONFIG_GLOBAL", &specification.global_config_path);
|
||||||
|
command.env("GIT_CONFIG_NOSYSTEM", "1");
|
||||||
command.env("GIT_CONFIG_COUNT", "1");
|
command.env("GIT_CONFIG_COUNT", "1");
|
||||||
command.env("GIT_CONFIG_KEY_0", "core.hooksPath");
|
command.env("GIT_CONFIG_KEY_0", "core.hooksPath");
|
||||||
command.env("GIT_CONFIG_VALUE_0", &specification.hooks_path);
|
command.env("GIT_CONFIG_VALUE_0", &specification.hooks_path);
|
||||||
|
for (name, value) in &specification.environment {
|
||||||
|
command.env(name, value);
|
||||||
|
}
|
||||||
let output_result = command.output();
|
let output_result = command.output();
|
||||||
let output = match output_result {
|
let output = match output_result {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
@@ -246,7 +269,7 @@ impl crate::GitBackend {
|
|||||||
source_id: &knowledge_domain_lib::SourceId,
|
source_id: &knowledge_domain_lib::SourceId,
|
||||||
revision: &knowledge_git_api::GitRevision,
|
revision: &knowledge_git_api::GitRevision,
|
||||||
) -> Result<std::path::PathBuf, crate::GitBackendError> {
|
) -> Result<std::path::PathBuf, crate::GitBackendError> {
|
||||||
if !is_safe_revision(revision.as_str()) {
|
if !crate::is_safe_revision(revision.as_str()) {
|
||||||
return Err(crate::GitBackendError::InvalidRevision);
|
return Err(crate::GitBackendError::InvalidRevision);
|
||||||
}
|
}
|
||||||
let source_key = crate::GitSourceKey::from_source_id(source_id);
|
let source_key = crate::GitSourceKey::from_source_id(source_id);
|
||||||
@@ -301,7 +324,7 @@ impl crate::GitBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configured_source(&self, source_id: &knowledge_domain_lib::SourceId) -> Result<&crate::GitSourceConfig, crate::GitBackendError> {
|
pub(crate) fn configured_source(&self, source_id: &knowledge_domain_lib::SourceId) -> Result<&crate::GitSourceConfig, crate::GitBackendError> {
|
||||||
for source in &self.sources {
|
for source in &self.sources {
|
||||||
if source.source_id() == source_id {
|
if source.source_id() == source_id {
|
||||||
return Ok(source);
|
return Ok(source);
|
||||||
@@ -310,16 +333,44 @@ impl crate::GitBackend {
|
|||||||
return Err(crate::GitBackendError::SourceNotConfigured);
|
return Err(crate::GitBackendError::SourceNotConfigured);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_state_directories(&self) -> Result<(), crate::GitBackendError> {
|
pub(crate) fn prepare_state_directories(&self) -> Result<(), crate::GitBackendError> {
|
||||||
for path in [self.config.hooks_path(), self.config.repositories_path(), self.config.snapshots_path()] {
|
for path in [self.config.hooks_path(), self.config.repositories_path(), self.config.snapshots_path(), self.config.write_worktrees_path()] {
|
||||||
let create_result = std::fs::create_dir_all(path);
|
let create_result = std::fs::create_dir_all(path);
|
||||||
if create_result.is_err() {
|
if create_result.is_err() {
|
||||||
return Err(crate::GitBackendError::StatePreparationFailed);
|
return Err(crate::GitBackendError::StatePreparationFailed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let config_path = self.config.empty_global_config_path();
|
||||||
|
if !config_path.exists() {
|
||||||
|
let write_result = std::fs::write(config_path, b"");
|
||||||
|
if write_result.is_err() {
|
||||||
|
return Err(crate::GitBackendError::StatePreparationFailed);
|
||||||
|
}
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run_controlled_git(
|
||||||
|
&self,
|
||||||
|
operation: crate::GitOperation,
|
||||||
|
arguments: Vec<std::ffi::OsString>,
|
||||||
|
environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
|
||||||
|
) -> Result<Vec<u8>, crate::GitBackendError> {
|
||||||
|
let specification = GitCommandSpec {
|
||||||
|
arguments,
|
||||||
|
environment,
|
||||||
|
executable: self.config.git_executable().to_path_buf(),
|
||||||
|
global_config_path: self.config.empty_global_config_path(),
|
||||||
|
hooks_path: self.config.hooks_path(),
|
||||||
|
operation,
|
||||||
|
};
|
||||||
|
let result = self.runner.run(&specification);
|
||||||
|
match result {
|
||||||
|
Ok(output) => return Ok(output.stdout),
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn clone_mirror(&self, source: &crate::GitSourceConfig, repository_path: &std::path::Path) -> Result<(), crate::GitBackendError> {
|
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")];
|
let mut arguments = vec![std::ffi::OsString::from("clone"), std::ffi::OsString::from("--mirror")];
|
||||||
if source.source_kind == crate::GitSourceKind::Local {
|
if source.source_kind == crate::GitSourceKind::Local {
|
||||||
@@ -330,7 +381,9 @@ impl crate::GitBackend {
|
|||||||
arguments.push(repository_path.as_os_str().to_os_string());
|
arguments.push(repository_path.as_os_str().to_os_string());
|
||||||
let specification = GitCommandSpec {
|
let specification = GitCommandSpec {
|
||||||
arguments,
|
arguments,
|
||||||
|
environment: Vec::new(),
|
||||||
executable: self.config.git_executable().to_path_buf(),
|
executable: self.config.git_executable().to_path_buf(),
|
||||||
|
global_config_path: self.config.empty_global_config_path(),
|
||||||
hooks_path: self.config.hooks_path(),
|
hooks_path: self.config.hooks_path(),
|
||||||
operation: crate::GitOperation::CloneMirror,
|
operation: crate::GitOperation::CloneMirror,
|
||||||
};
|
};
|
||||||
@@ -349,7 +402,9 @@ impl crate::GitBackend {
|
|||||||
std::ffi::OsString::from("rev-parse"),
|
std::ffi::OsString::from("rev-parse"),
|
||||||
std::ffi::OsString::from("--is-bare-repository"),
|
std::ffi::OsString::from("--is-bare-repository"),
|
||||||
],
|
],
|
||||||
|
environment: Vec::new(),
|
||||||
executable: self.config.git_executable().to_path_buf(),
|
executable: self.config.git_executable().to_path_buf(),
|
||||||
|
global_config_path: self.config.empty_global_config_path(),
|
||||||
hooks_path: self.config.hooks_path(),
|
hooks_path: self.config.hooks_path(),
|
||||||
operation: crate::GitOperation::InspectMirror,
|
operation: crate::GitOperation::InspectMirror,
|
||||||
};
|
};
|
||||||
@@ -390,7 +445,9 @@ impl crate::GitBackend {
|
|||||||
std::ffi::OsString::from("update"),
|
std::ffi::OsString::from("update"),
|
||||||
std::ffi::OsString::from("--prune"),
|
std::ffi::OsString::from("--prune"),
|
||||||
],
|
],
|
||||||
|
environment: Vec::new(),
|
||||||
executable: self.config.git_executable().to_path_buf(),
|
executable: self.config.git_executable().to_path_buf(),
|
||||||
|
global_config_path: self.config.empty_global_config_path(),
|
||||||
hooks_path: self.config.hooks_path(),
|
hooks_path: self.config.hooks_path(),
|
||||||
operation: crate::GitOperation::RefreshMirror,
|
operation: crate::GitOperation::RefreshMirror,
|
||||||
};
|
};
|
||||||
@@ -427,7 +484,9 @@ impl crate::GitBackend {
|
|||||||
std::ffi::OsString::from("--end-of-options"),
|
std::ffi::OsString::from("--end-of-options"),
|
||||||
std::ffi::OsString::from(revision_expression),
|
std::ffi::OsString::from(revision_expression),
|
||||||
],
|
],
|
||||||
|
environment: Vec::new(),
|
||||||
executable: self.config.git_executable().to_path_buf(),
|
executable: self.config.git_executable().to_path_buf(),
|
||||||
|
global_config_path: self.config.empty_global_config_path(),
|
||||||
hooks_path: self.config.hooks_path(),
|
hooks_path: self.config.hooks_path(),
|
||||||
operation: crate::GitOperation::ResolveRevision,
|
operation: crate::GitOperation::ResolveRevision,
|
||||||
};
|
};
|
||||||
@@ -443,7 +502,7 @@ impl crate::GitBackend {
|
|||||||
return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::ResolveRevision });
|
return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::ResolveRevision });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if !is_safe_revision(stdout) {
|
if !crate::is_safe_revision(stdout) {
|
||||||
return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::ResolveRevision });
|
return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::ResolveRevision });
|
||||||
}
|
}
|
||||||
return Ok(knowledge_git_api::GitRevision::new(stdout));
|
return Ok(knowledge_git_api::GitRevision::new(stdout));
|
||||||
@@ -459,7 +518,7 @@ impl crate::GitBackend {
|
|||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
}
|
}
|
||||||
if !is_safe_revision(revision.as_str()) {
|
if !crate::is_safe_revision(revision.as_str()) {
|
||||||
return Err(crate::GitBackendError::InvalidRevision);
|
return Err(crate::GitBackendError::InvalidRevision);
|
||||||
}
|
}
|
||||||
let mirror_result = self.inspect_mirror(source_id);
|
let mirror_result = self.inspect_mirror(source_id);
|
||||||
@@ -499,7 +558,9 @@ impl crate::GitBackend {
|
|||||||
snapshot_path.as_os_str().to_os_string(),
|
snapshot_path.as_os_str().to_os_string(),
|
||||||
std::ffi::OsString::from(revision.as_str()),
|
std::ffi::OsString::from(revision.as_str()),
|
||||||
],
|
],
|
||||||
|
environment: Vec::new(),
|
||||||
executable: self.config.git_executable().to_path_buf(),
|
executable: self.config.git_executable().to_path_buf(),
|
||||||
|
global_config_path: self.config.empty_global_config_path(),
|
||||||
hooks_path: self.config.hooks_path(),
|
hooks_path: self.config.hooks_path(),
|
||||||
operation: crate::GitOperation::CreateSnapshot,
|
operation: crate::GitOperation::CreateSnapshot,
|
||||||
};
|
};
|
||||||
@@ -527,7 +588,9 @@ impl crate::GitBackend {
|
|||||||
std::ffi::OsString::from("--verify"),
|
std::ffi::OsString::from("--verify"),
|
||||||
std::ffi::OsString::from("HEAD"),
|
std::ffi::OsString::from("HEAD"),
|
||||||
],
|
],
|
||||||
|
environment: Vec::new(),
|
||||||
executable: self.config.git_executable().to_path_buf(),
|
executable: self.config.git_executable().to_path_buf(),
|
||||||
|
global_config_path: self.config.empty_global_config_path(),
|
||||||
hooks_path: self.config.hooks_path(),
|
hooks_path: self.config.hooks_path(),
|
||||||
operation: crate::GitOperation::InspectSnapshot,
|
operation: crate::GitOperation::InspectSnapshot,
|
||||||
};
|
};
|
||||||
@@ -552,7 +615,9 @@ impl crate::GitBackend {
|
|||||||
std::ffi::OsString::from("--porcelain=v1"),
|
std::ffi::OsString::from("--porcelain=v1"),
|
||||||
std::ffi::OsString::from("--untracked-files=all"),
|
std::ffi::OsString::from("--untracked-files=all"),
|
||||||
],
|
],
|
||||||
|
environment: Vec::new(),
|
||||||
executable: self.config.git_executable().to_path_buf(),
|
executable: self.config.git_executable().to_path_buf(),
|
||||||
|
global_config_path: self.config.empty_global_config_path(),
|
||||||
hooks_path: self.config.hooks_path(),
|
hooks_path: self.config.hooks_path(),
|
||||||
operation: crate::GitOperation::InspectSnapshot,
|
operation: crate::GitOperation::InspectSnapshot,
|
||||||
};
|
};
|
||||||
@@ -592,7 +657,7 @@ impl knowledge_git_api::GitRepository for crate::GitBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_safe_revision(value: &str) -> bool {
|
pub(crate) fn is_safe_revision(value: &str) -> bool {
|
||||||
if value.is_empty() {
|
if value.is_empty() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/knowledge-git-lib/src/error.rs
|
// file: crates/knowledge-git-lib/src/error.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
//! Recoverable errors for controlled Git process execution.
|
//! Recoverable errors for controlled Git process execution.
|
||||||
|
|
||||||
@@ -18,6 +18,18 @@ pub enum GitOperation {
|
|||||||
CreateSnapshot,
|
CreateSnapshot,
|
||||||
/// Validate an existing detached snapshot before reuse.
|
/// Validate an existing detached snapshot before reuse.
|
||||||
InspectSnapshot,
|
InspectSnapshot,
|
||||||
|
/// Create one isolated detached worktree for candidate construction.
|
||||||
|
CreateWriteWorktree,
|
||||||
|
/// Stage the requested candidate mutations.
|
||||||
|
StageCandidate,
|
||||||
|
/// Inspect the candidate index, commit or parent relationship.
|
||||||
|
InspectCandidate,
|
||||||
|
/// Create the candidate commit with explicit identity.
|
||||||
|
CommitCandidate,
|
||||||
|
/// Retain the detached candidate commit under an internal backend reference.
|
||||||
|
PreserveCandidate,
|
||||||
|
/// Remove one backend-owned candidate worktree.
|
||||||
|
RemoveWriteWorktree,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for crate::GitOperation {
|
impl std::fmt::Display for crate::GitOperation {
|
||||||
@@ -29,6 +41,12 @@ impl std::fmt::Display for crate::GitOperation {
|
|||||||
crate::GitOperation::ResolveRevision => return formatter.write_str("resolve revision"),
|
crate::GitOperation::ResolveRevision => return formatter.write_str("resolve revision"),
|
||||||
crate::GitOperation::CreateSnapshot => return formatter.write_str("create snapshot"),
|
crate::GitOperation::CreateSnapshot => return formatter.write_str("create snapshot"),
|
||||||
crate::GitOperation::InspectSnapshot => return formatter.write_str("inspect snapshot"),
|
crate::GitOperation::InspectSnapshot => return formatter.write_str("inspect snapshot"),
|
||||||
|
crate::GitOperation::CreateWriteWorktree => return formatter.write_str("create write worktree"),
|
||||||
|
crate::GitOperation::StageCandidate => return formatter.write_str("stage candidate"),
|
||||||
|
crate::GitOperation::InspectCandidate => return formatter.write_str("inspect candidate"),
|
||||||
|
crate::GitOperation::CommitCandidate => return formatter.write_str("commit candidate"),
|
||||||
|
crate::GitOperation::PreserveCandidate => return formatter.write_str("preserve candidate"),
|
||||||
|
crate::GitOperation::RemoveWriteWorktree => return formatter.write_str("remove write worktree"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +89,20 @@ pub enum GitBackendError {
|
|||||||
InvalidRevision,
|
InvalidRevision,
|
||||||
/// An existing snapshot does not match the requested revision or is not clean.
|
/// An existing snapshot does not match the requested revision or is not clean.
|
||||||
InvalidSnapshot,
|
InvalidSnapshot,
|
||||||
|
/// The deterministic backend-owned write worktree path already exists unexpectedly.
|
||||||
|
WriteWorktreeExists,
|
||||||
|
/// A mutation would traverse or replace a symbolic link.
|
||||||
|
UnsafeMutationTarget,
|
||||||
|
/// A requested deletion target does not exist.
|
||||||
|
MutationTargetMissing,
|
||||||
|
/// A filesystem mutation or parent-directory preparation failed.
|
||||||
|
MutationFilesystemFailed,
|
||||||
|
/// The staged candidate paths do not exactly match the requested mutation set.
|
||||||
|
CandidateMutationMismatch,
|
||||||
|
/// The resulting candidate commit does not have the requested base as its sole first parent.
|
||||||
|
InvalidCandidate,
|
||||||
|
/// A backend-owned write worktree could not be removed deterministically.
|
||||||
|
WriteCleanupFailed,
|
||||||
/// Git returned output that does not satisfy the backend contract.
|
/// Git returned output that does not satisfy the backend contract.
|
||||||
UnexpectedGitOutput {
|
UnexpectedGitOutput {
|
||||||
/// Operation whose output could not be validated.
|
/// Operation whose output could not be validated.
|
||||||
@@ -95,6 +127,13 @@ impl std::fmt::Display for crate::GitBackendError {
|
|||||||
crate::GitBackendError::MirrorMissing => return formatter.write_str("controlled git mirror does not exist"),
|
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::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::InvalidSnapshot => return formatter.write_str("controlled git snapshot is invalid or dirty"),
|
||||||
|
crate::GitBackendError::WriteWorktreeExists => return formatter.write_str("controlled git write worktree already exists"),
|
||||||
|
crate::GitBackendError::UnsafeMutationTarget => return formatter.write_str("git mutation target traverses a symbolic link"),
|
||||||
|
crate::GitBackendError::MutationTargetMissing => return formatter.write_str("git deletion target does not exist"),
|
||||||
|
crate::GitBackendError::MutationFilesystemFailed => return formatter.write_str("git candidate filesystem mutation failed"),
|
||||||
|
crate::GitBackendError::CandidateMutationMismatch => return formatter.write_str("git candidate staged paths do not match requested mutations"),
|
||||||
|
crate::GitBackendError::InvalidCandidate => return formatter.write_str("git candidate commit does not match the requested base"),
|
||||||
|
crate::GitBackendError::WriteCleanupFailed => return formatter.write_str("controlled git write worktree cleanup failed"),
|
||||||
crate::GitBackendError::UnexpectedGitOutput { operation } => {
|
crate::GitBackendError::UnexpectedGitOutput { operation } => {
|
||||||
return write!(formatter, "git returned unexpected output while attempting to {operation}");
|
return write!(formatter, "git returned unexpected output while attempting to {operation}");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
// file: crates/knowledge-git-lib/src/lib.rs
|
// file: crates/knowledge-git-lib/src/lib.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
//! Controlled Git process backend for Knowledge content sources.
|
//! Controlled Git process backend for Knowledge content sources.
|
||||||
//!
|
//!
|
||||||
//! This crate owns physical Git source configuration, generated-state layout, provider-neutral read
|
//! This crate owns physical Git source configuration, generated-state layout, provider-neutral read
|
||||||
//! materialization and process execution. It deliberately contains no forge API, persistence,
|
//! materialization, isolated candidate construction and process execution. It deliberately contains no
|
||||||
//! scheduler or application orchestration.
|
//! forge API, persistence, scheduler or application orchestration.
|
||||||
|
|
||||||
mod backend;
|
mod backend;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod write;
|
||||||
|
|
||||||
/// Controlled Git backend that owns configured sources and local read materialization.
|
/// Controlled Git backend that owns configured sources and local read materialization.
|
||||||
pub use self::backend::GitBackend;
|
pub use self::backend::GitBackend;
|
||||||
@@ -28,3 +29,5 @@ pub use self::backend::GitSourceKind;
|
|||||||
pub use self::error::GitBackendError;
|
pub use self::error::GitBackendError;
|
||||||
/// Git operation category exposed by recoverable process errors without leaking command arguments.
|
/// Git operation category exposed by recoverable process errors without leaking command arguments.
|
||||||
pub use self::error::GitOperation;
|
pub use self::error::GitOperation;
|
||||||
|
|
||||||
|
pub(crate) use self::backend::is_safe_revision;
|
||||||
|
|||||||
322
crates/knowledge-git-lib/src/write.rs
Normal file
322
crates/knowledge-git-lib/src/write.rs
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
// file: crates/knowledge-git-lib/src/write.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Isolated candidate-commit creation for controlled Git sources.
|
||||||
|
|
||||||
|
impl crate::GitBackend {
|
||||||
|
/// Create one detached candidate commit from an explicit resolved base revision.
|
||||||
|
///
|
||||||
|
/// This operation does not update any remote branch. The candidate is retained by an internal
|
||||||
|
/// backend reference so a later guarded push can publish it without depending on worktree
|
||||||
|
/// lifetime or reflog retention.
|
||||||
|
pub fn create_candidate(
|
||||||
|
&mut self,
|
||||||
|
source_id: &knowledge_domain_lib::SourceId,
|
||||||
|
request: &knowledge_git_api::GitWriteRequest,
|
||||||
|
) -> Result<knowledge_git_api::GitRevision, crate::GitBackendError> {
|
||||||
|
if !crate::is_safe_revision(request.base_revision().as_str()) {
|
||||||
|
return Err(crate::GitBackendError::InvalidRevision);
|
||||||
|
}
|
||||||
|
let source_result = self.configured_source(source_id);
|
||||||
|
let source = match source_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let preparation_result = self.prepare_state_directories();
|
||||||
|
match preparation_result {
|
||||||
|
Ok(()) => (),
|
||||||
|
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 source_key = crate::GitSourceKey::from_source_id(source_id);
|
||||||
|
let source_write_root = self.config().write_worktrees_path().join(source_key.as_str());
|
||||||
|
let worktree_path = source_write_root.join("candidate");
|
||||||
|
if worktree_path.exists() {
|
||||||
|
return Err(crate::GitBackendError::WriteWorktreeExists);
|
||||||
|
}
|
||||||
|
let create_parent_result = std::fs::create_dir_all(&source_write_root);
|
||||||
|
if create_parent_result.is_err() {
|
||||||
|
return Err(crate::GitBackendError::MutationFilesystemFailed);
|
||||||
|
}
|
||||||
|
let add_result = self.run_controlled_git(
|
||||||
|
crate::GitOperation::CreateWriteWorktree,
|
||||||
|
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("--"),
|
||||||
|
worktree_path.as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from(request.base_revision().as_str()),
|
||||||
|
],
|
||||||
|
Vec::new(),
|
||||||
|
);
|
||||||
|
match add_result {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
let candidate_result = self.populate_candidate_worktree(mirror.repository_path(), &worktree_path, request);
|
||||||
|
let cleanup_result = self.remove_candidate_worktree(mirror.repository_path(), &worktree_path, candidate_result.is_err());
|
||||||
|
match cleanup_result {
|
||||||
|
Ok(()) => return candidate_result,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate_candidate_worktree(
|
||||||
|
&self,
|
||||||
|
repository_path: &std::path::Path,
|
||||||
|
worktree_path: &std::path::Path,
|
||||||
|
request: &knowledge_git_api::GitWriteRequest,
|
||||||
|
) -> Result<knowledge_git_api::GitRevision, crate::GitBackendError> {
|
||||||
|
for mutation in request.mutations() {
|
||||||
|
let mutation_result = apply_mutation(worktree_path, mutation);
|
||||||
|
match mutation_result {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut add_arguments = vec![
|
||||||
|
std::ffi::OsString::from("-C"),
|
||||||
|
worktree_path.as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from("add"),
|
||||||
|
std::ffi::OsString::from("--all"),
|
||||||
|
std::ffi::OsString::from("--"),
|
||||||
|
];
|
||||||
|
for mutation in request.mutations() {
|
||||||
|
add_arguments.push(std::ffi::OsString::from(mutation.path().as_str()));
|
||||||
|
}
|
||||||
|
let add_result = self.run_controlled_git(crate::GitOperation::StageCandidate, add_arguments, Vec::new());
|
||||||
|
match add_result {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
let staged_result = self.run_controlled_git(
|
||||||
|
crate::GitOperation::InspectCandidate,
|
||||||
|
vec![
|
||||||
|
std::ffi::OsString::from("-C"),
|
||||||
|
worktree_path.as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from("diff"),
|
||||||
|
std::ffi::OsString::from("--cached"),
|
||||||
|
std::ffi::OsString::from("--name-only"),
|
||||||
|
std::ffi::OsString::from("-z"),
|
||||||
|
std::ffi::OsString::from("--"),
|
||||||
|
],
|
||||||
|
Vec::new(),
|
||||||
|
);
|
||||||
|
let staged = match staged_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
if !staged_paths_match(&staged, request.mutations()) {
|
||||||
|
return Err(crate::GitBackendError::CandidateMutationMismatch);
|
||||||
|
}
|
||||||
|
let identity = request.identity();
|
||||||
|
let environment = vec![
|
||||||
|
(std::ffi::OsString::from("GIT_AUTHOR_NAME"), std::ffi::OsString::from(identity.name())),
|
||||||
|
(std::ffi::OsString::from("GIT_AUTHOR_EMAIL"), std::ffi::OsString::from(identity.email())),
|
||||||
|
(std::ffi::OsString::from("GIT_COMMITTER_NAME"), std::ffi::OsString::from(identity.name())),
|
||||||
|
(std::ffi::OsString::from("GIT_COMMITTER_EMAIL"), std::ffi::OsString::from(identity.email())),
|
||||||
|
];
|
||||||
|
let commit_result = self.run_controlled_git(
|
||||||
|
crate::GitOperation::CommitCandidate,
|
||||||
|
vec![
|
||||||
|
std::ffi::OsString::from("-C"),
|
||||||
|
worktree_path.as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from("commit"),
|
||||||
|
std::ffi::OsString::from("--no-verify"),
|
||||||
|
std::ffi::OsString::from("--no-gpg-sign"),
|
||||||
|
std::ffi::OsString::from("--cleanup=verbatim"),
|
||||||
|
std::ffi::OsString::from("-m"),
|
||||||
|
std::ffi::OsString::from(request.message()),
|
||||||
|
],
|
||||||
|
environment,
|
||||||
|
);
|
||||||
|
match commit_result {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
let candidate_result = self.resolve_worktree_revision(worktree_path, "HEAD", crate::GitOperation::InspectCandidate);
|
||||||
|
let candidate = match candidate_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let parent_result = self.resolve_worktree_revision(worktree_path, "HEAD^1", crate::GitOperation::InspectCandidate);
|
||||||
|
let parent = match parent_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
if &parent != request.base_revision() || candidate == parent {
|
||||||
|
return Err(crate::GitBackendError::InvalidCandidate);
|
||||||
|
}
|
||||||
|
let candidate_reference = format!("refs/knowledge/candidates/{}", candidate.as_str());
|
||||||
|
let preserve_result = self.run_controlled_git(
|
||||||
|
crate::GitOperation::PreserveCandidate,
|
||||||
|
vec![
|
||||||
|
std::ffi::OsString::from("--git-dir"),
|
||||||
|
repository_path.as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from("update-ref"),
|
||||||
|
std::ffi::OsString::from(candidate_reference),
|
||||||
|
std::ffi::OsString::from(candidate.as_str()),
|
||||||
|
],
|
||||||
|
Vec::new(),
|
||||||
|
);
|
||||||
|
match preserve_result {
|
||||||
|
Ok(_) => return Ok(candidate),
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_worktree_revision(
|
||||||
|
&self,
|
||||||
|
worktree_path: &std::path::Path,
|
||||||
|
expression: &str,
|
||||||
|
operation: crate::GitOperation,
|
||||||
|
) -> Result<knowledge_git_api::GitRevision, crate::GitBackendError> {
|
||||||
|
let output_result = self.run_controlled_git(
|
||||||
|
operation,
|
||||||
|
vec![
|
||||||
|
std::ffi::OsString::from("-C"),
|
||||||
|
worktree_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(expression),
|
||||||
|
],
|
||||||
|
Vec::new(),
|
||||||
|
);
|
||||||
|
let output = match output_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let text_result = std::str::from_utf8(&output);
|
||||||
|
let text = match text_result {
|
||||||
|
Ok(value) => value.trim(),
|
||||||
|
Err(_) => return Err(crate::GitBackendError::UnexpectedGitOutput { operation }),
|
||||||
|
};
|
||||||
|
if !crate::is_safe_revision(text) {
|
||||||
|
return Err(crate::GitBackendError::UnexpectedGitOutput { operation });
|
||||||
|
}
|
||||||
|
return Ok(knowledge_git_api::GitRevision::new(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_candidate_worktree(&self, repository_path: &std::path::Path, worktree_path: &std::path::Path, force: bool) -> Result<(), crate::GitBackendError> {
|
||||||
|
let mut arguments = vec![
|
||||||
|
std::ffi::OsString::from("--git-dir"),
|
||||||
|
repository_path.as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from("worktree"),
|
||||||
|
std::ffi::OsString::from("remove"),
|
||||||
|
];
|
||||||
|
if force {
|
||||||
|
arguments.push(std::ffi::OsString::from("--force"));
|
||||||
|
}
|
||||||
|
arguments.push(std::ffi::OsString::from("--"));
|
||||||
|
arguments.push(worktree_path.as_os_str().to_os_string());
|
||||||
|
let remove_result = self.run_controlled_git(crate::GitOperation::RemoveWriteWorktree, arguments, Vec::new());
|
||||||
|
match remove_result {
|
||||||
|
Ok(_) => return Ok(()),
|
||||||
|
Err(_) => return Err(crate::GitBackendError::WriteCleanupFailed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_mutation(worktree_path: &std::path::Path, mutation: &knowledge_git_api::GitFileMutation) -> Result<(), crate::GitBackendError> {
|
||||||
|
let target_result = checked_target_path(worktree_path, mutation.path());
|
||||||
|
let target = match target_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
match mutation {
|
||||||
|
knowledge_git_api::GitFileMutation::Write { content, .. } => {
|
||||||
|
let parent = match target.parent() {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return Err(crate::GitBackendError::MutationFilesystemFailed),
|
||||||
|
};
|
||||||
|
let create_result = std::fs::create_dir_all(parent);
|
||||||
|
if create_result.is_err() {
|
||||||
|
return Err(crate::GitBackendError::MutationFilesystemFailed);
|
||||||
|
}
|
||||||
|
let write_result = std::fs::write(target, content);
|
||||||
|
if write_result.is_err() {
|
||||||
|
return Err(crate::GitBackendError::MutationFilesystemFailed);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
knowledge_git_api::GitFileMutation::Delete { .. } => {
|
||||||
|
if !target.exists() {
|
||||||
|
return Err(crate::GitBackendError::MutationTargetMissing);
|
||||||
|
}
|
||||||
|
let remove_result = std::fs::remove_file(target);
|
||||||
|
if remove_result.is_err() {
|
||||||
|
return Err(crate::GitBackendError::MutationFilesystemFailed);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checked_target_path(
|
||||||
|
worktree_path: &std::path::Path,
|
||||||
|
mutation_path: &knowledge_git_api::GitMutationPath,
|
||||||
|
) -> Result<std::path::PathBuf, crate::GitBackendError> {
|
||||||
|
let components: Vec<&str> = mutation_path.as_str().split('/').collect();
|
||||||
|
let mut current = worktree_path.to_path_buf();
|
||||||
|
for (index, component) in components.iter().enumerate() {
|
||||||
|
current.push(*component);
|
||||||
|
let metadata_result = std::fs::symlink_metadata(¤t);
|
||||||
|
let metadata = match metadata_result {
|
||||||
|
Ok(value) => Some(value),
|
||||||
|
Err(error) => {
|
||||||
|
if error.kind() == std::io::ErrorKind::NotFound {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
return Err(crate::GitBackendError::MutationFilesystemFailed);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let metadata = match metadata {
|
||||||
|
Some(value) => value,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
if metadata.file_type().is_symlink() {
|
||||||
|
return Err(crate::GitBackendError::UnsafeMutationTarget);
|
||||||
|
}
|
||||||
|
let is_final = index + 1 == components.len();
|
||||||
|
if !is_final && !metadata.is_dir() {
|
||||||
|
return Err(crate::GitBackendError::MutationFilesystemFailed);
|
||||||
|
}
|
||||||
|
if is_final && metadata.is_dir() {
|
||||||
|
return Err(crate::GitBackendError::MutationFilesystemFailed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn staged_paths_match(staged: &[u8], mutations: &[knowledge_git_api::GitFileMutation]) -> bool {
|
||||||
|
let mut observed = std::collections::HashSet::new();
|
||||||
|
for bytes in staged.split(|byte| return *byte == 0) {
|
||||||
|
if bytes.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let path_result = std::str::from_utf8(bytes);
|
||||||
|
let path = match path_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
observed.insert(path);
|
||||||
|
}
|
||||||
|
if observed.len() != mutations.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for mutation in mutations {
|
||||||
|
if !observed.contains(mutation.path().as_str()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
312
crates/knowledge-git-lib/tests/git_write_candidate.rs
Normal file
312
crates/knowledge-git-lib/tests/git_write_candidate.rs
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
// file: crates/knowledge-git-lib/tests/git_write_candidate.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Integration tests for isolated real Git candidate creation using local temporary repositories.
|
||||||
|
|
||||||
|
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-write-{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 run_git_output(arguments: Vec<std::ffi::OsString>) -> Option<Vec<u8>> {
|
||||||
|
let mut command = std::process::Command::new("git");
|
||||||
|
command.args(arguments);
|
||||||
|
command.env("GIT_TERMINAL_PROMPT", "0");
|
||||||
|
let output_result = command.output();
|
||||||
|
let output = match output_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(output.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
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", "Ambient Identity"]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !run_git(&source, &["config", "user.email", "ambient@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 std::fs::write(source.join("delete.txt"), "delete me\n").is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !run_git(&source, &["add", "content.txt", "delete.txt"]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !run_git(&source, &["commit", "-m", "first"]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configured_backend(root: &std::path::Path, source: &std::path::Path) -> Option<(knowledge_git_lib::GitBackend, knowledge_domain_lib::SourceId)> {
|
||||||
|
let source_id_result = knowledge_domain_lib::SourceId::new("integration-write-source");
|
||||||
|
let source_id = match source_id_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolved_main(backend: &mut knowledge_git_lib::GitBackend, source_id: &knowledge_domain_lib::SourceId) -> Option<knowledge_git_api::GitRevision> {
|
||||||
|
if knowledge_git_api::GitRepository::refresh(backend, source_id).is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let reference = knowledge_git_api::GitReference::new("refs/heads/main");
|
||||||
|
let revision_result = knowledge_git_api::GitRepository::resolve_revision(backend, source_id, &reference);
|
||||||
|
match revision_result {
|
||||||
|
Ok(value) => return Some(value),
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mutation_path(value: &str) -> Option<knowledge_git_api::GitMutationPath> {
|
||||||
|
let result = knowledge_git_api::GitMutationPath::new(value);
|
||||||
|
match result {
|
||||||
|
Ok(value) => return Some(value),
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit_identity() -> Option<knowledge_git_api::GitCommitIdentity> {
|
||||||
|
let result = knowledge_git_api::GitCommitIdentity::new("Knowledge Writer", "writer@example.invalid");
|
||||||
|
match result {
|
||||||
|
Ok(value) => return Some(value),
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn candidate_commit_applies_exact_mutations_uses_explicit_identity_and_cleans_worktree() {
|
||||||
|
let root = test_root("candidate");
|
||||||
|
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 base_result = resolved_main(&mut backend, &source_id);
|
||||||
|
assert!(base_result.is_some());
|
||||||
|
let base = match base_result {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let snapshot_result = knowledge_git_api::GitRepository::prepare_snapshot(&backend, &source_id, &base);
|
||||||
|
assert!(snapshot_result.is_ok());
|
||||||
|
let snapshot = match snapshot_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
let content_path = match mutation_path("content.txt") {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let nested_path = match mutation_path("nested/new.txt") {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let delete_path = match mutation_path("delete.txt") {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let identity = match commit_identity() {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let request_result = knowledge_git_api::GitWriteRequest::new(
|
||||||
|
base.clone(),
|
||||||
|
vec![
|
||||||
|
knowledge_git_api::GitFileMutation::Write { path: content_path, content: b"second\n".to_vec() },
|
||||||
|
knowledge_git_api::GitFileMutation::Write { path: nested_path, content: b"nested\n".to_vec() },
|
||||||
|
knowledge_git_api::GitFileMutation::Delete { path: delete_path },
|
||||||
|
],
|
||||||
|
identity,
|
||||||
|
"candidate commit",
|
||||||
|
);
|
||||||
|
assert!(request_result.is_ok());
|
||||||
|
let request = match request_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
let candidate_result = backend.create_candidate(&source_id, &request);
|
||||||
|
assert!(candidate_result.is_ok());
|
||||||
|
let candidate = match candidate_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_ne!(candidate, base);
|
||||||
|
let repository_path = backend.repository_path(&source_id);
|
||||||
|
let parent_output = run_git_output(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(format!("{}^1", candidate.as_str())),
|
||||||
|
]);
|
||||||
|
assert!(parent_output.is_some());
|
||||||
|
let parent = match parent_output {
|
||||||
|
Some(value) => String::from_utf8_lossy(&value).trim().to_owned(),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
assert_eq!(parent, base.as_str());
|
||||||
|
let content_output = run_git_output(vec![
|
||||||
|
std::ffi::OsString::from("--git-dir"),
|
||||||
|
repository_path.as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from("show"),
|
||||||
|
std::ffi::OsString::from(format!("{}:content.txt", candidate.as_str())),
|
||||||
|
]);
|
||||||
|
assert_eq!(content_output, Some(b"second\n".to_vec()));
|
||||||
|
let nested_output = run_git_output(vec![
|
||||||
|
std::ffi::OsString::from("--git-dir"),
|
||||||
|
repository_path.as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from("show"),
|
||||||
|
std::ffi::OsString::from(format!("{}:nested/new.txt", candidate.as_str())),
|
||||||
|
]);
|
||||||
|
assert_eq!(nested_output, Some(b"nested\n".to_vec()));
|
||||||
|
let deleted_output = run_git_output(vec![
|
||||||
|
std::ffi::OsString::from("--git-dir"),
|
||||||
|
repository_path.as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from("show"),
|
||||||
|
std::ffi::OsString::from(format!("{}:delete.txt", candidate.as_str())),
|
||||||
|
]);
|
||||||
|
assert!(deleted_output.is_none());
|
||||||
|
let identity_output = run_git_output(vec![
|
||||||
|
std::ffi::OsString::from("--git-dir"),
|
||||||
|
repository_path.as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from("show"),
|
||||||
|
std::ffi::OsString::from("-s"),
|
||||||
|
std::ffi::OsString::from("--format=%an%x00%ae%x00%cn%x00%ce"),
|
||||||
|
std::ffi::OsString::from(candidate.as_str()),
|
||||||
|
]);
|
||||||
|
assert!(identity_output.is_some());
|
||||||
|
let identity_text = match identity_output {
|
||||||
|
Some(value) => String::from_utf8_lossy(&value).trim().to_owned(),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
assert_eq!(identity_text, "Knowledge Writer\0writer@example.invalid\0Knowledge Writer\0writer@example.invalid");
|
||||||
|
let candidate_ref = format!("refs/knowledge/candidates/{}", candidate.as_str());
|
||||||
|
let retained = run_git_output(vec![
|
||||||
|
std::ffi::OsString::from("--git-dir"),
|
||||||
|
repository_path.as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from("show-ref"),
|
||||||
|
std::ffi::OsString::from("--verify"),
|
||||||
|
std::ffi::OsString::from(candidate_ref),
|
||||||
|
]);
|
||||||
|
assert!(retained.is_some());
|
||||||
|
let source_key = knowledge_git_lib::GitSourceKey::from_source_id(&source_id);
|
||||||
|
assert!(!backend.config().write_worktrees_path().join(source_key.as_str()).join("candidate").exists());
|
||||||
|
let snapshot_content = std::fs::read_to_string(snapshot.path().join("content.txt"));
|
||||||
|
assert_eq!(snapshot_content, Ok(String::from("first\n")));
|
||||||
|
assert!(snapshot.path().join("delete.txt").is_file());
|
||||||
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn candidate_rejects_symlink_escape_and_removes_owned_failed_worktree() {
|
||||||
|
let root = test_root("symlink");
|
||||||
|
let source_result = prepare_source(&root);
|
||||||
|
assert!(source_result.is_some());
|
||||||
|
let source = match source_result {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let outside = root.join("outside");
|
||||||
|
assert!(std::fs::create_dir_all(&outside).is_ok());
|
||||||
|
assert!(std::os::unix::fs::symlink(&outside, source.join("escape")).is_ok());
|
||||||
|
assert!(run_git(&source, &["add", "escape"]));
|
||||||
|
assert!(run_git(&source, &["commit", "-m", "add escape symlink"]));
|
||||||
|
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 base_result = resolved_main(&mut backend, &source_id);
|
||||||
|
assert!(base_result.is_some());
|
||||||
|
let base = match base_result {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let path = match mutation_path("escape/pwned.txt") {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let identity = match commit_identity() {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let request_result = knowledge_git_api::GitWriteRequest::new(
|
||||||
|
base,
|
||||||
|
vec![knowledge_git_api::GitFileMutation::Write { path, content: b"forbidden\n".to_vec() }],
|
||||||
|
identity,
|
||||||
|
"must fail",
|
||||||
|
);
|
||||||
|
assert!(request_result.is_ok());
|
||||||
|
let request = match request_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
let result = backend.create_candidate(&source_id, &request);
|
||||||
|
assert_eq!(result, Err(knowledge_git_lib::GitBackendError::UnsafeMutationTarget));
|
||||||
|
assert!(!outside.join("pwned.txt").exists());
|
||||||
|
let source_key = knowledge_git_lib::GitSourceKey::from_source_id(&source_id);
|
||||||
|
assert!(!backend.config().write_worktrees_path().join(source_key.as_str()).join("candidate").exists());
|
||||||
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/knowledge-git-lib/unit_tests/backend.rs
|
// file: crates/knowledge-git-lib/unit_tests/backend.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct RecordingRunner {
|
struct RecordingRunner {
|
||||||
@@ -159,6 +159,9 @@ fn local_source_clone_uses_mirror_no_local_and_argument_separator() {
|
|||||||
assert!(backend.config().hooks_path().is_dir());
|
assert!(backend.config().hooks_path().is_dir());
|
||||||
assert!(backend.config().repositories_path().is_dir());
|
assert!(backend.config().repositories_path().is_dir());
|
||||||
assert!(backend.config().snapshots_path().is_dir());
|
assert!(backend.config().snapshots_path().is_dir());
|
||||||
|
assert!(backend.config().write_worktrees_path().is_dir());
|
||||||
|
assert!(backend.config().empty_global_config_path().is_file());
|
||||||
|
assert_eq!(calls[0].global_config_path, backend.config().empty_global_config_path());
|
||||||
let _ = std::fs::remove_dir_all(root);
|
let _ = std::fs::remove_dir_all(root);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
256
deltas/0.0.3/alpha.3.md
Normal file
256
deltas/0.0.3/alpha.3.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
<!-- file: deltas/0.0.3/alpha.3.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.0.3-alpha.3 — worktree d'écriture et commit candidat
|
||||||
|
|
||||||
|
## Baseline requise
|
||||||
|
|
||||||
|
Partir de `0.0.3-alpha.2` après application, commit et push de son history `PASS`.
|
||||||
|
|
||||||
|
La version devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.0.3-alpha.3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Objet
|
||||||
|
|
||||||
|
Implémenter dans `knowledge-git-lib` la création locale réelle d'une révision candidate à partir d'une base Git explicite, sans mettre encore à jour de branche distante.
|
||||||
|
|
||||||
|
La tranche :
|
||||||
|
|
||||||
|
- ajoute un worktree d'écriture temporaire et déterministe sous la racine d'état du backend ;
|
||||||
|
- réutilise le mirror contrôlé du `SourceId` et exige une `GitRevision` de base sûre ;
|
||||||
|
- applique exactement les mutations `Write` / `Delete` demandées ;
|
||||||
|
- refuse toute cible ou tout parent filesystem qui est un symlink ;
|
||||||
|
- stage uniquement les chemins demandés puis vérifie que l'index contient exactement cet ensemble ;
|
||||||
|
- neutralise la configuration Git système/globale ambiante pertinente et conserve `core.hooksPath` sous contrôle ;
|
||||||
|
- crée le commit avec auteur et committer explicitement issus de `GitCommitIdentity`, `--no-verify` et `--no-gpg-sign` ;
|
||||||
|
- vérifie que le candidat a exactement la base demandée comme premier parent ;
|
||||||
|
- retient l'objet candidat sous `refs/knowledge/candidates/<revision>` avant suppression du worktree ;
|
||||||
|
- retire le worktree possédé après succès et tente une suppression forcée après une erreur survenue dans ce worktree ;
|
||||||
|
- refuse un worktree `candidate` préexistant au lieu de le supprimer silencieusement ;
|
||||||
|
- ajoute des tests d'intégration Git réels pour le candidat et l'échappement par symlink.
|
||||||
|
|
||||||
|
`alpha.3` ne met à jour aucun remote et n'implémente pas encore `GitRepositoryWriter` sur `GitBackend`, car ce trait exige également `push_candidate`. L'implémentation complète du trait et le compare-and-exchange distant restent réservés à `alpha.4`.
|
||||||
|
|
||||||
|
## Scope des fichiers
|
||||||
|
|
||||||
|
REPLACE :
|
||||||
|
|
||||||
|
```text
|
||||||
|
VERSION
|
||||||
|
Cargo.toml
|
||||||
|
README.md
|
||||||
|
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
|
||||||
|
docs/architecture/002-KNOWLEDGE_ARCHITECTURE.md
|
||||||
|
docs/plans/003-V0_0_3_GIT_WRITE_PLAN.md
|
||||||
|
docs/validation/001-VALIDATION_MATRIX.md
|
||||||
|
```
|
||||||
|
|
||||||
|
ADD :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/knowledge-git-lib/src/write.rs
|
||||||
|
crates/knowledge-git-lib/tests/git_write_candidate.rs
|
||||||
|
deltas/0.0.3/alpha.3.md
|
||||||
|
```
|
||||||
|
|
||||||
|
DELETE : aucun.
|
||||||
|
|
||||||
|
`knowledge-git-api`, `knowledge-app-lib`, `Web/`, `CHANGELOG.md` et `ROADMAP.md` ne sont pas modifiés.
|
||||||
|
|
||||||
|
Aucun history `alpha.3` n'est inclus dans ce delta.
|
||||||
|
|
||||||
|
## Contrat backend matérialisé
|
||||||
|
|
||||||
|
Le chemin local devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
GitRevision de base
|
||||||
|
-> mirror contrôlé
|
||||||
|
-> write-worktrees/<source-key>/candidate
|
||||||
|
-> mutations confinées
|
||||||
|
-> git add limité aux chemins demandés
|
||||||
|
-> commit avec identité explicite
|
||||||
|
-> vérification HEAD / HEAD^1
|
||||||
|
-> refs/knowledge/candidates/<revision>
|
||||||
|
-> suppression du worktree temporaire
|
||||||
|
-> GitRevision candidate
|
||||||
|
```
|
||||||
|
|
||||||
|
La racine d'état contrôlée contient désormais également :
|
||||||
|
|
||||||
|
```text
|
||||||
|
empty-global.gitconfig
|
||||||
|
write-worktrees/
|
||||||
|
```
|
||||||
|
|
||||||
|
`empty-global.gitconfig` neutralise la configuration utilisateur globale pour les commandes possédées par le backend ; `GIT_CONFIG_NOSYSTEM=1` neutralise la configuration système. La configuration repository nécessaire au mirror reste disponible, tandis que `core.hooksPath` est forcé vers `hooks-disabled`.
|
||||||
|
|
||||||
|
## Confinement filesystem
|
||||||
|
|
||||||
|
`GitMutationPath` fournit déjà le confinement lexical. `alpha.3` ajoute le contrôle physique : avant toute écriture ou suppression, chaque composant existant du chemin sous le worktree est inspecté avec `symlink_metadata`.
|
||||||
|
|
||||||
|
Un symlink rencontré produit `UnsafeMutationTarget`. Une suppression d'une cible absente produit `MutationTargetMissing`. Les erreurs filesystem restent récupérables et ne contiennent pas le chemin ou le contenu sensible dans leur texte public.
|
||||||
|
|
||||||
|
Un worktree préexistant à l'emplacement contrôlé produit `WriteWorktreeExists`. Il n'est pas supprimé automatiquement.
|
||||||
|
|
||||||
|
## Candidate et nettoyage
|
||||||
|
|
||||||
|
Une candidate n'est valide que si :
|
||||||
|
|
||||||
|
- les chemins réellement stagés correspondent exactement aux mutations demandées ;
|
||||||
|
- le commit est créé avec l'identité explicite de la requête ;
|
||||||
|
- `HEAD` est une révision Git sûre ;
|
||||||
|
- `HEAD^1` est exactement la base demandée ;
|
||||||
|
- une référence interne `refs/knowledge/candidates/<revision>` conserve l'objet avant retrait du worktree.
|
||||||
|
|
||||||
|
Le retrait normal n'utilise pas `--force`. Si une erreur survient après création du worktree possédé par l'opération, le backend utilise `worktree remove --force` pour retirer uniquement cet état temporaire connu. Un échec de nettoyage devient `WriteCleanupFailed` afin qu'un état inattendu ne soit pas masqué.
|
||||||
|
|
||||||
|
## Gate opérateur
|
||||||
|
|
||||||
|
Exécuter depuis la racine du dépôt après application du delta :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test "$(cat VERSION)" = '0.0.3-alpha.3'
|
||||||
|
grep -F 'version = "0.0.3-alpha.3"' Cargo.toml
|
||||||
|
|
||||||
|
git diff --check
|
||||||
|
|
||||||
|
python3 -B scripts/audit_markdown_headers.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts deltas history
|
||||||
|
python3 -B scripts/audit_markdown_links.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts deltas history
|
||||||
|
python3 -B scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts deltas history
|
||||||
|
|
||||||
|
cargo fmt --all
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
|
||||||
|
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_crate_root_facades.py crates
|
||||||
|
python3 -B scripts/audit_rust_export_completeness.py --root . --summary-only
|
||||||
|
|
||||||
|
cargo metadata --format-version 1 --no-deps > /tmp/sasedev-knowledge-0.0.3-alpha.3-metadata.json
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p knowledge-git-lib --test git_read_path
|
||||||
|
cargo test -p knowledge-git-lib --test git_write_candidate
|
||||||
|
cargo test --workspace --all-targets --all-features
|
||||||
|
|
||||||
|
python3 - <<'PYGATE'
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
history = Path('history/0.0.3/alpha.2.md').read_text(encoding='utf-8')
|
||||||
|
assert '\nPASS\n' in history
|
||||||
|
|
||||||
|
write = Path('crates/knowledge-git-lib/src/write.rs').read_text(encoding='utf-8')
|
||||||
|
backend = Path('crates/knowledge-git-lib/src/backend.rs').read_text(encoding='utf-8')
|
||||||
|
errors = Path('crates/knowledge-git-lib/src/error.rs').read_text(encoding='utf-8')
|
||||||
|
api = Path('crates/knowledge-git-api/src/write.rs').read_text(encoding='utf-8')
|
||||||
|
plan = Path('docs/plans/003-V0_0_3_GIT_WRITE_PLAN.md').read_text(encoding='utf-8')
|
||||||
|
changelog = Path('CHANGELOG.md').read_text(encoding='utf-8')
|
||||||
|
|
||||||
|
for required in (
|
||||||
|
'pub fn create_candidate(',
|
||||||
|
'symlink_metadata',
|
||||||
|
'CandidateMutationMismatch',
|
||||||
|
'GIT_AUTHOR_NAME',
|
||||||
|
'GIT_COMMITTER_NAME',
|
||||||
|
'--no-verify',
|
||||||
|
'--no-gpg-sign',
|
||||||
|
'HEAD^1',
|
||||||
|
'refs/knowledge/candidates/',
|
||||||
|
'RemoveWriteWorktree',
|
||||||
|
):
|
||||||
|
assert required in write, required
|
||||||
|
|
||||||
|
assert 'GIT_CONFIG_GLOBAL' in backend
|
||||||
|
assert 'GIT_CONFIG_NOSYSTEM' in backend
|
||||||
|
assert 'core.hooksPath' in backend
|
||||||
|
assert 'write_worktrees_path' in backend
|
||||||
|
assert 'empty_global_config_path' in backend
|
||||||
|
|
||||||
|
for required_error in (
|
||||||
|
'WriteWorktreeExists',
|
||||||
|
'UnsafeMutationTarget',
|
||||||
|
'MutationTargetMissing',
|
||||||
|
'MutationFilesystemFailed',
|
||||||
|
'CandidateMutationMismatch',
|
||||||
|
'InvalidCandidate',
|
||||||
|
'WriteCleanupFailed',
|
||||||
|
):
|
||||||
|
assert required_error in errors, required_error
|
||||||
|
|
||||||
|
assert 'trait GitRepositoryWriter' in api
|
||||||
|
assert 'fn create_candidate(' in api
|
||||||
|
assert 'fn push_candidate(' in api
|
||||||
|
assert 'impl knowledge_git_api::GitRepositoryWriter for crate::GitBackend' not in write
|
||||||
|
assert 'GitPushRequest' not in write
|
||||||
|
assert 'push_candidate' not in write
|
||||||
|
|
||||||
|
assert '`alpha.2` a ensuite matérialisé le contrat d\'écriture provider-neutral et sa gate opérateur est `PASS`' in plan
|
||||||
|
assert '## 0.0.3' not in changelog
|
||||||
|
assert not Path('history/0.0.3/alpha.3.md').exists()
|
||||||
|
assert not Path('crates/knowledge-forge-gitea-lib').exists()
|
||||||
|
assert not Path('crates/knowledge-store-postgres-lib').exists()
|
||||||
|
assert not Path('migrations').exists()
|
||||||
|
|
||||||
|
print('Knowledge 0.0.3-alpha.3 candidate-worktree audit: clean')
|
||||||
|
PYGATE
|
||||||
|
|
||||||
|
python3 - <<'PYTEXT'
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
roots = [Path('README.md'), Path('RULES.md'), Path('ROADMAP.md'), Path('CHANGELOG.md'), Path('docs'), Path('prompts'), Path('deltas'), Path('history')]
|
||||||
|
paths = []
|
||||||
|
for root in roots:
|
||||||
|
if root.is_file():
|
||||||
|
paths.append(root)
|
||||||
|
elif root.is_dir():
|
||||||
|
paths.extend(path for path in root.rglob('*') if path.is_file())
|
||||||
|
for path in paths:
|
||||||
|
data = path.read_bytes()
|
||||||
|
assert b'\r' not in data, path
|
||||||
|
assert data.endswith(b'\n'), path
|
||||||
|
assert not data.endswith(b'\n\n'), path
|
||||||
|
text = data.decode('utf-8')
|
||||||
|
for number, line in enumerate(text.splitlines(), start=1):
|
||||||
|
assert line == line.rstrip(' \t'), (path, number)
|
||||||
|
print(f'Knowledge 0.0.3-alpha.3 text hygiene: clean ({len(paths)} file(s))')
|
||||||
|
PYTEXT
|
||||||
|
|
||||||
|
test -z "$(git diff --name-only -- crates/knowledge-git-api crates/knowledge-app-lib Web CHANGELOG.md ROADMAP.md)"
|
||||||
|
test ! -e history/0.0.3/alpha.3.md
|
||||||
|
|
||||||
|
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 -f Web/knowledge-template-lab/package-lock.json
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git diff --cached --check
|
||||||
|
git status --short
|
||||||
|
```
|
||||||
|
|
||||||
|
La gate ne rejoue pas npm/Vite/HTTP : aucun fichier `Web/`, launcher ou contrat runtime Web n'est modifié.
|
||||||
|
|
||||||
|
## Transition
|
||||||
|
|
||||||
|
### PASS
|
||||||
|
|
||||||
|
Produire `history/0.0.3/alpha.3.md`, l'appliquer, commit/push `0.0.3-alpha.3`, puis préparer `0.0.3-alpha.4` pour l'observation distante, le compare-and-exchange et le push gardé avec conflits explicites.
|
||||||
|
|
||||||
|
### FAIL / PARTIAL / BLOCKED
|
||||||
|
|
||||||
|
Conserver le résultat réel dans son history avant toute correction. Une correction bornée utilise `alpha.3.fix.N` ; une remise en cause du cycle de vie des worktrees, de la frontière provider-neutral ou de la stratégie process Git exige un `REPLAN` explicite.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/002-KNOWLEDGE_ARCHITECTURE.md -->
|
<!-- file: docs/architecture/002-KNOWLEDGE_ARCHITECTURE.md -->
|
||||||
<!-- version: 15 -->
|
<!-- version: 16 -->
|
||||||
|
|
||||||
# Architecture de fondation Knowledge
|
# Architecture de fondation Knowledge
|
||||||
|
|
||||||
@@ -135,12 +135,16 @@ Les données générées résident hors du dépôt applicatif et hors des dépô
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
<git-state-root>/
|
<git-state-root>/
|
||||||
|
empty-global.gitconfig
|
||||||
hooks-disabled/
|
hooks-disabled/
|
||||||
repositories/
|
repositories/
|
||||||
<source-key>.git/
|
<source-key>.git/
|
||||||
snapshots/
|
snapshots/
|
||||||
<source-key>/
|
<source-key>/
|
||||||
<resolved-revision>/
|
<resolved-revision>/
|
||||||
|
write-worktrees/
|
||||||
|
<source-key>/
|
||||||
|
candidate/
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
||||||
@@ -387,7 +391,11 @@ Après publication vérifiée de `v0.0.2`, la prochaine mission explicitement en
|
|||||||
|
|
||||||
`alpha.2` matérialise cette frontière dans `knowledge-git-api` sans implémentation de backend : `GitMutationPath` confine lexicalement les mutations à des chemins relatifs normalisés hors `.git`, `GitFileMutation` représente écriture ou suppression, `GitCommitIdentity` impose une identité explicite, `GitBranch` borne la cible à une référence de branche complète valide, et `GitRemoteState` représente soit l'absence attendue soit une révision distante attendue. `GitWriteRequest`, `GitPushRequest` et `GitPushOutcome` portent respectivement la création de candidate, la comparaison-et-échange distante et son résultat. Le trait `GitRepositoryWriter` reste séparé de `GitRepository` afin qu'un consommateur ou fake lecture seule n'acquière aucune obligation d'écriture.
|
`alpha.2` matérialise cette frontière dans `knowledge-git-api` sans implémentation de backend : `GitMutationPath` confine lexicalement les mutations à des chemins relatifs normalisés hors `.git`, `GitFileMutation` représente écriture ou suppression, `GitCommitIdentity` impose une identité explicite, `GitBranch` borne la cible à une référence de branche complète valide, et `GitRemoteState` représente soit l'absence attendue soit une révision distante attendue. `GitWriteRequest`, `GitPushRequest` et `GitPushOutcome` portent respectivement la création de candidate, la comparaison-et-échange distante et son résultat. Le trait `GitRepositoryWriter` reste séparé de `GitRepository` afin qu'un consommateur ou fake lecture seule n'acquière aucune obligation d'écriture.
|
||||||
|
|
||||||
Le confinement filesystem contre les symlinks, la neutralisation des hooks, la création du commit et l'exécution du push gardé restent des responsabilités de `knowledge-git-lib` à partir de `alpha.3`; l'API passive ne prétend pas les prouver seule.
|
`alpha.3` implémente la moitié locale de ce contrat dans `knowledge-git-lib`. Le backend crée un worktree détaché temporaire sous `write-worktrees/<source-key>/candidate`, applique uniquement les chemins demandés et refuse tout composant filesystem qui est un symlink. Le staging est contrôlé contre l'ensemble exact des mutations avant commit. L'identité auteur/committer est injectée explicitement ; la configuration Git système et globale est neutralisée, les hooks sont redirigés vers `hooks-disabled`, et le commit désactive également hook/signature à l'appel.
|
||||||
|
|
||||||
|
Après commit, le backend vérifie `HEAD` et `HEAD^1`, exige que le parent soit exactement la `GitRevision` de base puis conserve l'objet candidat sous `refs/knowledge/candidates/<revision>`. Le worktree possédé est retiré après succès ; après une erreur survenue dans un worktree créé par l'opération, le backend tente une suppression forcée de cet état temporaire. Un worktree préexistant n'est jamais supprimé silencieusement et produit une erreur récupérable.
|
||||||
|
|
||||||
|
Le push distant gardé reste réservé à `alpha.4`. `alpha.3` ne met à jour aucune branche remote et n'implémente ni rebase ni merge.
|
||||||
|
|
||||||
PostgreSQL, workflow complet de proposition, auth/admin, parsing final et adapter Gitea restent des missions distinctes.
|
PostgreSQL, workflow complet de proposition, auth/admin, parsing final et adapter Gitea restent des missions distinctes.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/003-V0_0_3_GIT_WRITE_PLAN.md -->
|
<!-- file: docs/plans/003-V0_0_3_GIT_WRITE_PLAN.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Plan 0.0.3 — écriture Git contrôlée et push gardé
|
# Plan 0.0.3 — écriture Git contrôlée et push gardé
|
||||||
|
|
||||||
@@ -165,12 +165,14 @@ La gate de `alpha.1` reste une gate de cadrage : elle valide baseline, documenta
|
|||||||
|
|
||||||
### alpha.3 — worktree d'écriture et commit candidat
|
### alpha.3 — worktree d'écriture et commit candidat
|
||||||
|
|
||||||
|
- partir de `alpha.2 = PASS` historisé et committé ;
|
||||||
- matérialiser dans `knowledge-git-lib` un worktree d'écriture isolé dérivé d'une base résolue ;
|
- matérialiser dans `knowledge-git-lib` un worktree d'écriture isolé dérivé d'une base résolue ;
|
||||||
- appliquer create/update/delete sous confinement ;
|
- appliquer create/update/delete sous confinement et refuser les traversées par symlink ;
|
||||||
- neutraliser hooks/configuration ambiante pertinente ;
|
- neutraliser la configuration Git système/globale pertinente et les hooks ;
|
||||||
- créer un commit avec identité explicite ;
|
- créer un commit avec identité auteur/committer explicite ;
|
||||||
- retourner une révision candidate vérifiée ;
|
- vérifier que le candidat a exactement la base demandée comme parent ;
|
||||||
- nettoyer l'état temporaire de manière déterministe sans masquer un état inattendu.
|
- retenir le candidat sous une référence backend interne sans mettre à jour de branche distante ;
|
||||||
|
- nettoyer l'état temporaire de manière déterministe sans supprimer silencieusement un worktree préexistant.
|
||||||
|
|
||||||
### alpha.4 — push gardé et concurrence
|
### alpha.4 — push gardé et concurrence
|
||||||
|
|
||||||
@@ -235,7 +237,7 @@ La release est fermable lorsque :
|
|||||||
- les règles/architecture décrivent l'état effectivement obtenu ;
|
- les règles/architecture décrivent l'état effectivement obtenu ;
|
||||||
- beta et RC ont fermé les non-régressions prévues.
|
- beta et RC ont fermé les non-régressions prévues.
|
||||||
|
|
||||||
Le `PARTIAL` de `alpha.1` a autorisé `0.0.3-alpha.1.fix.1`. Le retour opérateur de ce premier fix reste `PARTIAL` parce que son transcript s'arrête avant les assertions finales imposées par le delta ; il autorise donc uniquement `0.0.3-alpha.1.fix.2`. Un `PASS` complet de `alpha.1.fix.2` autorise `0.0.3-alpha.2`. Tout autre statut produit d'abord son history puis un nouveau fix ou REPLAN selon le diagnostic.
|
Le `PARTIAL` de `alpha.1` a autorisé `0.0.3-alpha.1.fix.1`. Le premier fix est resté `PARTIAL`, puis `alpha.1.fix.2` a fermé la preuve en `PASS`. `alpha.2` a ensuite matérialisé le contrat d'écriture provider-neutral et sa gate opérateur est `PASS`; son history autorise `0.0.3-alpha.3`. Tout autre statut de `alpha.3` produit d'abord son history puis un fix borné ou un REPLAN selon le diagnostic.
|
||||||
|
|
||||||
## Revue d'horizon
|
## Revue d'horizon
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/001-VALIDATION_MATRIX.md -->
|
<!-- file: docs/validation/001-VALIDATION_MATRIX.md -->
|
||||||
<!-- version: 34 -->
|
<!-- version: 35 -->
|
||||||
|
|
||||||
# Matrice de validation Knowledge
|
# Matrice de validation Knowledge
|
||||||
|
|
||||||
@@ -135,6 +135,27 @@ La tranche de contrat d'écriture Git doit prouver :
|
|||||||
- les tests workspace passent car la façade publique de la crate change ;
|
- les tests workspace passent car la façade publique de la crate change ;
|
||||||
- `knowledge-git-lib`, `knowledge-app-lib`, `Web/`, PostgreSQL et adapters forge ne sont pas modifiés par cette tranche.
|
- `knowledge-git-lib`, `knowledge-app-lib`, `Web/`, PostgreSQL et adapters forge ne sont pas modifiés par cette tranche.
|
||||||
|
|
||||||
|
## Gate de `0.0.3-alpha.3`
|
||||||
|
|
||||||
|
La tranche de création locale du commit candidat doit prouver :
|
||||||
|
|
||||||
|
- `VERSION = 0.0.3-alpha.3` et version workspace identique ;
|
||||||
|
- history `history/0.0.3/alpha.2.md` présent avec statut `PASS` et committé avant application ;
|
||||||
|
- `knowledge-git-api` inchangé par rapport à `alpha.2` ;
|
||||||
|
- `knowledge-git-lib` crée un worktree d'écriture isolé sous sa racine d'état sans modifier les snapshots de lecture ;
|
||||||
|
- create/update/delete limités exactement aux chemins de mutation demandés ;
|
||||||
|
- toute traversée par symlink refusée avant écriture hors worktree ;
|
||||||
|
- configuration Git système/globale pertinente neutralisée et hooks désactivés ;
|
||||||
|
- identité auteur/committer explicitement fournie par `GitWriteRequest` ;
|
||||||
|
- commit candidat vérifié comme enfant direct de la révision de base ;
|
||||||
|
- candidat retenu sous une référence backend interne avant suppression du worktree temporaire ;
|
||||||
|
- worktree temporaire supprimé après succès et après erreur possédée, mais worktree préexistant jamais effacé silencieusement ;
|
||||||
|
- aucun push distant, rebase, merge, forge, PostgreSQL ou orchestration application ajouté ;
|
||||||
|
- test d'intégration Git réel prouvant modification/création/suppression, identité explicite, base inchangée et nettoyage ;
|
||||||
|
- test Unix prouvant qu'un symlink versionné ne permet pas d'écrire hors du worktree ;
|
||||||
|
- `cargo fmt --all`, check, Clippy strict, tests ciblés `knowledge-git-lib` et tests workspace propres ;
|
||||||
|
- `knowledge-app-lib` et `Web/` sans diff, `Cargo.lock` ignoré/non suivi et staging final propre.
|
||||||
|
|
||||||
## PostgreSQL
|
## PostgreSQL
|
||||||
|
|
||||||
Aucune gate PostgreSQL n'est créée avant matérialisation d'une dépendance ou d'un schéma réel.
|
Aucune gate PostgreSQL n'est créée avant matérialisation d'une dépendance ou d'un schéma réel.
|
||||||
|
|||||||
149
history/0.0.3/alpha.3.md
Normal file
149
history/0.0.3/alpha.3.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<!-- file: history/0.0.3/alpha.3.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# History 0.0.3-alpha.3 — worktree d'écriture et commit candidat
|
||||||
|
|
||||||
|
## Statut
|
||||||
|
|
||||||
|
FAIL
|
||||||
|
|
||||||
|
La gate opérateur de `0.0.3-alpha.3` exécutée le `2026-09-29` échoue sur la compilation du nouveau test d'intégration `git_write_candidate`. Le défaut est limité à une assertion de test qui tente de comparer directement deux `Result<String, std::io::Error>` alors que `std::io::Error` n'implémente pas `PartialEq`.
|
||||||
|
|
||||||
|
L'implémentation produit passe `cargo check --workspace`; les audits structurels et spécifiques de la tranche restent propres. Le statut demeure néanmoins `FAIL`, car Clippy `--all-targets` et les tests requis ne compilent pas.
|
||||||
|
|
||||||
|
## Baseline opérateur
|
||||||
|
|
||||||
|
`alpha.2` `PASS` a été historisé, committé et poussé avant application de `alpha.3` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
history/0.0.3/alpha.2.md PASS
|
||||||
|
commit 88a63b0
|
||||||
|
message 0.0.3-alpha.2
|
||||||
|
branch master
|
||||||
|
push origin/master PASS
|
||||||
|
workspace après push propre
|
||||||
|
```
|
||||||
|
|
||||||
|
Le push observé a avancé `origin/master` de `3004e4b` à `88a63b0`.
|
||||||
|
|
||||||
|
## Version et documentation
|
||||||
|
|
||||||
|
Les contrôles initiaux passent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
VERSION 0.0.3-alpha.3
|
||||||
|
Cargo workspace version 0.0.3-alpha.3
|
||||||
|
git diff --check PASS
|
||||||
|
Markdown header audit clean (108 file(s))
|
||||||
|
Markdown link audit clean (78 local link(s), 108 file(s))
|
||||||
|
Markdown table audit clean (4 table(s), 108 file(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formatage et audits Rust
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
cargo fmt --all -- --check PASS
|
||||||
|
Rust test-layout audit clean (9 crate(s))
|
||||||
|
Rust blank-line audit clean (35 file(s))
|
||||||
|
Rust item-spacing audit clean (35 file(s))
|
||||||
|
Rust module-order audit clean (35 file(s))
|
||||||
|
Rust trait-import-block audit clean (35 file(s))
|
||||||
|
Rust re-export-block audit clean (35 file(s))
|
||||||
|
Rust crate-root facade audit clean (6 crate root(s))
|
||||||
|
Rust export completeness audit 0 candidate(s) across 9 package(s)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cargo, Clippy et tests
|
||||||
|
|
||||||
|
`cargo metadata` et `cargo check --workspace` passent. La première défaillance obligatoire survient pendant Clippy :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo metadata --format-version 1 --no-deps PASS
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
--all-features -- -D warnings FAIL
|
||||||
|
```
|
||||||
|
|
||||||
|
Erreur observée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
error[E0369]: binary operation `==` cannot be applied to type
|
||||||
|
`Result<String, std::io::Error>`
|
||||||
|
|
||||||
|
crates/knowledge-git-lib/tests/git_write_candidate.rs:253
|
||||||
|
assert_eq!(snapshot_content, Ok(String::from("first\n")));
|
||||||
|
|
||||||
|
std::io::Error does not implement PartialEq
|
||||||
|
```
|
||||||
|
|
||||||
|
Le chemin de lecture historique reste propre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo test -p knowledge-git-lib --test git_read_path PASS (2)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le nouveau test ciblé et la gate workspace reproduisent le même défaut de compilation :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo test -p knowledge-git-lib --test git_write_candidate FAIL E0369
|
||||||
|
cargo test --workspace --all-targets --all-features FAIL E0369
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun échec runtime du worktree/candidat n'est donc observé dans cette gate : `git_write_candidate` ne peut pas encore être exécuté jusqu'à ses assertions.
|
||||||
|
|
||||||
|
## Audits spécifiques après l'échec
|
||||||
|
|
||||||
|
Les contrôles statiques et de scope exécutés après l'échec passent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Knowledge 0.0.3-alpha.3 candidate-worktree audit: clean
|
||||||
|
Knowledge 0.0.3-alpha.3 text hygiene: clean (108 file(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
Ils confirment notamment :
|
||||||
|
|
||||||
|
- le contrat `alpha.2` historisé `PASS` ;
|
||||||
|
- la présence du worktree candidat, des contrôles de symlink, de l'identité explicite, de `--no-verify`, `--no-gpg-sign`, `HEAD^1` et de la référence candidate interne ;
|
||||||
|
- la neutralisation des configurations Git globale/système et des hooks ;
|
||||||
|
- l'absence de `push_candidate` dans l'implémentation `alpha.3` ;
|
||||||
|
- l'absence de diff sous `knowledge-git-api`, `knowledge-app-lib`, `Web/`, `CHANGELOG.md` et `ROADMAP.md` ;
|
||||||
|
- `Cargo.lock` présent localement, ignoré et non suivi ;
|
||||||
|
- le package-lock Web présent.
|
||||||
|
|
||||||
|
Le staging final est cohérent avec la tranche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
M Cargo.toml
|
||||||
|
M README.md
|
||||||
|
M VERSION
|
||||||
|
M crates/knowledge-git-lib/src/backend.rs
|
||||||
|
M crates/knowledge-git-lib/src/error.rs
|
||||||
|
M crates/knowledge-git-lib/src/lib.rs
|
||||||
|
A crates/knowledge-git-lib/src/write.rs
|
||||||
|
A crates/knowledge-git-lib/tests/git_write_candidate.rs
|
||||||
|
M crates/knowledge-git-lib/unit_tests/backend.rs
|
||||||
|
A deltas/0.0.3/alpha.3.md
|
||||||
|
M docs/architecture/002-KNOWLEDGE_ARCHITECTURE.md
|
||||||
|
M docs/plans/003-V0_0_3_GIT_WRITE_PLAN.md
|
||||||
|
M docs/validation/001-VALIDATION_MATRIX.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Diagnostic
|
||||||
|
|
||||||
|
Le défaut est borné au test :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let snapshot_content = std::fs::read_to_string(snapshot.path().join("content.txt"));
|
||||||
|
assert_eq!(snapshot_content, Ok(String::from("first\n")));
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette comparaison exige `PartialEq` sur le type d'erreur du `Result`, ce que `std::io::Error` ne fournit pas. Le test doit vérifier le cas `Ok("first\n")` sans comparer la variante `Err`.
|
||||||
|
|
||||||
|
Aucun élément du retour opérateur ne justifie un REPLAN, une modification du contrat public ou un changement de l'implémentation de worktree.
|
||||||
|
|
||||||
|
## Décision
|
||||||
|
|
||||||
|
`0.0.3-alpha.3` est `FAIL`.
|
||||||
|
|
||||||
|
La correction autorisée est `0.0.3-alpha.3.fix.1`, bornée à l'assertion de test fautive et au suivi version/plan. Elle doit rejouer Clippy, le test `git_write_candidate` et la gate workspace avant d'autoriser `alpha.4`.
|
||||||
Reference in New Issue
Block a user