0.0.3-alpha.4
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 35
|
# version: 36
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
@@ -16,7 +16,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.3-alpha.3.fix.1"
|
version = "0.0.3-alpha.4"
|
||||||
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: 20 -->
|
<!-- version: 21 -->
|
||||||
|
|
||||||
# SaseDev Knowledge
|
# SaseDev Knowledge
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ 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.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.
|
`0.0.3-alpha.4` ferme maintenant le chemin Git générique d'écriture : après création locale d'un commit candidat isolé, `knowledge-git-lib` observe l'état réel de la branche distante puis effectue un push compare-and-exchange protégé par `--force-with-lease` contre l'état explicitement attendu. Une branche peut être créée uniquement lorsque son absence est attendue ; toute divergence devient un conflit récupérable avec l'état distant réellement observé et aucun écrasement silencieux. 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. 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.
|
`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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/knowledge-git-lib/src/error.rs
|
// file: crates/knowledge-git-lib/src/error.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
//! Recoverable errors for controlled Git process execution.
|
//! Recoverable errors for controlled Git process execution.
|
||||||
|
|
||||||
@@ -30,6 +30,10 @@ pub enum GitOperation {
|
|||||||
PreserveCandidate,
|
PreserveCandidate,
|
||||||
/// Remove one backend-owned candidate worktree.
|
/// Remove one backend-owned candidate worktree.
|
||||||
RemoveWriteWorktree,
|
RemoveWriteWorktree,
|
||||||
|
/// Observe the current state of one remote branch.
|
||||||
|
InspectRemote,
|
||||||
|
/// Push one candidate revision with an explicit compare-and-exchange guard.
|
||||||
|
PushCandidate,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for crate::GitOperation {
|
impl std::fmt::Display for crate::GitOperation {
|
||||||
@@ -47,6 +51,8 @@ impl std::fmt::Display for crate::GitOperation {
|
|||||||
crate::GitOperation::CommitCandidate => return formatter.write_str("commit candidate"),
|
crate::GitOperation::CommitCandidate => return formatter.write_str("commit candidate"),
|
||||||
crate::GitOperation::PreserveCandidate => return formatter.write_str("preserve candidate"),
|
crate::GitOperation::PreserveCandidate => return formatter.write_str("preserve candidate"),
|
||||||
crate::GitOperation::RemoveWriteWorktree => return formatter.write_str("remove write worktree"),
|
crate::GitOperation::RemoveWriteWorktree => return formatter.write_str("remove write worktree"),
|
||||||
|
crate::GitOperation::InspectRemote => return formatter.write_str("inspect remote branch"),
|
||||||
|
crate::GitOperation::PushCandidate => return formatter.write_str("push guarded candidate"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// file: crates/knowledge-git-lib/src/lib.rs
|
// file: crates/knowledge-git-lib/src/lib.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
//! 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, isolated candidate construction and process execution. It deliberately contains no
|
//! materialization, isolated candidate construction, guarded remote branch updates and process execution.
|
||||||
//! forge API, persistence, scheduler or application orchestration.
|
//! It deliberately contains no forge API, persistence, scheduler or application orchestration.
|
||||||
|
|
||||||
mod backend;
|
mod backend;
|
||||||
mod error;
|
mod error;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// file: crates/knowledge-git-lib/src/write.rs
|
// file: crates/knowledge-git-lib/src/write.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
//! Isolated candidate-commit creation for controlled Git sources.
|
//! Isolated candidate-commit creation and guarded remote publication for controlled Git sources.
|
||||||
|
|
||||||
impl crate::GitBackend {
|
impl crate::GitBackend {
|
||||||
/// Create one detached candidate commit from an explicit resolved base revision.
|
/// Create one detached candidate commit from an explicit resolved base revision.
|
||||||
@@ -223,6 +223,189 @@ impl crate::GitBackend {
|
|||||||
Err(_) => return Err(crate::GitBackendError::WriteCleanupFailed),
|
Err(_) => return Err(crate::GitBackendError::WriteCleanupFailed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempt one guarded remote branch update for a retained candidate revision.
|
||||||
|
///
|
||||||
|
/// The remote is first observed for an early conflict result, then Git's server-side
|
||||||
|
/// `--force-with-lease=<ref>:<expected>` guard performs the authoritative compare-and-exchange.
|
||||||
|
/// A failed guarded push is observed again so a concurrent update is returned as a recoverable
|
||||||
|
/// [`knowledge_git_api::GitPushOutcome::Conflict`] instead of being collapsed into a process error.
|
||||||
|
pub fn push_candidate(
|
||||||
|
&mut self,
|
||||||
|
source_id: &knowledge_domain_lib::SourceId,
|
||||||
|
request: &knowledge_git_api::GitPushRequest,
|
||||||
|
) -> Result<knowledge_git_api::GitPushOutcome, crate::GitBackendError> {
|
||||||
|
if !crate::is_safe_revision(request.candidate_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 mirror_result = self.ensure_mirror(source);
|
||||||
|
let mirror = match mirror_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let candidate_result = self.validate_retained_candidate(mirror.repository_path(), request.candidate_revision());
|
||||||
|
match candidate_result {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
let observed_result = self.observe_remote_state(mirror.repository_path(), request.target_branch());
|
||||||
|
let observed = match observed_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
if &observed != request.expected_remote() {
|
||||||
|
return Ok(knowledge_git_api::GitPushOutcome::Conflict { actual: observed });
|
||||||
|
}
|
||||||
|
let lease = match request.expected_remote() {
|
||||||
|
knowledge_git_api::GitRemoteState::Absent => format!("--force-with-lease={}:", request.target_branch().as_str()),
|
||||||
|
knowledge_git_api::GitRemoteState::Revision(revision) => {
|
||||||
|
if !crate::is_safe_revision(revision.as_str()) {
|
||||||
|
return Err(crate::GitBackendError::InvalidRevision);
|
||||||
|
}
|
||||||
|
format!("--force-with-lease={}:{}", request.target_branch().as_str(), revision.as_str())
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let refspec = format!("{}:{}", request.candidate_revision().as_str(), request.target_branch().as_str());
|
||||||
|
let push_result = self.run_controlled_git(
|
||||||
|
crate::GitOperation::PushCandidate,
|
||||||
|
vec![
|
||||||
|
std::ffi::OsString::from("--git-dir"),
|
||||||
|
mirror.repository_path().as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from("-c"),
|
||||||
|
std::ffi::OsString::from("remote.origin.mirror=false"),
|
||||||
|
std::ffi::OsString::from("push"),
|
||||||
|
std::ffi::OsString::from("--porcelain"),
|
||||||
|
std::ffi::OsString::from("--no-verify"),
|
||||||
|
std::ffi::OsString::from(lease),
|
||||||
|
std::ffi::OsString::from("origin"),
|
||||||
|
std::ffi::OsString::from(refspec),
|
||||||
|
],
|
||||||
|
Vec::new(),
|
||||||
|
);
|
||||||
|
match push_result {
|
||||||
|
Ok(_) => return Ok(knowledge_git_api::GitPushOutcome::Updated(request.candidate_revision().clone())),
|
||||||
|
Err(push_error) => {
|
||||||
|
let after_result = self.observe_remote_state(mirror.repository_path(), request.target_branch());
|
||||||
|
let after = match after_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return Err(push_error),
|
||||||
|
};
|
||||||
|
if &after != request.expected_remote() {
|
||||||
|
if after == knowledge_git_api::GitRemoteState::Revision(request.candidate_revision().clone()) {
|
||||||
|
return Ok(knowledge_git_api::GitPushOutcome::Updated(request.candidate_revision().clone()));
|
||||||
|
}
|
||||||
|
return Ok(knowledge_git_api::GitPushOutcome::Conflict { actual: after });
|
||||||
|
}
|
||||||
|
return Err(push_error);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_retained_candidate(&self, repository_path: &std::path::Path, candidate: &knowledge_git_api::GitRevision) -> Result<(), crate::GitBackendError> {
|
||||||
|
let candidate_reference = format!("refs/knowledge/candidates/{}^{{commit}}", candidate.as_str());
|
||||||
|
let output_result = self.run_controlled_git(
|
||||||
|
crate::GitOperation::InspectCandidate,
|
||||||
|
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("--verify"),
|
||||||
|
std::ffi::OsString::from("--end-of-options"),
|
||||||
|
std::ffi::OsString::from(candidate_reference),
|
||||||
|
],
|
||||||
|
Vec::new(),
|
||||||
|
);
|
||||||
|
let output = match output_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return Err(crate::GitBackendError::InvalidCandidate),
|
||||||
|
};
|
||||||
|
let text_result = std::str::from_utf8(&output);
|
||||||
|
let text = match text_result {
|
||||||
|
Ok(value) => value.trim(),
|
||||||
|
Err(_) => return Err(crate::GitBackendError::InvalidCandidate),
|
||||||
|
};
|
||||||
|
if text != candidate.as_str() {
|
||||||
|
return Err(crate::GitBackendError::InvalidCandidate);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn observe_remote_state(
|
||||||
|
&self,
|
||||||
|
repository_path: &std::path::Path,
|
||||||
|
branch: &knowledge_git_api::GitBranch,
|
||||||
|
) -> Result<knowledge_git_api::GitRemoteState, crate::GitBackendError> {
|
||||||
|
let output_result = self.run_controlled_git(
|
||||||
|
crate::GitOperation::InspectRemote,
|
||||||
|
vec![
|
||||||
|
std::ffi::OsString::from("--git-dir"),
|
||||||
|
repository_path.as_os_str().to_os_string(),
|
||||||
|
std::ffi::OsString::from("ls-remote"),
|
||||||
|
std::ffi::OsString::from("--refs"),
|
||||||
|
std::ffi::OsString::from("origin"),
|
||||||
|
std::ffi::OsString::from(branch.as_str()),
|
||||||
|
],
|
||||||
|
Vec::new(),
|
||||||
|
);
|
||||||
|
let output = match output_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
if output.is_empty() {
|
||||||
|
return Ok(knowledge_git_api::GitRemoteState::Absent);
|
||||||
|
}
|
||||||
|
let text_result = std::str::from_utf8(&output);
|
||||||
|
let text = match text_result {
|
||||||
|
Ok(value) => value.trim(),
|
||||||
|
Err(_) => return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::InspectRemote }),
|
||||||
|
};
|
||||||
|
let mut lines = text.lines();
|
||||||
|
let line = match lines.next() {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return Ok(knowledge_git_api::GitRemoteState::Absent),
|
||||||
|
};
|
||||||
|
if lines.next().is_some() {
|
||||||
|
return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::InspectRemote });
|
||||||
|
}
|
||||||
|
let mut fields = line.split('\t');
|
||||||
|
let revision = match fields.next() {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::InspectRemote }),
|
||||||
|
};
|
||||||
|
let remote_reference = match fields.next() {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::InspectRemote }),
|
||||||
|
};
|
||||||
|
if fields.next().is_some() || remote_reference != branch.as_str() || !crate::is_safe_revision(revision) {
|
||||||
|
return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::InspectRemote });
|
||||||
|
}
|
||||||
|
return Ok(knowledge_git_api::GitRemoteState::Revision(knowledge_git_api::GitRevision::new(revision)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl knowledge_git_api::GitRepositoryWriter for crate::GitBackend {
|
||||||
|
type Error = crate::GitBackendError;
|
||||||
|
|
||||||
|
fn create_candidate(
|
||||||
|
&mut self,
|
||||||
|
source_id: &knowledge_domain_lib::SourceId,
|
||||||
|
request: &knowledge_git_api::GitWriteRequest,
|
||||||
|
) -> Result<knowledge_git_api::GitRevision, Self::Error> {
|
||||||
|
return crate::GitBackend::create_candidate(self, source_id, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_candidate(
|
||||||
|
&mut self,
|
||||||
|
source_id: &knowledge_domain_lib::SourceId,
|
||||||
|
request: &knowledge_git_api::GitPushRequest,
|
||||||
|
) -> Result<knowledge_git_api::GitPushOutcome, Self::Error> {
|
||||||
|
return crate::GitBackend::push_candidate(self, source_id, request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_mutation(worktree_path: &std::path::Path, mutation: &knowledge_git_api::GitFileMutation) -> Result<(), crate::GitBackendError> {
|
fn apply_mutation(worktree_path: &std::path::Path, mutation: &knowledge_git_api::GitFileMutation) -> Result<(), crate::GitBackendError> {
|
||||||
|
|||||||
282
crates/knowledge-git-lib/tests/git_write_push.rs
Normal file
282
crates/knowledge-git-lib/tests/git_write_push.rs
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
// file: crates/knowledge-git-lib/tests/git_write_push.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Integration tests for guarded real Git pushes using local bare remotes.
|
||||||
|
|
||||||
|
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-push-{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_with_path(repository: &std::path::Path, arguments: &[&str], path: &std::path::Path) -> bool {
|
||||||
|
let mut command = std::process::Command::new("git");
|
||||||
|
command.current_dir(repository);
|
||||||
|
command.args(arguments);
|
||||||
|
command.arg(path);
|
||||||
|
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_dir_output(repository: &std::path::Path, reference: &str) -> Option<String> {
|
||||||
|
let mut command = std::process::Command::new("git");
|
||||||
|
command.arg("--git-dir");
|
||||||
|
command.arg(repository);
|
||||||
|
command.arg("rev-parse");
|
||||||
|
command.arg("--verify");
|
||||||
|
command.arg(reference);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
let text_result = std::str::from_utf8(&output.stdout);
|
||||||
|
match text_result {
|
||||||
|
Ok(value) => return Some(value.trim().to_owned()),
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_remote(root: &std::path::Path) -> Option<(std::path::PathBuf, std::path::PathBuf)> {
|
||||||
|
let remote = root.join("remote.git");
|
||||||
|
let seed = root.join("seed");
|
||||||
|
if std::fs::create_dir_all(root).is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !run_git_with_path(root, &["init", "--bare"], &remote) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !run_git_with_path(root, &["init", "-b", "main"], &seed) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !run_git(&seed, &["config", "user.name", "Remote Writer"]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !run_git(&seed, &["config", "user.email", "remote@example.invalid"]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !run_git(&seed, &["config", "commit.gpgsign", "false"]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if std::fs::write(seed.join("content.txt"), "first\n").is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !run_git(&seed, &["add", "content.txt"]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !run_git(&seed, &["commit", "-m", "first"]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !run_git_with_path(&seed, &["remote", "add", "origin"], &remote) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !run_git(&seed, &["push", "origin", "main"]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some((remote, seed));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configured_backend(root: &std::path::Path, remote: &std::path::Path) -> Option<(knowledge_git_lib::GitBackend, knowledge_domain_lib::SourceId)> {
|
||||||
|
let source_id_result = knowledge_domain_lib::SourceId::new("integration-push-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(), remote.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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn branch(value: &str) -> Option<knowledge_git_api::GitBranch> {
|
||||||
|
let result = knowledge_git_api::GitBranch::new(value);
|
||||||
|
match result {
|
||||||
|
Ok(value) => return Some(value),
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidate_for(
|
||||||
|
backend: &mut knowledge_git_lib::GitBackend,
|
||||||
|
source_id: &knowledge_domain_lib::SourceId,
|
||||||
|
base: knowledge_git_api::GitRevision,
|
||||||
|
path: &str,
|
||||||
|
content: &[u8],
|
||||||
|
message: &str,
|
||||||
|
) -> Option<knowledge_git_api::GitRevision> {
|
||||||
|
let mutation = match mutation_path(path) {
|
||||||
|
Some(value) => knowledge_git_api::GitFileMutation::Write { path: value, content: content.to_vec() },
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
let identity = match commit_identity() {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
let request_result = knowledge_git_api::GitWriteRequest::new(base, vec![mutation], identity, message);
|
||||||
|
let request = match request_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
let candidate_result = knowledge_git_api::GitRepositoryWriter::create_candidate(backend, source_id, &request);
|
||||||
|
match candidate_result {
|
||||||
|
Ok(value) => return Some(value),
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guarded_push_updates_expected_branch_and_creates_only_expected_absent_branch() {
|
||||||
|
let root = test_root("success");
|
||||||
|
let remote_result = prepare_remote(&root);
|
||||||
|
assert!(remote_result.is_some());
|
||||||
|
let (remote, _) = match remote_result {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let backend_result = configured_backend(&root, &remote);
|
||||||
|
assert!(backend_result.is_some());
|
||||||
|
let (mut backend, source_id) = match backend_result {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let base = match resolved_main(&mut backend, &source_id) {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let candidate = match candidate_for(&mut backend, &source_id, base.clone(), "content.txt", b"second\n", "second") {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let main = match branch("refs/heads/main") {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let push = knowledge_git_api::GitPushRequest::new(main, knowledge_git_api::GitRemoteState::Revision(base), candidate.clone());
|
||||||
|
let outcome = knowledge_git_api::GitRepositoryWriter::push_candidate(&mut backend, &source_id, &push);
|
||||||
|
assert_eq!(outcome, Ok(knowledge_git_api::GitPushOutcome::Updated(candidate.clone())));
|
||||||
|
assert_eq!(run_git_dir_output(&remote, "refs/heads/main"), Some(candidate.as_str().to_owned()));
|
||||||
|
let proposal_candidate = match candidate_for(&mut backend, &source_id, candidate, "proposal.txt", b"proposal\n", "proposal") {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let proposal = match branch("refs/heads/proposal") {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let create = knowledge_git_api::GitPushRequest::new(proposal, knowledge_git_api::GitRemoteState::Absent, proposal_candidate.clone());
|
||||||
|
let create_outcome = knowledge_git_api::GitRepositoryWriter::push_candidate(&mut backend, &source_id, &create);
|
||||||
|
assert_eq!(create_outcome, Ok(knowledge_git_api::GitPushOutcome::Updated(proposal_candidate.clone())));
|
||||||
|
assert_eq!(run_git_dir_output(&remote, "refs/heads/proposal"), Some(proposal_candidate.as_str().to_owned()));
|
||||||
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guarded_push_returns_conflict_after_concurrent_remote_change_without_overwrite() {
|
||||||
|
let root = test_root("conflict");
|
||||||
|
let remote_result = prepare_remote(&root);
|
||||||
|
assert!(remote_result.is_some());
|
||||||
|
let (remote, seed) = match remote_result {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let backend_result = configured_backend(&root, &remote);
|
||||||
|
assert!(backend_result.is_some());
|
||||||
|
let (mut backend, source_id) = match backend_result {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let base = match resolved_main(&mut backend, &source_id) {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let candidate = match candidate_for(&mut backend, &source_id, base.clone(), "content.txt", b"candidate\n", "candidate") {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
assert!(std::fs::write(seed.join("content.txt"), "concurrent\n").is_ok());
|
||||||
|
assert!(run_git(&seed, &["add", "content.txt"]));
|
||||||
|
assert!(run_git(&seed, &["commit", "-m", "concurrent"]));
|
||||||
|
assert!(run_git(&seed, &["push", "origin", "main"]));
|
||||||
|
let concurrent_text = match run_git_dir_output(&remote, "refs/heads/main") {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let concurrent = knowledge_git_api::GitRevision::new(concurrent_text.as_str());
|
||||||
|
assert_ne!(concurrent, base);
|
||||||
|
let main = match branch("refs/heads/main") {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let request = knowledge_git_api::GitPushRequest::new(main, knowledge_git_api::GitRemoteState::Revision(base), candidate);
|
||||||
|
let outcome = knowledge_git_api::GitRepositoryWriter::push_candidate(&mut backend, &source_id, &request);
|
||||||
|
assert_eq!(outcome, Ok(knowledge_git_api::GitPushOutcome::Conflict { actual: knowledge_git_api::GitRemoteState::Revision(concurrent.clone()) }));
|
||||||
|
assert_eq!(run_git_dir_output(&remote, "refs/heads/main"), Some(concurrent.as_str().to_owned()));
|
||||||
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
186
deltas/0.0.3/alpha.4.md
Normal file
186
deltas/0.0.3/alpha.4.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<!-- file: deltas/0.0.3/alpha.4.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.0.3-alpha.4 — push gardé et concurrence
|
||||||
|
|
||||||
|
## Baseline requise
|
||||||
|
|
||||||
|
Partir de `0.0.3-alpha.3.fix.1` après application, commit et push de son history `PASS`.
|
||||||
|
|
||||||
|
La version devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.0.3-alpha.4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Objet
|
||||||
|
|
||||||
|
Fermer le scope fonctionnel alpha de `0.0.3` en implémentant le push Git provider-neutral gardé par l'état distant attendu.
|
||||||
|
|
||||||
|
La tranche :
|
||||||
|
|
||||||
|
- conserve la création locale de candidat acquise en `alpha.3.fix.1` ;
|
||||||
|
- exige que le candidat soit encore retenu sous `refs/knowledge/candidates/<revision>` ;
|
||||||
|
- observe la branche distante par `git ls-remote --refs origin <refs/heads/...>` ;
|
||||||
|
- compare l'état observé à `GitRemoteState::Absent` ou `GitRemoteState::Revision(...)` ;
|
||||||
|
- utilise `--force-with-lease=<branche>:<attendu>` comme compare-and-exchange autoritaire ;
|
||||||
|
- neutralise `remote.origin.mirror=true` uniquement pour la commande de push explicite du mirror ;
|
||||||
|
- crée une branche uniquement lorsque l'absence est explicitement attendue ;
|
||||||
|
- réobserve l'état distant après un rejet de push afin de produire un conflit récupérable en cas de divergence ;
|
||||||
|
- implémente `knowledge_git_api::GitRepositoryWriter` sur `GitBackend` ;
|
||||||
|
- ajoute des tests d'intégration avec remote bare local pour mise à jour, création et conflit sans écrasement.
|
||||||
|
|
||||||
|
Aucun rebase/merge automatique, aucun force-push non gardé, aucune API de forge, aucun PostgreSQL et aucune orchestration applicative de proposition ne sont introduits.
|
||||||
|
|
||||||
|
## Scope des fichiers
|
||||||
|
|
||||||
|
REPLACE :
|
||||||
|
|
||||||
|
```text
|
||||||
|
VERSION
|
||||||
|
Cargo.toml
|
||||||
|
README.md
|
||||||
|
crates/knowledge-git-lib/src/error.rs
|
||||||
|
crates/knowledge-git-lib/src/lib.rs
|
||||||
|
crates/knowledge-git-lib/src/write.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/tests/git_write_push.rs
|
||||||
|
deltas/0.0.3/alpha.4.md
|
||||||
|
```
|
||||||
|
|
||||||
|
DELETE : aucun.
|
||||||
|
|
||||||
|
`knowledge-git-api`, `knowledge-app-lib`, `Web/`, `CHANGELOG.md` et `ROADMAP.md` ne sont pas modifiés.
|
||||||
|
|
||||||
|
Aucun history `alpha.4` n'est inclus dans ce delta.
|
||||||
|
|
||||||
|
## Compare-and-exchange distant
|
||||||
|
|
||||||
|
Le chemin d'écriture complet devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
base explicite
|
||||||
|
-> candidat local retenu
|
||||||
|
-> observation distante
|
||||||
|
-> comparaison à l'état attendu
|
||||||
|
-> push --force-with-lease exact
|
||||||
|
-> Updated(candidate)
|
||||||
|
ou Conflict { actual }
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour une branche existante :
|
||||||
|
|
||||||
|
```text
|
||||||
|
--force-with-lease=refs/heads/main:<revision-attendue>
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour une branche qui doit être absente :
|
||||||
|
|
||||||
|
```text
|
||||||
|
--force-with-lease=refs/heads/proposal:
|
||||||
|
```
|
||||||
|
|
||||||
|
L'observation préalable fournit un conflit rapide, mais elle n'est pas la garde de concurrence définitive. La lease est évaluée par Git au moment du push et protège la course entre observation et mise à jour distante.
|
||||||
|
|
||||||
|
Un mirror créé avec `git clone --mirror` possède `remote.origin.mirror=true`. Le push de `alpha.4` fournit donc explicitement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
-c remote.origin.mirror=false
|
||||||
|
```
|
||||||
|
|
||||||
|
pour cette commande uniquement, afin d'autoriser le refspec ciblé sans transformer le mirror ou pousser toutes ses références internes.
|
||||||
|
|
||||||
|
## 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.4'
|
||||||
|
grep -F 'version = "0.0.3-alpha.4"' 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.4-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 -p knowledge-git-lib --test git_write_push
|
||||||
|
cargo test --workspace --all-targets --all-features
|
||||||
|
```
|
||||||
|
|
||||||
|
Les contrôles spécifiques sont volontairement courts et indépendants afin d'éviter les problèmes de copier/coller de blocs heredoc :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -F 'PASS' history/0.0.3/alpha.3.fix.1.md
|
||||||
|
grep -F 'pub fn push_candidate(' crates/knowledge-git-lib/src/write.rs
|
||||||
|
grep -F -- '--force-with-lease=' crates/knowledge-git-lib/src/write.rs
|
||||||
|
grep -F 'remote.origin.mirror=false' crates/knowledge-git-lib/src/write.rs
|
||||||
|
grep -F 'GitOperation::InspectRemote' crates/knowledge-git-lib/src/write.rs
|
||||||
|
grep -F 'GitOperation::PushCandidate' crates/knowledge-git-lib/src/write.rs
|
||||||
|
grep -F 'impl knowledge_git_api::GitRepositoryWriter for crate::GitBackend' crates/knowledge-git-lib/src/write.rs
|
||||||
|
grep -F 'guarded_push_updates_expected_branch_and_creates_only_expected_absent_branch' crates/knowledge-git-lib/tests/git_write_push.rs
|
||||||
|
grep -F 'guarded_push_returns_conflict_after_concurrent_remote_change_without_overwrite' crates/knowledge-git-lib/tests/git_write_push.rs
|
||||||
|
! grep -F '## 0.0.3' CHANGELOG.md
|
||||||
|
test ! -e history/0.0.3/alpha.4.md
|
||||||
|
test ! -e crates/knowledge-forge-gitea-lib
|
||||||
|
test ! -e crates/knowledge-store-postgres-lib
|
||||||
|
test ! -e migrations
|
||||||
|
test -z "$(git diff --name-only -- crates/knowledge-git-api crates/knowledge-app-lib Web CHANGELOG.md ROADMAP.md)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Hygiène texte :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -c '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=[p for r in roots for p in ([r] if r.is_file() else r.rglob("*")) if p.is_file()]; bad=[str(p) for p in paths if (lambda b: b"\r" in b or not b.endswith(b"\n") or b.endswith(b"\n\n") or any(line != line.rstrip(" \t") for line in b.decode("utf-8").splitlines()))(p.read_bytes())]; print(f"Knowledge 0.0.3-alpha.4 text hygiene: clean ({len(paths)} file(s))") if not bad else (_ for _ in ()).throw(AssertionError(bad))'
|
||||||
|
```
|
||||||
|
|
||||||
|
Politique des lockfiles et staging :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.4.md`, l'appliquer, commit/push `0.0.3-alpha.4`, puis préparer `0.0.3-beta.1` pour réconciliation, revue d'horizon étendue et stabilisation.
|
||||||
|
|
||||||
|
La revue d'horizon beta doit notamment réintroduire explicitement la trajectoire produit complète des trois sites PGR / Snippets / Howtos : templates publics et admin, prototypes Web, Actix Web, adaptation Maud, PostgreSQL, consultation/gestion et workflow de proposition/publication.
|
||||||
|
|
||||||
|
### FAIL / PARTIAL / BLOCKED
|
||||||
|
|
||||||
|
Conserver le résultat réel dans son history avant toute correction. Une correction bornée utilise `alpha.4.fix.N` ; une remise en cause de la garantie compare-and-exchange, de la frontière provider-neutral ou du modèle de remote exige un `REPLAN` explicite.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/002-KNOWLEDGE_ARCHITECTURE.md -->
|
<!-- file: docs/architecture/002-KNOWLEDGE_ARCHITECTURE.md -->
|
||||||
<!-- version: 16 -->
|
<!-- version: 17 -->
|
||||||
|
|
||||||
# Architecture de fondation Knowledge
|
# Architecture de fondation Knowledge
|
||||||
|
|
||||||
@@ -480,3 +480,21 @@ snippets source Git courte/contextualisée séparée
|
|||||||
Les dépôts de contenu ne sont pas fusionnés dans PGR et ne sont pas requis comme submodules Git du dépôt applicatif. Leur localisation et leurs credentials sont de la configuration de déploiement.
|
Les dépôts de contenu ne sont pas fusionnés dans PGR et ne sont pas requis comme submodules Git du dépôt applicatif. Leur localisation et leurs credentials sont de la configuration de déploiement.
|
||||||
|
|
||||||
Pendant `0.0.2`, cette configuration de déploiement est adaptée en configuration physique possédée par `knowledge-git-lib`. La racine de mirrors/worktrees reste un état généré reconstructible et ne remplace pas le futur stockage logique PostgreSQL.
|
Pendant `0.0.2`, cette configuration de déploiement est adaptée en configuration physique possédée par `knowledge-git-lib`. La racine de mirrors/worktrees reste un état généré reconstructible et ne remplace pas le futur stockage logique PostgreSQL.
|
||||||
|
|
||||||
|
## Écriture Git gardée acquise en 0.0.3-alpha.4
|
||||||
|
|
||||||
|
Le backend Git complète désormais le cycle provider-neutral d'écriture sans adapter de forge : un candidat retenu localement peut être publié vers une branche `refs/heads/...` uniquement sous garde explicite de l'état distant attendu.
|
||||||
|
|
||||||
|
Le chemin est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
candidat retenu sous refs/knowledge/candidates/<revision>
|
||||||
|
-> observation `git ls-remote --refs origin <branche>`
|
||||||
|
-> comparaison avec GitRemoteState attendu
|
||||||
|
-> git push --force-with-lease=<branche>:<attendu>
|
||||||
|
-> Updated(candidate) ou Conflict { actual }
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour `GitRemoteState::Absent`, la lease utilise une attente vide et autorise donc uniquement la création d'une branche encore absente. Pour `GitRemoteState::Revision(r)`, la mise à jour distante n'est acceptée que si la branche pointe encore exactement sur `r`. L'observation préalable fournit un conflit rapide ; `--force-with-lease` reste la garde autoritaire contre une course entre observation et push. Après un rejet de push, le backend observe à nouveau la branche afin de traduire une divergence concurrente en conflit récupérable.
|
||||||
|
|
||||||
|
Cette capacité ne fait aucun rebase/merge automatique et ne publie aucune pull/merge request. Elle utilise uniquement le remote `origin` possédé par le mirror contrôlé et conserve la forge hors du contrat.
|
||||||
|
|||||||
@@ -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: 6 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Plan 0.0.3 — écriture Git contrôlée et push gardé
|
# Plan 0.0.3 — écriture Git contrôlée et push gardé
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ Knowledge start prompt prompts/003-V0_0_3_START_PROMPT.md
|
|||||||
|
|
||||||
PGR `v0.1.3` reste une provenance historique de l'adoption fermée pendant `0.0.2`. Aucune version PGR plus récente n'est une source de cette release et aucune réévaluation PGR n'est engagée.
|
PGR `v0.1.3` reste une provenance historique de l'adoption fermée pendant `0.0.2`. Aucune version PGR plus récente n'est une source de cette release et aucune réévaluation PGR n'est engagée.
|
||||||
|
|
||||||
`0.0.3-alpha.1` et `alpha.1.fix.1` sont conservées `PARTIAL` avec leurs preuves réelles. `alpha.1.fix.2` a complété la gate documentaire et technique en `PASS`. `alpha.2` a matérialisé le contrat d'écriture provider-neutral en `PASS`. `alpha.3` a ensuite introduit le worktree et le commit candidat, mais sa gate est `FAIL` sur une assertion du nouveau test d'intégration qui compare un `Result<String, std::io::Error>` avec `assert_eq!`. `alpha.3.fix.1` corrige uniquement cette preuve de test avant toute progression vers `alpha.4`.
|
`0.0.3-alpha.1` et `alpha.1.fix.1` sont conservées `PARTIAL` avec leurs preuves réelles. `alpha.1.fix.2` a complété la gate documentaire et technique en `PASS`. `alpha.2` a matérialisé le contrat d'écriture provider-neutral en `PASS`. `alpha.3` reste historisée `FAIL` pour son défaut de test `E0369`; `alpha.3.fix.1` a corrigé uniquement cette assertion et sa gate opérateur est `PASS`. `alpha.4` ferme maintenant le scope fonctionnel alpha avec le push gardé et les conflits explicites.
|
||||||
|
|
||||||
## Mission
|
## Mission
|
||||||
|
|
||||||
@@ -62,12 +62,14 @@ Décisions prises par l'étude `006` :
|
|||||||
- traiter une divergence comme conflit récupérable, sans rebase/merge automatique ;
|
- traiter une divergence comme conflit récupérable, sans rebase/merge automatique ;
|
||||||
- ne créer aucune nouvelle crate tant qu'un owner distinct n'est pas démontré.
|
- ne créer aucune nouvelle crate tant qu'un owner distinct n'est pas démontré.
|
||||||
|
|
||||||
Questions laissées à l'implémentation sans modifier la mission :
|
Décisions d'implémentation désormais résolues dans les alphas :
|
||||||
|
|
||||||
- forme exacte des types de mutation publique dans `knowledge-git-api` ;
|
- mutations publiques provider-neutral matérialisées dans `knowledge-git-api` ;
|
||||||
- stratégie interne de nommage/cleanup des worktrees d'écriture ;
|
- worktree d'écriture backend déterministe sous `write-worktrees/<source-key>/candidate` ;
|
||||||
- découpage exact des erreurs de conflit versus erreurs process ;
|
- candidats retenus sous `refs/knowledge/candidates/<revision>` ;
|
||||||
- détail de la commande Git utilisée pour la garde distante, sous réserve de la sémantique décidée.
|
- conflits distants représentés par `GitPushOutcome::Conflict`, séparés des erreurs process ;
|
||||||
|
- garde distante matérialisée par observation `ls-remote` puis `push --force-with-lease` exact ;
|
||||||
|
- `remote.origin.mirror=true` neutralisé uniquement sur le push ciblé afin de ne jamais publier toutes les refs internes du mirror.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
@@ -184,10 +186,14 @@ La gate de `alpha.1` reste une gate de cadrage : elle valide baseline, documenta
|
|||||||
|
|
||||||
### alpha.4 — push gardé et concurrence
|
### alpha.4 — push gardé et concurrence
|
||||||
|
|
||||||
- observer l'état de la branche distante ;
|
- partir de `alpha.3.fix.1 = PASS` historisé et committé ;
|
||||||
|
- observer l'état de la branche distante via le remote générique `origin` ;
|
||||||
|
- exiger que la révision candidate soit encore retenue par le backend ;
|
||||||
- pousser la révision candidate uniquement si la cible correspond à l'attente explicite ;
|
- pousser la révision candidate uniquement si la cible correspond à l'attente explicite ;
|
||||||
|
- utiliser `--force-with-lease` comme garde compare-and-exchange autoritaire ;
|
||||||
- gérer création de branche lorsque l'absence est attendue ;
|
- gérer création de branche lorsque l'absence est attendue ;
|
||||||
- produire un conflit récupérable en cas de divergence ;
|
- réobserver après rejet afin de produire un conflit récupérable en cas de divergence concurrente ;
|
||||||
|
- implémenter `GitRepositoryWriter` sur `GitBackend` maintenant que create + push sont disponibles ;
|
||||||
- prouver l'absence d'écrasement par tests d'intégration avec remote bare local ;
|
- prouver l'absence d'écrasement par tests d'intégration avec remote bare local ;
|
||||||
- fermer le scope fonctionnel alpha sans ajouter workflow de proposition ou forge.
|
- fermer le scope fonctionnel alpha sans ajouter workflow de proposition ou forge.
|
||||||
|
|
||||||
@@ -245,7 +251,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 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 avec une gate `PASS`. `alpha.3` est conservée `FAIL` à cause de l'assertion de test `E0369`; son history autorise uniquement `alpha.3.fix.1`. Un `PASS` complet de ce fix autorise `alpha.4`; tout autre statut produit d'abord son history puis un fix borné ou un 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 avec une gate `PASS`. `alpha.3` est conservée `FAIL` à cause de l'assertion de test `E0369`; `alpha.3.fix.1` corrige uniquement le test et sa gate opérateur est `PASS`. `alpha.4` peut donc fermer le scope fonctionnel alpha par le push gardé et les conflits explicites.
|
||||||
|
|
||||||
## 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: 35 -->
|
<!-- version: 36 -->
|
||||||
|
|
||||||
# Matrice de validation Knowledge
|
# Matrice de validation Knowledge
|
||||||
|
|
||||||
@@ -156,6 +156,27 @@ La tranche de création locale du commit candidat doit prouver :
|
|||||||
- `cargo fmt --all`, check, Clippy strict, tests ciblés `knowledge-git-lib` et tests workspace propres ;
|
- `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.
|
- `knowledge-app-lib` et `Web/` sans diff, `Cargo.lock` ignoré/non suivi et staging final propre.
|
||||||
|
|
||||||
|
|
||||||
|
## Gate de `0.0.3-alpha.4`
|
||||||
|
|
||||||
|
La tranche de push gardé et concurrence doit prouver :
|
||||||
|
|
||||||
|
- `VERSION = 0.0.3-alpha.4` et version workspace identique ;
|
||||||
|
- history `history/0.0.3/alpha.3.fix.1.md` présent avec statut `PASS` et committé avant application ;
|
||||||
|
- `GitBackend` implémente désormais `GitRepositoryWriter` sans modifier le contrat public `knowledge-git-api` ;
|
||||||
|
- le candidat à pousser doit exister sous la référence interne contrôlée créée par `alpha.3` ;
|
||||||
|
- l'état distant est observé par le remote générique `origin`, sans API de forge ;
|
||||||
|
- une attente `GitRemoteState::Revision(r)` utilise une lease exacte sur `r` ;
|
||||||
|
- une attente `GitRemoteState::Absent` n'autorise que la création d'une branche absente ;
|
||||||
|
- un écart observé avant le push produit immédiatement `GitPushOutcome::Conflict` ;
|
||||||
|
- `--force-with-lease` protège également la course entre observation et push ;
|
||||||
|
- après rejet d'un push, une nouvelle observation traduit une divergence en conflit récupérable sans écrasement ;
|
||||||
|
- aucun rebase/merge automatique, force-push non gardé, forge, PostgreSQL ou orchestration applicative n'est ajouté ;
|
||||||
|
- tests d'intégration Git réels avec remote bare local couvrant mise à jour d'une branche existante, création sous attente d'absence et conflit concurrent sans écrasement ;
|
||||||
|
- `cargo fmt --all`, check, Clippy strict, tests ciblés `knowledge-git-lib` et tests workspace propres ;
|
||||||
|
- `knowledge-git-api`, `knowledge-app-lib`, `Web/`, `CHANGELOG.md` et `ROADMAP.md` 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.
|
||||||
|
|||||||
132
history/0.0.3/alpha.4.md
Normal file
132
history/0.0.3/alpha.4.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<!-- file: history/0.0.3/alpha.4.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# History 0.0.3-alpha.4 — push gardé et concurrence
|
||||||
|
|
||||||
|
## Statut
|
||||||
|
|
||||||
|
PASS
|
||||||
|
|
||||||
|
La gate opérateur de `0.0.3-alpha.4` exécutée le `2026-09-29` est complète et propre. Le chemin Git d'écriture provider-neutral sait désormais pousser un candidat vers une branche distante uniquement lorsque l'état distant correspond à l'attente explicite, avec conflit récupérable en cas de divergence concurrente et sans écrasement silencieux.
|
||||||
|
|
||||||
|
## Baseline opérateur
|
||||||
|
|
||||||
|
`alpha.3.fix.1` `PASS` a été historisé, committé et poussé avant application de `alpha.4` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
history/0.0.3/alpha.3.fix.1.md PASS
|
||||||
|
commit aca00e6
|
||||||
|
message 0.0.3-alpha.3.fix.1
|
||||||
|
branch master
|
||||||
|
push origin/master PASS
|
||||||
|
workspace après push propre
|
||||||
|
```
|
||||||
|
|
||||||
|
Le push observé a avancé `origin/master` de `c3a8256` à `aca00e6`.
|
||||||
|
|
||||||
|
## Version et documentation
|
||||||
|
|
||||||
|
```text
|
||||||
|
VERSION 0.0.3-alpha.4
|
||||||
|
Cargo workspace version 0.0.3-alpha.4
|
||||||
|
git diff --check PASS
|
||||||
|
Markdown header audit clean (112 file(s))
|
||||||
|
Markdown link audit clean (78 local link(s), 112 file(s))
|
||||||
|
Markdown table audit clean (4 table(s), 112 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 (36 file(s))
|
||||||
|
Rust item-spacing audit clean (36 file(s))
|
||||||
|
Rust module-order audit clean (36 file(s))
|
||||||
|
Rust trait-import-block audit clean (36 file(s))
|
||||||
|
Rust re-export-block audit clean (36 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
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo metadata --format-version 1 --no-deps PASS
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets --all-features
|
||||||
|
-- -D warnings PASS
|
||||||
|
cargo test -p knowledge-git-lib --test git_read_path PASS (2)
|
||||||
|
cargo test -p knowledge-git-lib --test git_write_candidate PASS (2)
|
||||||
|
cargo test -p knowledge-git-lib --test git_write_push PASS (2)
|
||||||
|
cargo test --workspace --all-targets --all-features PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests workspace observés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
knowledge-app-lib 3 PASS
|
||||||
|
knowledge-domain-lib 3 PASS
|
||||||
|
knowledge-git-api 7 PASS
|
||||||
|
knowledge-git-lib 8 unit + 6 integration PASS
|
||||||
|
knowledge-sync-api 1 PASS
|
||||||
|
knowledge-web-lib 4 PASS
|
||||||
|
launchers 0 test chacun
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tests réels sur remote bare local prouvent :
|
||||||
|
|
||||||
|
- mise à jour d'une branche existante lorsque sa révision correspond exactement à l'attente ;
|
||||||
|
- création d'une branche uniquement lorsque son absence est attendue ;
|
||||||
|
- protection compare-and-exchange via `--force-with-lease` ;
|
||||||
|
- neutralisation locale de `remote.origin.mirror=true` pour le refspec ciblé du push ;
|
||||||
|
- détection d'une modification distante concurrente ;
|
||||||
|
- retour d'un conflit sans écrasement de la révision distante concurrente.
|
||||||
|
|
||||||
|
## Contrôles spécifiques
|
||||||
|
|
||||||
|
Les contrôles courts et indépendants passent tous :
|
||||||
|
|
||||||
|
```text
|
||||||
|
history alpha.3.fix.1 PASS confirmé
|
||||||
|
pub fn push_candidate présent
|
||||||
|
--force-with-lease présent
|
||||||
|
remote.origin.mirror=false présent
|
||||||
|
GitOperation::InspectRemote présent
|
||||||
|
GitOperation::PushCandidate présent
|
||||||
|
impl GitRepositoryWriter for GitBackend présent
|
||||||
|
tests succès/création et conflit concurrent présents
|
||||||
|
CHANGELOG 0.0.3 encore absent
|
||||||
|
history alpha.4 avant gate absent
|
||||||
|
crates Gitea/PostgreSQL et migrations absentes
|
||||||
|
aucun diff knowledge-git-api / knowledge-app-lib / Web PASS
|
||||||
|
aucun diff CHANGELOG.md / ROADMAP.md PASS
|
||||||
|
Knowledge 0.0.3-alpha.4 text hygiene clean (112 file(s))
|
||||||
|
Cargo.lock présent localement, ignoré, non suivi
|
||||||
|
package-lock Web présent
|
||||||
|
```
|
||||||
|
|
||||||
|
Après staging, `git diff --cached --check` passe.
|
||||||
|
|
||||||
|
Le staging final observé avant ajout du présent history est exactement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
M Cargo.toml
|
||||||
|
M README.md
|
||||||
|
M VERSION
|
||||||
|
M crates/knowledge-git-lib/src/error.rs
|
||||||
|
M crates/knowledge-git-lib/src/lib.rs
|
||||||
|
M crates/knowledge-git-lib/src/write.rs
|
||||||
|
A crates/knowledge-git-lib/tests/git_write_push.rs
|
||||||
|
A deltas/0.0.3/alpha.4.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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Décision
|
||||||
|
|
||||||
|
`0.0.3-alpha.4` est `PASS`.
|
||||||
|
|
||||||
|
Le scope fonctionnel alpha de `0.0.3` est fermé : lecture Git, création de candidat et push gardé provider-neutral sont acquis. `0.0.3-beta.1` peut maintenant réconcilier l'architecture et les `DEFER`, préparer le changelog candidat, rejouer la non-régression de stabilisation et effectuer la revue d'horizon produit complète des trois sites PGR / HOWTOs / snippets avant la release candidate.
|
||||||
Reference in New Issue
Block a user