0.0.3-alpha.2
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: Cargo.toml
|
||||
# version: 32
|
||||
# version: 33
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
@@ -16,7 +16,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.3-alpha.1.fix.2"
|
||||
version = "0.0.3-alpha.2"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
repository = "https://git.sasedev.com/Sasedev/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.3-alpha.1` ouvre la release d'écriture Git contrôlée et de push gardé. Cette première tranche est exclusivement un cadrage : elle sélectionne un worktree d'écriture isolé, un contrat d'écriture distinct de la lecture et une mise à jour distante avec attente explicite sur l'état précédent. Aucun code produit n'est encore modifié. PostgreSQL, forge, workflow complet de proposition, rebase/merge automatique et auth/admin restent hors scope.
|
||||
`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.
|
||||
|
||||
`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.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/knowledge-git-api/src/lib.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Provider-neutral contracts for Git-backed Knowledge sources.
|
||||
//!
|
||||
@@ -10,6 +10,7 @@ mod reference;
|
||||
mod repository;
|
||||
mod revision;
|
||||
mod snapshot;
|
||||
mod write;
|
||||
|
||||
/// Opaque Git reference requested by a caller, such as a branch, tag or object expression.
|
||||
pub use self::reference::GitReference;
|
||||
@@ -19,6 +20,11 @@ pub use self::repository::GitRepository;
|
||||
pub use self::revision::GitRevision;
|
||||
/// Read-only local materialization of one authoritative Git revision.
|
||||
pub use self::snapshot::GitSnapshot;
|
||||
/// Provider-neutral types and operations for controlled Git writes.
|
||||
pub use self::write::{
|
||||
GitBranch, GitCommitIdentity, GitFileMutation, GitMutationPath, GitPushOutcome, GitPushRequest, GitRemoteState, GitRepositoryWriter, GitWriteInputError,
|
||||
GitWriteRequest,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/lib.rs"]
|
||||
|
||||
319
crates/knowledge-git-api/src/write.rs
Normal file
319
crates/knowledge-git-api/src/write.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
// file: crates/knowledge-git-api/src/write.rs
|
||||
// version: 1
|
||||
|
||||
/// Invalid value supplied to the provider-neutral Git write contract.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum GitWriteInputError {
|
||||
/// A mutation path is empty or contains only whitespace.
|
||||
EmptyMutationPath,
|
||||
/// A mutation path is absolute or uses a platform-specific separator.
|
||||
AbsoluteMutationPath,
|
||||
/// A mutation path contains an empty, current-directory or parent-directory component.
|
||||
UnnormalizedMutationPath,
|
||||
/// A mutation path addresses Git's private `.git` area.
|
||||
ProtectedGitPath,
|
||||
/// A commit identity name is empty or contains a forbidden line break or NUL byte.
|
||||
InvalidCommitIdentityName,
|
||||
/// A commit identity email is empty or contains a forbidden line break or NUL byte.
|
||||
InvalidCommitIdentityEmail,
|
||||
/// A commit message is empty or contains a NUL byte.
|
||||
InvalidCommitMessage,
|
||||
/// A write request does not contain any file mutation.
|
||||
NoMutations,
|
||||
/// More than one mutation targets the same normalized path.
|
||||
DuplicateMutationPath,
|
||||
/// A target branch is not a valid full `refs/heads/...` Git reference.
|
||||
InvalidBranch,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for crate::GitWriteInputError {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
crate::GitWriteInputError::EmptyMutationPath => return formatter.write_str("Git mutation path must not be empty"),
|
||||
crate::GitWriteInputError::AbsoluteMutationPath => return formatter.write_str("Git mutation path must be relative and use '/' separators"),
|
||||
crate::GitWriteInputError::UnnormalizedMutationPath => return formatter.write_str("Git mutation path must be normalized"),
|
||||
crate::GitWriteInputError::ProtectedGitPath => return formatter.write_str("Git mutation path must not address .git"),
|
||||
crate::GitWriteInputError::InvalidCommitIdentityName => return formatter.write_str("Git commit identity name is invalid"),
|
||||
crate::GitWriteInputError::InvalidCommitIdentityEmail => return formatter.write_str("Git commit identity email is invalid"),
|
||||
crate::GitWriteInputError::InvalidCommitMessage => return formatter.write_str("Git commit message is invalid"),
|
||||
crate::GitWriteInputError::NoMutations => return formatter.write_str("Git write request must contain at least one mutation"),
|
||||
crate::GitWriteInputError::DuplicateMutationPath => return formatter.write_str("Git write request must not mutate one path more than once"),
|
||||
crate::GitWriteInputError::InvalidBranch => return formatter.write_str("Git target branch must be a valid refs/heads reference"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for crate::GitWriteInputError {}
|
||||
|
||||
/// Normalized relative path that may be mutated inside an isolated Git write worktree.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct GitMutationPath(String);
|
||||
|
||||
impl crate::GitMutationPath {
|
||||
/// Validate and build a provider-neutral repository-relative mutation path.
|
||||
pub fn new(value: &str) -> Result<crate::GitMutationPath, crate::GitWriteInputError> {
|
||||
if value.trim().is_empty() {
|
||||
return Err(crate::GitWriteInputError::EmptyMutationPath);
|
||||
}
|
||||
if value.starts_with('/') || value.contains('\\') || crate::write::has_windows_drive_prefix(value) {
|
||||
return Err(crate::GitWriteInputError::AbsoluteMutationPath);
|
||||
}
|
||||
if value.contains('\0') {
|
||||
return Err(crate::GitWriteInputError::UnnormalizedMutationPath);
|
||||
}
|
||||
for component in value.split('/') {
|
||||
if component.is_empty() || component == "." || component == ".." {
|
||||
return Err(crate::GitWriteInputError::UnnormalizedMutationPath);
|
||||
}
|
||||
if component.eq_ignore_ascii_case(".git") {
|
||||
return Err(crate::GitWriteInputError::ProtectedGitPath);
|
||||
}
|
||||
}
|
||||
return Ok(Self(value.to_owned()));
|
||||
}
|
||||
|
||||
/// Return the normalized repository-relative path.
|
||||
pub fn as_str(&self) -> &str {
|
||||
return self.0.as_str();
|
||||
}
|
||||
}
|
||||
|
||||
/// One file mutation requested for a candidate Git commit.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum GitFileMutation {
|
||||
/// Create or replace one file with the provided bytes.
|
||||
Write {
|
||||
/// Validated repository-relative file path.
|
||||
path: crate::GitMutationPath,
|
||||
/// Complete bytes that must become the file content.
|
||||
content: Vec<u8>,
|
||||
},
|
||||
/// Delete one existing path from the candidate tree.
|
||||
Delete {
|
||||
/// Validated repository-relative file path.
|
||||
path: crate::GitMutationPath,
|
||||
},
|
||||
}
|
||||
|
||||
impl crate::GitFileMutation {
|
||||
/// Return the validated path targeted by this mutation.
|
||||
pub fn path(&self) -> &crate::GitMutationPath {
|
||||
match self {
|
||||
crate::GitFileMutation::Write { path, .. } => return path,
|
||||
crate::GitFileMutation::Delete { path } => return path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Explicit author and committer identity used to create a candidate commit.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct GitCommitIdentity {
|
||||
name: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl crate::GitCommitIdentity {
|
||||
/// Validate and build an explicit Git commit identity.
|
||||
pub fn new(name: &str, email: &str) -> Result<crate::GitCommitIdentity, crate::GitWriteInputError> {
|
||||
if crate::write::invalid_identity_component(name) {
|
||||
return Err(crate::GitWriteInputError::InvalidCommitIdentityName);
|
||||
}
|
||||
if crate::write::invalid_identity_component(email) {
|
||||
return Err(crate::GitWriteInputError::InvalidCommitIdentityEmail);
|
||||
}
|
||||
return Ok(Self { name: name.to_owned(), email: email.to_owned() });
|
||||
}
|
||||
|
||||
/// Return the explicit identity name.
|
||||
pub fn name(&self) -> &str {
|
||||
return self.name.as_str();
|
||||
}
|
||||
|
||||
/// Return the explicit identity email.
|
||||
pub fn email(&self) -> &str {
|
||||
return self.email.as_str();
|
||||
}
|
||||
}
|
||||
|
||||
/// Validated full Git branch reference accepted as a remote write target.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct GitBranch(String);
|
||||
|
||||
impl crate::GitBranch {
|
||||
/// Validate a full `refs/heads/...` branch reference.
|
||||
pub fn new(value: &str) -> Result<crate::GitBranch, crate::GitWriteInputError> {
|
||||
if !crate::write::valid_branch_reference(value) {
|
||||
return Err(crate::GitWriteInputError::InvalidBranch);
|
||||
}
|
||||
return Ok(Self(value.to_owned()));
|
||||
}
|
||||
|
||||
/// Return the validated full branch reference.
|
||||
pub fn as_str(&self) -> &str {
|
||||
return self.0.as_str();
|
||||
}
|
||||
}
|
||||
|
||||
/// Expected or observed state of one remote branch.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum GitRemoteState {
|
||||
/// The branch does not exist.
|
||||
Absent,
|
||||
/// The branch resolves to the provided revision.
|
||||
Revision(crate::GitRevision),
|
||||
}
|
||||
|
||||
/// Request to build one candidate commit from an explicit base revision.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct GitWriteRequest {
|
||||
base_revision: crate::GitRevision,
|
||||
mutations: Vec<crate::GitFileMutation>,
|
||||
identity: crate::GitCommitIdentity,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl crate::GitWriteRequest {
|
||||
/// Validate and build a candidate-commit request.
|
||||
pub fn new(
|
||||
base_revision: crate::GitRevision,
|
||||
mutations: Vec<crate::GitFileMutation>,
|
||||
identity: crate::GitCommitIdentity,
|
||||
message: &str,
|
||||
) -> Result<crate::GitWriteRequest, crate::GitWriteInputError> {
|
||||
if mutations.is_empty() {
|
||||
return Err(crate::GitWriteInputError::NoMutations);
|
||||
}
|
||||
if message.trim().is_empty() || message.contains('\0') {
|
||||
return Err(crate::GitWriteInputError::InvalidCommitMessage);
|
||||
}
|
||||
let mut paths = std::collections::HashSet::new();
|
||||
for mutation in &mutations {
|
||||
if !paths.insert(mutation.path().as_str()) {
|
||||
return Err(crate::GitWriteInputError::DuplicateMutationPath);
|
||||
}
|
||||
}
|
||||
return Ok(Self { base_revision, mutations, identity, message: message.to_owned() });
|
||||
}
|
||||
|
||||
/// Return the resolved base revision from which the candidate must be created.
|
||||
pub fn base_revision(&self) -> &crate::GitRevision {
|
||||
return &self.base_revision;
|
||||
}
|
||||
|
||||
/// Return the ordered mutations to apply in the isolated write worktree.
|
||||
pub fn mutations(&self) -> &[crate::GitFileMutation] {
|
||||
return self.mutations.as_slice();
|
||||
}
|
||||
|
||||
/// Return the explicit author/committer identity.
|
||||
pub fn identity(&self) -> &crate::GitCommitIdentity {
|
||||
return &self.identity;
|
||||
}
|
||||
|
||||
/// Return the candidate commit message.
|
||||
pub fn message(&self) -> &str {
|
||||
return self.message.as_str();
|
||||
}
|
||||
}
|
||||
|
||||
/// Request to compare-and-exchange one remote branch to a candidate revision.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct GitPushRequest {
|
||||
target_branch: crate::GitBranch,
|
||||
expected_remote: crate::GitRemoteState,
|
||||
candidate_revision: crate::GitRevision,
|
||||
}
|
||||
|
||||
impl crate::GitPushRequest {
|
||||
/// Build a guarded remote update request from already validated values.
|
||||
pub fn new(target_branch: crate::GitBranch, expected_remote: crate::GitRemoteState, candidate_revision: crate::GitRevision) -> crate::GitPushRequest {
|
||||
return Self { target_branch, expected_remote, candidate_revision };
|
||||
}
|
||||
|
||||
/// Return the target remote branch.
|
||||
pub fn target_branch(&self) -> &crate::GitBranch {
|
||||
return &self.target_branch;
|
||||
}
|
||||
|
||||
/// Return the remote state that must still hold for the update to succeed.
|
||||
pub fn expected_remote(&self) -> &crate::GitRemoteState {
|
||||
return &self.expected_remote;
|
||||
}
|
||||
|
||||
/// Return the candidate revision to publish.
|
||||
pub fn candidate_revision(&self) -> &crate::GitRevision {
|
||||
return &self.candidate_revision;
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of one guarded remote branch update.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum GitPushOutcome {
|
||||
/// The target branch was updated to the candidate revision.
|
||||
Updated(
|
||||
/// Revision now expected at the updated target branch.
|
||||
crate::GitRevision,
|
||||
),
|
||||
/// The target branch no longer matched the expected state.
|
||||
Conflict {
|
||||
/// Actual remote state observed when the guarded update was rejected.
|
||||
actual: crate::GitRemoteState,
|
||||
},
|
||||
}
|
||||
|
||||
/// Provider-neutral write operations required from a Git repository backend.
|
||||
///
|
||||
/// This capability is intentionally separate from [`crate::GitRepository`] so read-only consumers
|
||||
/// and fakes do not need to implement write methods. A concrete backend may implement this trait
|
||||
/// independently from the read contract.
|
||||
pub trait GitRepositoryWriter {
|
||||
/// Implementation-specific recoverable error.
|
||||
type Error;
|
||||
|
||||
/// Create a candidate commit without updating a remote branch.
|
||||
fn create_candidate(&mut self, source_id: &knowledge_domain_lib::SourceId, request: &crate::GitWriteRequest) -> Result<crate::GitRevision, Self::Error>;
|
||||
|
||||
/// Attempt one guarded remote branch update for a previously created candidate revision.
|
||||
fn push_candidate(&mut self, source_id: &knowledge_domain_lib::SourceId, request: &crate::GitPushRequest) -> Result<crate::GitPushOutcome, Self::Error>;
|
||||
}
|
||||
|
||||
fn has_windows_drive_prefix(value: &str) -> bool {
|
||||
let bytes = value.as_bytes();
|
||||
if bytes.len() < 2 {
|
||||
return false;
|
||||
}
|
||||
return bytes[0].is_ascii_alphabetic() && bytes[1] == b':';
|
||||
}
|
||||
|
||||
fn invalid_identity_component(value: &str) -> bool {
|
||||
return value.trim().is_empty() || value.contains('\0') || value.contains('\n') || value.contains('\r');
|
||||
}
|
||||
|
||||
fn valid_branch_reference(value: &str) -> bool {
|
||||
const PREFIX: &str = "refs/heads/";
|
||||
let branch = match value.strip_prefix(PREFIX) {
|
||||
Some(value) => value,
|
||||
None => return false,
|
||||
};
|
||||
if branch.is_empty()
|
||||
|| branch.starts_with('/')
|
||||
|| branch.ends_with('/')
|
||||
|| branch.ends_with('.')
|
||||
|| branch.contains("..")
|
||||
|| branch.contains("@{")
|
||||
|| branch.contains("//")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for byte in branch.bytes() {
|
||||
if byte <= 0x20 || byte == 0x7f || matches!(byte, b'~' | b'^' | b':' | b'?' | b'*' | b'[' | b'\\') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for component in branch.split('/') {
|
||||
if component.is_empty() || component.starts_with('.') || component.ends_with(".lock") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/knowledge-git-api/unit_tests/lib.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct FakeGitError;
|
||||
@@ -22,6 +22,20 @@ impl crate::GitRepository for FakeGitRepository {
|
||||
}
|
||||
}
|
||||
|
||||
struct FakeGitWriter;
|
||||
|
||||
impl crate::GitRepositoryWriter for FakeGitWriter {
|
||||
type Error = FakeGitError;
|
||||
|
||||
fn create_candidate(&mut self, _source_id: &knowledge_domain_lib::SourceId, request: &crate::GitWriteRequest) -> Result<crate::GitRevision, Self::Error> {
|
||||
return Ok(crate::GitRevision::new(&format!("candidate:{}", request.base_revision().as_str())));
|
||||
}
|
||||
|
||||
fn push_candidate(&mut self, _source_id: &knowledge_domain_lib::SourceId, request: &crate::GitPushRequest) -> Result<crate::GitPushOutcome, Self::Error> {
|
||||
return Ok(crate::GitPushOutcome::Updated(request.candidate_revision().clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_repository_contract_does_not_require_a_forge() {
|
||||
let source_result = knowledge_domain_lib::SourceId::new("local-pgr");
|
||||
@@ -43,3 +57,140 @@ fn generic_repository_contract_does_not_require_a_forge() {
|
||||
assert_eq!(snapshot_result, Ok(crate::GitSnapshot::new(revision, std::path::PathBuf::from("/controlled/snapshot"))));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutation_paths_are_relative_normalized_and_protect_git_metadata() {
|
||||
let valid = crate::GitMutationPath::new("docs/rules/200-GOVERNANCE.md");
|
||||
assert_eq!(valid.as_ref().map(crate::GitMutationPath::as_str), Ok("docs/rules/200-GOVERNANCE.md"));
|
||||
assert_eq!(crate::GitMutationPath::new(""), Err(crate::GitWriteInputError::EmptyMutationPath));
|
||||
assert_eq!(crate::GitMutationPath::new("/etc/passwd"), Err(crate::GitWriteInputError::AbsoluteMutationPath));
|
||||
assert_eq!(crate::GitMutationPath::new("C:/outside.txt"), Err(crate::GitWriteInputError::AbsoluteMutationPath));
|
||||
assert_eq!(crate::GitMutationPath::new("docs\\rules.md"), Err(crate::GitWriteInputError::AbsoluteMutationPath));
|
||||
assert_eq!(crate::GitMutationPath::new("docs/../RULES.md"), Err(crate::GitWriteInputError::UnnormalizedMutationPath));
|
||||
assert_eq!(crate::GitMutationPath::new("docs/./RULES.md"), Err(crate::GitWriteInputError::UnnormalizedMutationPath));
|
||||
assert_eq!(crate::GitMutationPath::new(".git/config"), Err(crate::GitWriteInputError::ProtectedGitPath));
|
||||
assert_eq!(crate::GitMutationPath::new("nested/.GIT/config"), Err(crate::GitWriteInputError::ProtectedGitPath));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_identity_is_explicit_and_rejects_line_breaks() {
|
||||
let identity = crate::GitCommitIdentity::new("Knowledge Publisher", "knowledge@example.invalid");
|
||||
assert_eq!(identity.as_ref().map(crate::GitCommitIdentity::name), Ok("Knowledge Publisher"));
|
||||
assert_eq!(identity.as_ref().map(crate::GitCommitIdentity::email), Ok("knowledge@example.invalid"));
|
||||
assert_eq!(crate::GitCommitIdentity::new(" ", "knowledge@example.invalid"), Err(crate::GitWriteInputError::InvalidCommitIdentityName));
|
||||
assert_eq!(
|
||||
crate::GitCommitIdentity::new("Knowledge\nPublisher", "knowledge@example.invalid"),
|
||||
Err(crate::GitWriteInputError::InvalidCommitIdentityName)
|
||||
);
|
||||
assert_eq!(crate::GitCommitIdentity::new("Knowledge Publisher", " "), Err(crate::GitWriteInputError::InvalidCommitIdentityEmail));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_branch_is_limited_to_valid_heads_references() {
|
||||
let branch = crate::GitBranch::new("refs/heads/knowledge/proposal-42");
|
||||
assert_eq!(branch.as_ref().map(crate::GitBranch::as_str), Ok("refs/heads/knowledge/proposal-42"));
|
||||
for invalid in [
|
||||
"main",
|
||||
"refs/tags/v1",
|
||||
"refs/heads/",
|
||||
"refs/heads/.hidden",
|
||||
"refs/heads/topic..next",
|
||||
"refs/heads/topic.lock",
|
||||
"refs/heads/topic@{next",
|
||||
"refs/heads/topic\\next",
|
||||
"refs/heads/topic//next",
|
||||
] {
|
||||
assert_eq!(crate::GitBranch::new(invalid), Err(crate::GitWriteInputError::InvalidBranch), "{invalid}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_request_requires_mutations_unique_paths_and_message() {
|
||||
let identity_result = crate::GitCommitIdentity::new("Knowledge Publisher", "knowledge@example.invalid");
|
||||
let identity = match identity_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let empty = crate::GitWriteRequest::new(crate::GitRevision::new("base"), Vec::new(), identity.clone(), "candidate");
|
||||
assert_eq!(empty, Err(crate::GitWriteInputError::NoMutations));
|
||||
let path_result = crate::GitMutationPath::new("content.md");
|
||||
let path = match path_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let duplicates =
|
||||
vec![crate::GitFileMutation::Write { path: path.clone(), content: b"one".to_vec() }, crate::GitFileMutation::Delete { path: path.clone() }];
|
||||
let duplicate_request = crate::GitWriteRequest::new(crate::GitRevision::new("base"), duplicates, identity.clone(), "candidate");
|
||||
assert_eq!(duplicate_request, Err(crate::GitWriteInputError::DuplicateMutationPath));
|
||||
let mutation = crate::GitFileMutation::Write { path, content: b"content".to_vec() };
|
||||
let invalid_message = crate::GitWriteRequest::new(crate::GitRevision::new("base"), vec![mutation.clone()], identity.clone(), " ");
|
||||
assert_eq!(invalid_message, Err(crate::GitWriteInputError::InvalidCommitMessage));
|
||||
let request = crate::GitWriteRequest::new(crate::GitRevision::new("base"), vec![mutation], identity, "candidate");
|
||||
assert!(request.is_ok());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_request_models_absent_or_revision_remote_state() {
|
||||
let branch_result = crate::GitBranch::new("refs/heads/proposals/example");
|
||||
let branch = match branch_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let candidate = crate::GitRevision::new("candidate");
|
||||
let create = crate::GitPushRequest::new(branch.clone(), crate::GitRemoteState::Absent, candidate.clone());
|
||||
assert_eq!(create.target_branch(), &branch);
|
||||
assert_eq!(create.expected_remote(), &crate::GitRemoteState::Absent);
|
||||
assert_eq!(create.candidate_revision(), &candidate);
|
||||
let existing = crate::GitPushRequest::new(branch, crate::GitRemoteState::Revision(crate::GitRevision::new("old")), candidate);
|
||||
assert_eq!(existing.expected_remote(), &crate::GitRemoteState::Revision(crate::GitRevision::new("old")));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_contract_is_separate_from_read_contract_and_provider_neutral() {
|
||||
let source_result = knowledge_domain_lib::SourceId::new("snippets-extra");
|
||||
let source_id = match source_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let path_result = crate::GitMutationPath::new("snippets/example.md");
|
||||
let path = match path_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let identity_result = crate::GitCommitIdentity::new("Knowledge Publisher", "knowledge@example.invalid");
|
||||
let identity = match identity_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let write_result = crate::GitWriteRequest::new(
|
||||
crate::GitRevision::new("base"),
|
||||
vec![crate::GitFileMutation::Write { path, content: b"snippet".to_vec() }],
|
||||
identity,
|
||||
"Add snippet",
|
||||
);
|
||||
let write_request = match write_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let mut writer = FakeGitWriter;
|
||||
let candidate_result = crate::GitRepositoryWriter::create_candidate(&mut writer, &source_id, &write_request);
|
||||
assert_eq!(candidate_result, Ok(crate::GitRevision::new("candidate:base")));
|
||||
let candidate = match candidate_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let branch_result = crate::GitBranch::new("refs/heads/proposals/snippet");
|
||||
let branch = match branch_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let push_request = crate::GitPushRequest::new(branch, crate::GitRemoteState::Absent, candidate.clone());
|
||||
let push_result = crate::GitRepositoryWriter::push_candidate(&mut writer, &source_id, &push_request);
|
||||
assert_eq!(push_result, Ok(crate::GitPushOutcome::Updated(candidate)));
|
||||
return;
|
||||
}
|
||||
|
||||
232
deltas/0.0.3/alpha.2.md
Normal file
232
deltas/0.0.3/alpha.2.md
Normal file
@@ -0,0 +1,232 @@
|
||||
<!-- file: deltas/0.0.3/alpha.2.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.0.3-alpha.2 — contrat d'écriture Git provider-neutral
|
||||
|
||||
## Baseline requise
|
||||
|
||||
Partir de `0.0.3-alpha.1.fix.2` après application, commit et push de son history `PASS`.
|
||||
|
||||
La version devient :
|
||||
|
||||
```text
|
||||
0.0.3-alpha.2
|
||||
```
|
||||
|
||||
## Objet
|
||||
|
||||
Matérialiser uniquement la frontière publique provider-neutral nécessaire aux futures opérations d'écriture Git, sans implémenter encore le worktree d'écriture ou le push distant réel.
|
||||
|
||||
La tranche :
|
||||
|
||||
- conserve `GitRepository` comme contrat de lecture inchangé ;
|
||||
- ajoute `GitRepositoryWriter` comme capacité distincte afin de ne pas imposer l'écriture aux consommateurs/fakes lecture seule ;
|
||||
- introduit `GitMutationPath` pour des chemins relatifs normalisés, sans `.` / `..`, séparateur plateforme, chemin absolu, drive Windows ou composant `.git` ;
|
||||
- introduit `GitFileMutation` pour écrire/créer/remplacer ou supprimer un fichier ;
|
||||
- introduit `GitCommitIdentity` et exige une identité explicite ;
|
||||
- introduit `GitBranch` et borne la publication à une référence complète valide sous `refs/heads/...` ;
|
||||
- introduit `GitRemoteState` pour représenter explicitement branche absente ou révision attendue ;
|
||||
- introduit `GitWriteRequest`, `GitPushRequest` et `GitPushOutcome` pour séparer création de candidate, mise à jour distante gardée et conflit ;
|
||||
- rejette une requête sans mutation, un message vide/NUL et plusieurs mutations visant le même chemin ;
|
||||
- documente la matérialisation de la frontière sans déplacer les responsabilités backend prévues pour `alpha.3` / `alpha.4`.
|
||||
|
||||
Aucun processus Git d'écriture n'est lancé par cette tranche.
|
||||
|
||||
## Scope des fichiers
|
||||
|
||||
REPLACE :
|
||||
|
||||
```text
|
||||
VERSION
|
||||
Cargo.toml
|
||||
README.md
|
||||
crates/knowledge-git-api/src/lib.rs
|
||||
crates/knowledge-git-api/unit_tests/lib.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-api/src/write.rs
|
||||
deltas/0.0.3/alpha.2.md
|
||||
```
|
||||
|
||||
DELETE : aucun.
|
||||
|
||||
Aucun fichier sous `knowledge-git-lib`, `knowledge-app-lib` ou `Web/` n'est modifié.
|
||||
|
||||
Aucun history `alpha.2` n'est inclus dans ce delta.
|
||||
|
||||
## Contrat public matérialisé
|
||||
|
||||
Le contrat expose les valeurs suivantes depuis la façade `knowledge_git_api` :
|
||||
|
||||
```text
|
||||
GitWriteInputError
|
||||
GitMutationPath
|
||||
GitFileMutation
|
||||
GitCommitIdentity
|
||||
GitBranch
|
||||
GitRemoteState
|
||||
GitWriteRequest
|
||||
GitPushRequest
|
||||
GitPushOutcome
|
||||
GitRepositoryWriter
|
||||
```
|
||||
|
||||
Le chemin conceptuel reste :
|
||||
|
||||
```text
|
||||
GitRevision de base
|
||||
+ mutations confinées
|
||||
+ identité/message explicites
|
||||
-> create_candidate
|
||||
-> GitRevision candidate
|
||||
|
||||
GitBranch
|
||||
+ GitRemoteState attendu
|
||||
+ GitRevision candidate
|
||||
-> push_candidate
|
||||
-> Updated ou Conflict
|
||||
```
|
||||
|
||||
`alpha.2` définit ce contrat mais ne prétend pas encore prouver le confinement filesystem face aux symlinks, la neutralisation des hooks, la création effective d'un commit ou le compare-and-exchange distant. Ces preuves appartiennent au backend `knowledge-git-lib` des tranches suivantes.
|
||||
|
||||
## 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.2'
|
||||
grep -F 'version = "0.0.3-alpha.2"' 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.2-metadata.json
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
cargo test -p knowledge-git-api --all-targets --all-features
|
||||
cargo test --workspace --all-targets --all-features
|
||||
|
||||
python3 - <<'PYGATE'
|
||||
from pathlib import Path
|
||||
|
||||
api = Path('crates/knowledge-git-api/src/lib.rs').read_text(encoding='utf-8')
|
||||
write = Path('crates/knowledge-git-api/src/write.rs').read_text(encoding='utf-8')
|
||||
read_contract = Path('crates/knowledge-git-api/src/repository.rs').read_text(encoding='utf-8')
|
||||
manifest = Path('crates/knowledge-git-api/Cargo.toml').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 item in (
|
||||
'GitWriteInputError',
|
||||
'GitMutationPath',
|
||||
'GitFileMutation',
|
||||
'GitCommitIdentity',
|
||||
'GitBranch',
|
||||
'GitRemoteState',
|
||||
'GitWriteRequest',
|
||||
'GitPushRequest',
|
||||
'GitPushOutcome',
|
||||
'GitRepositoryWriter',
|
||||
):
|
||||
assert item in api, item
|
||||
assert item in write, item
|
||||
|
||||
assert 'fn refresh(' in read_contract
|
||||
assert 'fn resolve_revision(' in read_contract
|
||||
assert 'fn prepare_snapshot(' in read_contract
|
||||
assert 'create_candidate' not in read_contract
|
||||
assert 'push_candidate' not in read_contract
|
||||
|
||||
assert 'refs/heads/' in write
|
||||
assert 'ProtectedGitPath' in write
|
||||
assert 'DuplicateMutationPath' in write
|
||||
assert 'GitRemoteState::Absent' not in write or 'GitRemoteState' in write
|
||||
assert 'rebase' not in write.lower()
|
||||
assert 'merge' not in write.lower()
|
||||
|
||||
for forbidden in ('actix', 'tokio', 'gitea', 'gitlab', 'github', 'postgres', 'sqlx', 'diesel'):
|
||||
assert forbidden not in manifest.lower(), forbidden
|
||||
|
||||
assert '`alpha.1.fix.2` a complété la gate documentaire et technique en `PASS`' in plan
|
||||
assert '### alpha.2 — contrat d\'écriture provider-neutral' in plan
|
||||
assert '## 0.0.3' not in changelog
|
||||
|
||||
assert Path('history/0.0.3/alpha.1.fix.2.md').is_file()
|
||||
assert not Path('history/0.0.3/alpha.2.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.2 write-contract 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.2 text hygiene: clean ({len(paths)} file(s))')
|
||||
PYTEXT
|
||||
|
||||
test -z "$(git diff --name-only -- crates/knowledge-git-lib crates/knowledge-app-lib Web)"
|
||||
test ! -e history/0.0.3/alpha.2.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 : `Web/`, les launchers et leur contrat runtime ne sont pas modifiés.
|
||||
|
||||
## Transition
|
||||
|
||||
### PASS
|
||||
|
||||
Produire `history/0.0.3/alpha.2.md`, l'appliquer, commit/push `0.0.3-alpha.2`, puis préparer `0.0.3-alpha.3` pour le worktree d'écriture isolé et le commit candidat réel dans `knowledge-git-lib`.
|
||||
|
||||
### FAIL / PARTIAL / BLOCKED
|
||||
|
||||
Conserver le résultat réel dans son history avant toute correction. Une correction bornée utilise `alpha.2.fix.N` ; une modification de la mission ou de la frontière d'ownership exige un `REPLAN` explicite plutôt qu'un fix absorbant une nouvelle fonctionnalité.
|
||||
@@ -385,6 +385,10 @@ Après publication vérifiée de `v0.0.2`, la prochaine mission explicitement en
|
||||
|
||||
`0.0.3` borne la mise à jour distante à des branches `refs/heads/...` et exige un état distant attendu. Un push n'est accepté que lorsque cette attente reste vraie ; sinon le résultat est un conflit récupérable. Le backend ne résout pas ce conflit par rebase/merge automatique.
|
||||
|
||||
`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.
|
||||
|
||||
PostgreSQL, workflow complet de proposition, auth/admin, parsing final et adapter Gitea restent des missions distinctes.
|
||||
|
||||
## 10. Administration
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/003-V0_0_3_GIT_WRITE_PLAN.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# 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.
|
||||
|
||||
`0.0.3-alpha.1` a produit une gate technique propre jusqu'aux tests workspace mais reste `PARTIAL` : la fin de gate n'a pas été fournie et la revue opérateur a détecté un mauvais cycle de rédaction du changelog, du prompt suivant et de la roadmap. `0.0.3-alpha.1.fix.1` corrige uniquement ce workflow documentaire avant l'implémentation fonctionnelle.
|
||||
`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`, ce qui ferme le cadrage et autorise `alpha.2`. `alpha.2` matérialise maintenant uniquement le contrat d'écriture provider-neutral dans `knowledge-git-api`; le backend de worktree reste réservé à `alpha.3`.
|
||||
|
||||
## Mission
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/001-VALIDATION_MATRIX.md -->
|
||||
<!-- version: 33 -->
|
||||
<!-- version: 34 -->
|
||||
|
||||
# Matrice de validation Knowledge
|
||||
|
||||
@@ -117,6 +117,24 @@ Les tests doivent permettre plusieurs `source_id` et ne pas confondre famille de
|
||||
|
||||
Un test live de remote ou de forge doit être explicitement classé comme externe, séparé des tests unitaires et ne doit pas devenir une condition cachée pour les opérations locales.
|
||||
|
||||
## Gate de `0.0.3-alpha.2`
|
||||
|
||||
La tranche de contrat d'écriture Git doit prouver :
|
||||
|
||||
- `VERSION = 0.0.3-alpha.2` et version workspace identique ;
|
||||
- `knowledge-git-api` reste une crate passive sans dépendance de forge, runtime, filesystem backend ou PostgreSQL ;
|
||||
- `GitRepository` de lecture reste inchangé et implémentable sans capacité d'écriture ;
|
||||
- `GitRepositoryWriter` est une capacité distincte ;
|
||||
- les chemins de mutation refusent valeur vide, chemin absolu/séparateur plateforme, `.` / `..`, composant vide et `.git` ;
|
||||
- les mutations représentent écriture create/update et suppression, avec rejet des chemins dupliqués dans une même requête ;
|
||||
- l'identité de commit et le message sont explicites et validés ;
|
||||
- la branche cible est une référence complète valide sous `refs/heads/...` ;
|
||||
- l'état distant attendu représente explicitement branche absente ou révision connue ;
|
||||
- le résultat de push peut représenter succès ou conflit sans rebase/merge automatique ;
|
||||
- `cargo fmt --all`, check, Clippy strict et tests ciblés `knowledge-git-api` passent ;
|
||||
- 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.
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
Aucune gate PostgreSQL n'est créée avant matérialisation d'une dépendance ou d'un schéma réel.
|
||||
|
||||
142
history/0.0.3/alpha.2.md
Normal file
142
history/0.0.3/alpha.2.md
Normal file
@@ -0,0 +1,142 @@
|
||||
<!-- file: history/0.0.3/alpha.2.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# History 0.0.3-alpha.2 — contrat d'écriture Git provider-neutral
|
||||
|
||||
## Statut
|
||||
|
||||
PASS
|
||||
|
||||
La gate opérateur de `0.0.3-alpha.2` exécutée le `2026-09-29` est complète et propre. Elle valide la frontière publique d'écriture Git provider-neutral et autorise `0.0.3-alpha.3` pour le worktree d'écriture isolé et la création réelle du commit candidat.
|
||||
|
||||
## Baseline opérateur
|
||||
|
||||
`alpha.1.fix.2` `PASS` a été historisé, committé et poussé avant application de `alpha.2` :
|
||||
|
||||
```text
|
||||
history/0.0.3/alpha.1.fix.2.md PASS
|
||||
commit 3004e4b
|
||||
message 0.0.3-alpha.1.fix.2
|
||||
branch master
|
||||
push origin/master PASS
|
||||
workspace après push propre
|
||||
```
|
||||
|
||||
Le push observé a avancé `origin/master` de `dd63e85` à `3004e4b`.
|
||||
|
||||
## Version et documentation
|
||||
|
||||
Les contrôles de version et Markdown passent :
|
||||
|
||||
```text
|
||||
VERSION 0.0.3-alpha.2
|
||||
Cargo workspace version 0.0.3-alpha.2
|
||||
git diff --check PASS
|
||||
Markdown header audit clean (106 file(s))
|
||||
Markdown link audit clean (78 local link(s), 106 file(s))
|
||||
Markdown table audit clean (4 table(s), 106 file(s))
|
||||
```
|
||||
|
||||
## Formatage et audits Rust
|
||||
|
||||
La séquence locale imposée a été respectée :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
cargo fmt --all -- --check PASS
|
||||
Rust test-layout audit clean (9 crate(s))
|
||||
Rust blank-line audit clean (33 file(s))
|
||||
Rust item-spacing audit clean (33 file(s))
|
||||
Rust module-order audit clean (33 file(s))
|
||||
Rust trait-import-block audit clean (33 file(s))
|
||||
Rust re-export-block audit clean (33 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
|
||||
|
||||
Les neuf packages compilent en `0.0.3-alpha.2` et Clippy ne produit aucun warning avec `-D warnings`.
|
||||
|
||||
```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-api --all-targets --all-features PASS (7)
|
||||
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 PASS + 2 integration PASS
|
||||
knowledge-sync-api 1 PASS
|
||||
knowledge-web-lib 4 PASS
|
||||
launchers 0 test chacun
|
||||
```
|
||||
|
||||
Les sept tests `knowledge-git-api` valident notamment :
|
||||
|
||||
- la séparation du contrat d'écriture du contrat de lecture ;
|
||||
- l'absence de dépendance à une forge ;
|
||||
- la validation des chemins de mutation et la protection de `.git` ;
|
||||
- l'identité de commit explicite ;
|
||||
- les branches bornées à `refs/heads/...` ;
|
||||
- les états distants absent/révision ;
|
||||
- le rejet des requêtes sans mutation, messages invalides et chemins dupliqués.
|
||||
|
||||
## Audit spécifique d'écriture
|
||||
|
||||
L'audit de tranche passe :
|
||||
|
||||
```text
|
||||
Knowledge 0.0.3-alpha.2 write-contract audit: clean
|
||||
```
|
||||
|
||||
Il confirme notamment :
|
||||
|
||||
- la présence et le réexport des dix types/traits d'écriture attendus ;
|
||||
- `GitRepository` de lecture inchangé et sans méthodes `create_candidate` / `push_candidate` ;
|
||||
- la validation `refs/heads/...`, `ProtectedGitPath` et `DuplicateMutationPath` ;
|
||||
- l'absence de dépendances Actix, Tokio, forge, PostgreSQL ou ORM dans `knowledge-git-api` ;
|
||||
- `CHANGELOG.md` toujours sans section prématurée `0.0.3` ;
|
||||
- absence de history `alpha.2` avant staging ;
|
||||
- absence de crates Gitea/PostgreSQL et de migrations.
|
||||
|
||||
## Hygiène et scope
|
||||
|
||||
```text
|
||||
Knowledge 0.0.3-alpha.2 text hygiene: clean (106 file(s))
|
||||
```
|
||||
|
||||
La gate confirme également :
|
||||
|
||||
- aucun diff sous `crates/knowledge-git-lib`, `crates/knowledge-app-lib` ou `Web/` ;
|
||||
- `Cargo.lock` local présent, ignoré et non suivi ;
|
||||
- `Web/knowledge-template-lab/package-lock.json` présent ;
|
||||
- `git diff --cached --check` propre.
|
||||
|
||||
Le staging final contient exactement les changements attendus de la tranche :
|
||||
|
||||
```text
|
||||
M Cargo.toml
|
||||
M README.md
|
||||
M VERSION
|
||||
M crates/knowledge-git-api/src/lib.rs
|
||||
A crates/knowledge-git-api/src/write.rs
|
||||
M crates/knowledge-git-api/unit_tests/lib.rs
|
||||
A deltas/0.0.3/alpha.2.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.2` est `PASS`.
|
||||
|
||||
La frontière provider-neutral est acquise. La tranche suivante peut implémenter dans `knowledge-git-lib` le worktree d'écriture isolé, les mutations confinées, l'identité explicite, la neutralisation des hooks/configurations pertinentes et la création d'une révision candidate vérifiée. Le push distant gardé reste réservé à `alpha.4`.
|
||||
Reference in New Issue
Block a user