worktree-runtime 0.1.0a4__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/Cargo.lock +2 -2
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/Cargo.toml +2 -2
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/PKG-INFO +2 -2
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/AGENTS.md +12 -7
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/application/binding_lifecycle.rs +22 -11
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain/audit.rs +12 -8
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain/binding.rs +11 -2
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain/paths.rs +9 -14
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/audit_dir.rs +6 -5
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/audit_reader.rs +4 -2
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/audit_writer.rs +12 -2
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/binding_store.rs +21 -6
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/git_adapter.rs +0 -9
- worktree_runtime-0.1.1/crates/worktree_runtime_core/src/infrastructure/project_id_resolver.rs +177 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/worktree_base_resolver.rs +112 -93
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/worktree_config_loader.rs +35 -15
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure.rs +0 -1
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/common/mod.rs +1 -1
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/e2e/binding_lifecycle_tests.rs +10 -3
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/integration/audit_dir_tests.rs +2 -2
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/integration/audit_writer_tests.rs +8 -6
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/integration/binding_store_tests.rs +12 -12
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/integration/main.rs +0 -1
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/integration/project_id_resolver_tests.rs +42 -30
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/integration/worktree_base_resolver_tests.rs +27 -15
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/integration/worktree_config_loader_tests.rs +22 -14
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/unit/audit_tests.rs +26 -4
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_pyo3/Cargo.toml +6 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_pyo3/src/convert.rs +19 -4
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_pyo3/src/functions.rs +87 -42
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_pyo3/src/types.rs +19 -7
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_pyo3/tests/pyo3_signature_freeze_tests.rs +2 -2
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/pyproject.toml +3 -3
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/src/worktree_runtime/api/__init__.pyi +8 -8
- worktree_runtime-0.1.0a4/crates/worktree_runtime_core/src/infrastructure/path_resolution.rs +0 -8
- worktree_runtime-0.1.0a4/crates/worktree_runtime_core/src/infrastructure/project_id_resolver.rs +0 -133
- worktree_runtime-0.1.0a4/crates/worktree_runtime_core/tests/integration/path_resolution_tests.rs +0 -17
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/LICENSE +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/README.md +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/Cargo.toml +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/application/git_worktree_service.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/application/validation.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/application.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain/git_worktree.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain/github_policy.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain/porcelain.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/audit_file_io.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/env_keys.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/github_policy_loader.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/worktree_list_reader.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/lib.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/e2e/git_worktree_service_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/e2e/main.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/e2e/validation_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/integration/audit_file_io_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/integration/audit_reader_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/integration/git_adapter_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/integration/github_policy_loader_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/integration/worktree_list_reader_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/unit/binding_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/unit/env_keys_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/unit/git_worktree_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/unit/github_policy_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/unit/main.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/unit/paths_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/unit/porcelain_tests.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_pyo3/AGENTS.md +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_pyo3/src/lib.rs +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/src/_worktree_runtime_rust/__init__.py +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/src/worktree_runtime/AGENTS.md +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/src/worktree_runtime/SURFACE.yaml +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/src/worktree_runtime/__init__.py +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/src/worktree_runtime/api/__init__.py +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/src/worktree_runtime/presentation/__init__.py +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/src/worktree_runtime/presentation/audit_cli.py +0 -0
- {worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/src/worktree_runtime/py.typed +0 -0
|
@@ -743,7 +743,7 @@ dependencies = [
|
|
|
743
743
|
|
|
744
744
|
[[package]]
|
|
745
745
|
name = "worktree_runtime_core"
|
|
746
|
-
version = "0.1.
|
|
746
|
+
version = "0.1.1"
|
|
747
747
|
dependencies = [
|
|
748
748
|
"chrono",
|
|
749
749
|
"fs2",
|
|
@@ -760,7 +760,7 @@ dependencies = [
|
|
|
760
760
|
|
|
761
761
|
[[package]]
|
|
762
762
|
name = "worktree_runtime_pyo3"
|
|
763
|
-
version = "0.1.
|
|
763
|
+
version = "0.1.1"
|
|
764
764
|
dependencies = [
|
|
765
765
|
"chrono",
|
|
766
766
|
"pyo3",
|
|
@@ -3,7 +3,7 @@ resolver = "2"
|
|
|
3
3
|
members = ["crates/worktree_runtime_core", "crates/worktree_runtime_pyo3"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.1.
|
|
6
|
+
version = "0.1.1"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
rust-version = "1.85"
|
|
9
9
|
license = "LicenseRef-Proprietary"
|
|
@@ -18,7 +18,7 @@ sha1 = "0.10"
|
|
|
18
18
|
fs2 = "0.4"
|
|
19
19
|
tempfile = "3.10"
|
|
20
20
|
wait-timeout = "0.2"
|
|
21
|
-
pyo3 = { version = "0.29", features = ["
|
|
21
|
+
pyo3 = { version = "0.29", features = ["abi3-py310"] }
|
|
22
22
|
chrono = "0.4"
|
|
23
23
|
|
|
24
24
|
[profile.release]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: worktree-runtime
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Classifier: Development Status ::
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: License :: Other/Proprietary License
|
|
6
6
|
Classifier: Operating System :: MacOS
|
|
7
7
|
Classifier: Operating System :: Microsoft :: Windows
|
|
@@ -38,13 +38,18 @@ do not treat pytest as the SSOT.
|
|
|
38
38
|
- **Config precedence**: `NOVELER_WORKTREE_BASE` and
|
|
39
39
|
`config/worktree.yaml` overrides are honored only when `CI` or
|
|
40
40
|
`GITHUB_ACTIONS` is set (`worktree_base_resolver.rs`). Outside CI the
|
|
41
|
-
base is `~/.adaptive-task/worktrees/<project_id>`.
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
base is `~/.adaptive-task/worktrees/<project_id>`. A missing
|
|
42
|
+
`config/worktree.yaml` is the default path. A file that exists but cannot
|
|
43
|
+
be read or parsed raises `WORKTREE_CONFIG_UNREADABLE` from the resolvers
|
|
44
|
+
that load it (`resolve_worktree_base`, `resolve_worktree_dir`,
|
|
45
|
+
`resolve_known_worktree_bases`) when CI consults it.
|
|
46
|
+
- **project_id**: a missing `pyproject.toml` or a missing
|
|
47
|
+
`tool.adaptive-task.project_id` is `noveler`. An existing file that is not
|
|
48
|
+
valid TOML raises `WORKTREE_PROJECT_CONFIG_UNREADABLE`. A declared id that
|
|
49
|
+
fails `^[a-z][a-z0-9_-]{0,63}$` raises `ProjectIdInvalidError`
|
|
50
|
+
(`PROJECT_ID_INVALID`) from `resolve_project_id` and
|
|
51
|
+
`resolve_state_dir_name`; it is not used as a directory name. An empty id
|
|
52
|
+
stays `noveler`.
|
|
48
53
|
- **Logging**: `log::debug!` / `log::warn!` for diagnostics; the two
|
|
49
54
|
`eprintln!` breadcrumbs (`failopen_message`, `suspicious_extra_message`)
|
|
50
55
|
are frozen by tests and stay on stderr.
|
|
@@ -57,11 +57,13 @@ impl From<BindingStoreError> for RegisterBindingError {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
fn session_state_subpath(
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
fn session_state_subpath(
|
|
61
|
+
repo_root: &Path,
|
|
62
|
+
) -> Result<PathBuf, crate::infrastructure::project_id_resolver::ProjectResolveError> {
|
|
63
|
+
let pid = resolve_project_id(Some(&repo_root.to_string_lossy()))?;
|
|
64
|
+
Ok(PathBuf::from(resolve_state_dir_name(Some(&pid))?)
|
|
63
65
|
.join("state")
|
|
64
|
-
.join("adaptive-task")
|
|
66
|
+
.join("adaptive-task"))
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
pub fn register_binding(
|
|
@@ -95,7 +97,16 @@ pub fn register_binding(
|
|
|
95
97
|
let state_path = if let Some(sp) = state_path {
|
|
96
98
|
normalize_path(sp)
|
|
97
99
|
} else {
|
|
98
|
-
let subpath = session_state_subpath(&repo)
|
|
100
|
+
let subpath = session_state_subpath(&repo).map_err(|err| match err {
|
|
101
|
+
crate::infrastructure::project_id_resolver::ProjectResolveError::Invalid {
|
|
102
|
+
message,
|
|
103
|
+
reason_code,
|
|
104
|
+
} => RegisterBindingError::Validation {
|
|
105
|
+
reason_code,
|
|
106
|
+
message,
|
|
107
|
+
},
|
|
108
|
+
other => RegisterBindingError::Store(other.into()),
|
|
109
|
+
})?;
|
|
99
110
|
normalize_path(&PathBuf::from(&worktree).join(&subpath).to_string_lossy())
|
|
100
111
|
};
|
|
101
112
|
let binding = WorktreeBinding::new(WorktreeBindingFields {
|
|
@@ -117,7 +128,7 @@ pub fn register_binding(
|
|
|
117
128
|
message: validation.message,
|
|
118
129
|
});
|
|
119
130
|
}
|
|
120
|
-
let store = WorktreeBindingStore::new(&repo)
|
|
131
|
+
let store = WorktreeBindingStore::new(&repo)?;
|
|
121
132
|
store.upsert(&binding)?;
|
|
122
133
|
Ok(binding)
|
|
123
134
|
}
|
|
@@ -126,18 +137,18 @@ pub fn load_binding(
|
|
|
126
137
|
session_id: &str,
|
|
127
138
|
repo_root: &str,
|
|
128
139
|
) -> Result<Option<WorktreeBinding>, BindingStoreError> {
|
|
129
|
-
WorktreeBindingStore::new(repo_root)
|
|
140
|
+
WorktreeBindingStore::new(repo_root)?.load(session_id)
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
pub fn list_bindings(repo_root: &str) -> Result<Vec<WorktreeBinding>, BindingStoreError> {
|
|
133
|
-
WorktreeBindingStore::new(repo_root)
|
|
144
|
+
WorktreeBindingStore::new(repo_root)?.list()
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
pub fn remove_binding(
|
|
137
148
|
session_id: &str,
|
|
138
149
|
repo_root: &str,
|
|
139
150
|
) -> Result<Option<WorktreeBinding>, BindingStoreError> {
|
|
140
|
-
WorktreeBindingStore::new(repo_root)
|
|
151
|
+
WorktreeBindingStore::new(repo_root)?.remove(session_id)
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
/// Rebuild a binding from legacy `metadata.json` and persist it. `Ok(None)`
|
|
@@ -164,7 +175,7 @@ fn recover_binding_from_metadata(
|
|
|
164
175
|
metadata_root: &Path,
|
|
165
176
|
repo_root: &Path,
|
|
166
177
|
) -> Result<Option<WorktreeBinding>, BindingStoreError> {
|
|
167
|
-
let state_subpath = session_state_subpath(repo_root)
|
|
178
|
+
let state_subpath = session_state_subpath(repo_root)?;
|
|
168
179
|
let metadata = metadata_root
|
|
169
180
|
.join(state_subpath)
|
|
170
181
|
.join("sessions")
|
|
@@ -310,7 +321,7 @@ fn build_env_fallback_binding(
|
|
|
310
321
|
.map(|s| s.to_string())
|
|
311
322
|
.unwrap_or_default();
|
|
312
323
|
let state_path = PathBuf::from(&normalized_worktree)
|
|
313
|
-
.join(session_state_subpath(&repo))
|
|
324
|
+
.join(session_state_subpath(&repo).ok()?)
|
|
314
325
|
.to_string_lossy()
|
|
315
326
|
.to_string();
|
|
316
327
|
Some(WorktreeBinding::new(WorktreeBindingFields {
|
{worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain/audit.rs
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
//! Audit record value objects, schema constants, and pure assembly/parse.
|
|
2
|
-
use serde::
|
|
2
|
+
use serde::Serialize;
|
|
3
3
|
use serde_json::Value;
|
|
4
4
|
|
|
5
5
|
pub const RECORD_CAP_BYTES: usize = 4096;
|
|
6
|
-
pub const SCHEMA_VERSION: &str = "
|
|
6
|
+
pub const SCHEMA_VERSION: &str = "v2";
|
|
7
7
|
pub const VALID_KINDS: &[&str] = &["worktree_remove", "branch_delete"];
|
|
8
8
|
pub const VALID_INVOKERS: &[&str] = &[
|
|
9
9
|
"canonical_wrapper",
|
|
@@ -23,7 +23,7 @@ pub fn is_secret_pattern(text: &str) -> bool {
|
|
|
23
23
|
|| lower.contains("password")
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
#[derive(Debug, Clone, Serialize
|
|
26
|
+
#[derive(Debug, Clone, Serialize)]
|
|
27
27
|
pub struct AuditRecord {
|
|
28
28
|
pub schema_version: String,
|
|
29
29
|
pub timestamp: String,
|
|
@@ -35,7 +35,7 @@ pub struct AuditRecord {
|
|
|
35
35
|
pub host: String,
|
|
36
36
|
pub invoker: String,
|
|
37
37
|
pub branch: String,
|
|
38
|
-
pub
|
|
38
|
+
pub worktree: String,
|
|
39
39
|
pub outcome: String,
|
|
40
40
|
pub elapsed_ms: Option<i64>,
|
|
41
41
|
pub correlation_id: Option<String>,
|
|
@@ -50,7 +50,7 @@ pub struct AuditRecord {
|
|
|
50
50
|
pub struct AuditEventInput<'a> {
|
|
51
51
|
pub kind: &'a str,
|
|
52
52
|
pub branch: &'a str,
|
|
53
|
-
pub
|
|
53
|
+
pub worktree: &'a str,
|
|
54
54
|
pub invoker: &'a str,
|
|
55
55
|
pub outcome: &'a str,
|
|
56
56
|
pub session_id: Option<&'a str>,
|
|
@@ -78,7 +78,7 @@ pub fn assemble_record(input: AuditEventInput<'_>, runtime: AuditRuntimeFields<'
|
|
|
78
78
|
"schema_version": SCHEMA_VERSION, "timestamp": runtime.timestamp, "kind": input.kind,
|
|
79
79
|
"session_id": input.session_id, "pid": runtime.pid, "parent_pid": runtime.parent_pid,
|
|
80
80
|
"cwd": runtime.cwd, "host": runtime.host, "invoker": input.invoker, "branch": input.branch,
|
|
81
|
-
"
|
|
81
|
+
"worktree": input.worktree, "outcome": input.outcome,
|
|
82
82
|
"elapsed_ms": input.elapsed_ms, "correlation_id": input.correlation_id,
|
|
83
83
|
"truncated": false, "extra": extra_value,
|
|
84
84
|
})
|
|
@@ -128,7 +128,11 @@ pub fn parse_record(payload: &Value) -> Option<AuditRecord> {
|
|
|
128
128
|
let kind = obj.get("kind")?.as_str()?.to_string();
|
|
129
129
|
let invoker = obj.get("invoker")?.as_str()?.to_string();
|
|
130
130
|
let branch = obj.get("branch")?.as_str()?.to_string();
|
|
131
|
-
let
|
|
131
|
+
let worktree = obj
|
|
132
|
+
.get("worktree")
|
|
133
|
+
.or_else(|| obj.get("worktree_path"))?
|
|
134
|
+
.as_str()?
|
|
135
|
+
.to_string();
|
|
132
136
|
let outcome = obj.get("outcome")?.as_str()?.to_string();
|
|
133
137
|
let pid = obj.get("pid")?.as_i64()?;
|
|
134
138
|
let schema_version = obj
|
|
@@ -172,7 +176,7 @@ pub fn parse_record(payload: &Value) -> Option<AuditRecord> {
|
|
|
172
176
|
host,
|
|
173
177
|
invoker,
|
|
174
178
|
branch,
|
|
175
|
-
|
|
179
|
+
worktree,
|
|
176
180
|
outcome,
|
|
177
181
|
elapsed_ms,
|
|
178
182
|
correlation_id,
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
//! Worktree binding records and validation primitives (product vocabulary).
|
|
2
2
|
use super::paths::normalize_path;
|
|
3
|
-
use serde::{Deserialize, Serialize};
|
|
4
3
|
use serde_json::Value;
|
|
5
4
|
|
|
6
5
|
pub const CURRENT_SCHEMA_VERSION: i64 = 2;
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
/// A session ↔ worktree record.
|
|
8
|
+
///
|
|
9
|
+
/// `schema_version` is the version of the payload this value was *read
|
|
10
|
+
/// from* (`from_dict` accepts v1 keys). It is informational: the struct is
|
|
11
|
+
/// always held in v2 vocabulary and [`WorktreeBinding::to_dict`] always
|
|
12
|
+
/// writes [`CURRENT_SCHEMA_VERSION`], so persisting a v1 record upgrades it
|
|
13
|
+
/// on disk. Do not write `self.schema_version` back out.
|
|
14
|
+
#[derive(Debug, Clone)]
|
|
9
15
|
pub struct WorktreeBinding {
|
|
10
16
|
pub session_id: String,
|
|
11
17
|
pub worktree: String,
|
|
@@ -14,6 +20,7 @@ pub struct WorktreeBinding {
|
|
|
14
20
|
pub git_common_dir: String,
|
|
15
21
|
pub main_worktree: String,
|
|
16
22
|
pub commit_ish: String,
|
|
23
|
+
/// Schema version of the source payload (see the type docs).
|
|
17
24
|
pub schema_version: i64,
|
|
18
25
|
}
|
|
19
26
|
|
|
@@ -75,6 +82,8 @@ impl WorktreeBinding {
|
|
|
75
82
|
}))
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
/// Serialize in the current (v2) vocabulary; always writes
|
|
86
|
+
/// [`CURRENT_SCHEMA_VERSION`], whatever `self.schema_version` says.
|
|
78
87
|
pub fn to_dict(&self) -> Value {
|
|
79
88
|
serde_json::json!({
|
|
80
89
|
"session_id": self.session_id,
|
{worktree_runtime-0.1.0a4 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain/paths.rs
RENAMED
|
@@ -100,23 +100,18 @@ pub fn home_dir(home: Option<&str>) -> PathBuf {
|
|
|
100
100
|
if let Some(h) = home {
|
|
101
101
|
return expanduser(h);
|
|
102
102
|
}
|
|
103
|
-
if cfg!(windows) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
let keys: &[&str] = if cfg!(windows) {
|
|
104
|
+
&["USERPROFILE", "HOME"]
|
|
105
|
+
} else {
|
|
106
|
+
&["HOME", "USERPROFILE"]
|
|
107
|
+
};
|
|
108
|
+
for key in keys {
|
|
109
|
+
if let Ok(value) = env::var(key) {
|
|
110
|
+
if !value.is_empty() {
|
|
111
|
+
return expanduser(&value);
|
|
107
112
|
}
|
|
108
113
|
}
|
|
109
114
|
}
|
|
110
|
-
if let Ok(home_env) = env::var("HOME") {
|
|
111
|
-
if !home_env.is_empty() {
|
|
112
|
-
return expanduser(&home_env);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
if let Ok(userprofile) = env::var("USERPROFILE") {
|
|
116
|
-
if !userprofile.is_empty() {
|
|
117
|
-
return expanduser(&userprofile);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
115
|
PathBuf::from(".")
|
|
121
116
|
}
|
|
122
117
|
|
|
@@ -6,7 +6,8 @@ use crate::infrastructure::env_keys::{
|
|
|
6
6
|
use crate::infrastructure::project_id_resolver::{resolve_project_id, resolve_state_dir_name};
|
|
7
7
|
use std::path::PathBuf;
|
|
8
8
|
|
|
9
|
-
pub fn resolve_adaptive_task_shared_audit_dir(
|
|
9
|
+
pub fn resolve_adaptive_task_shared_audit_dir(
|
|
10
|
+
) -> Result<PathBuf, crate::infrastructure::project_id_resolver::ProjectResolveError> {
|
|
10
11
|
let shared_env = std::env::var(ADAPTIVE_TASK_SHARED_DIR_ENV)
|
|
11
12
|
.ok()
|
|
12
13
|
.map(|v| v.trim().to_string())
|
|
@@ -20,7 +21,7 @@ pub fn resolve_adaptive_task_shared_audit_dir() -> PathBuf {
|
|
|
20
21
|
.unwrap_or_default();
|
|
21
22
|
if !shared_env.is_empty() {
|
|
22
23
|
let base = expanduser(&shared_env);
|
|
23
|
-
return base.join("audit");
|
|
24
|
+
return Ok(base.join("audit"));
|
|
24
25
|
}
|
|
25
26
|
let project_root = std::env::var(NOVELER_PROJECT_ROOT_ENV)
|
|
26
27
|
.unwrap_or_default()
|
|
@@ -32,7 +33,7 @@ pub fn resolve_adaptive_task_shared_audit_dir() -> PathBuf {
|
|
|
32
33
|
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
|
33
34
|
};
|
|
34
35
|
let root = std::fs::canonicalize(&base).unwrap_or(base);
|
|
35
|
-
let
|
|
36
|
-
|
|
37
|
-
root.join(state_dir_name).join("shared").join("audit")
|
|
36
|
+
let project_id = resolve_project_id(Some(&root.to_string_lossy()))?;
|
|
37
|
+
let state_dir_name = resolve_state_dir_name(Some(&project_id))?;
|
|
38
|
+
Ok(root.join(state_dir_name).join("shared").join("audit"))
|
|
38
39
|
}
|
|
@@ -11,8 +11,10 @@ pub fn read_records(
|
|
|
11
11
|
since: Option<chrono::DateTime<chrono::Utc>>,
|
|
12
12
|
until: Option<chrono::DateTime<chrono::Utc>>,
|
|
13
13
|
) -> Vec<AuditRecord> {
|
|
14
|
-
let audit_dir = resolve_adaptive_task_shared_audit_dir();
|
|
15
14
|
let mut out: Vec<AuditRecord> = Vec::new();
|
|
15
|
+
let Ok(audit_dir) = resolve_adaptive_task_shared_audit_dir() else {
|
|
16
|
+
return out;
|
|
17
|
+
};
|
|
16
18
|
// Rotated archives first (oldest first), then the current file, so the
|
|
17
19
|
// history is complete even after rotation.
|
|
18
20
|
let mut files = list_archives(&audit_dir);
|
|
@@ -57,7 +59,7 @@ fn read_file_into(
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
if let Some(wp) = worktree_path {
|
|
60
|
-
if record.
|
|
62
|
+
if record.worktree != wp {
|
|
61
63
|
continue;
|
|
62
64
|
}
|
|
63
65
|
}
|
|
@@ -40,7 +40,15 @@ pub fn suspicious_extra_message(keys: &[&str]) -> String {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
pub fn record_event(input: AuditEventInput) -> Result<(), AuditWriteError> {
|
|
43
|
-
let audit_dir = resolve_adaptive_task_shared_audit_dir()
|
|
43
|
+
let audit_dir = match resolve_adaptive_task_shared_audit_dir() {
|
|
44
|
+
Ok(dir) => dir,
|
|
45
|
+
Err(err) => {
|
|
46
|
+
return Err(AuditWriteError {
|
|
47
|
+
stage: "project_id",
|
|
48
|
+
message: err.to_string(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
44
52
|
if let Err(exc) = std::fs::create_dir_all(&audit_dir) {
|
|
45
53
|
return Err(fail("mkdir", exc.to_string()));
|
|
46
54
|
}
|
|
@@ -99,7 +107,9 @@ pub fn record_event(input: AuditEventInput) -> Result<(), AuditWriteError> {
|
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
pub fn reap_rotation_sentinel() {
|
|
102
|
-
let audit_dir = resolve_adaptive_task_shared_audit_dir()
|
|
110
|
+
let Ok(audit_dir) = resolve_adaptive_task_shared_audit_dir() else {
|
|
111
|
+
return;
|
|
112
|
+
};
|
|
103
113
|
reap_stale_sentinel(&audit_dir.join(AUDIT_FILENAME));
|
|
104
114
|
}
|
|
105
115
|
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
use crate::domain::binding::{WorktreeBinding, CURRENT_SCHEMA_VERSION};
|
|
8
8
|
use crate::domain::paths::normalize_path;
|
|
9
9
|
use crate::infrastructure::git_adapter::git_output;
|
|
10
|
-
use crate::infrastructure::project_id_resolver::{
|
|
10
|
+
use crate::infrastructure::project_id_resolver::{
|
|
11
|
+
resolve_project_id, store_relative_path, ProjectResolveError,
|
|
12
|
+
};
|
|
11
13
|
use fs2::FileExt;
|
|
12
14
|
use serde_json::{json, Value};
|
|
13
15
|
use std::fmt;
|
|
@@ -27,6 +29,8 @@ pub enum BindingStoreError {
|
|
|
27
29
|
path: PathBuf,
|
|
28
30
|
source: std::io::Error,
|
|
29
31
|
},
|
|
32
|
+
/// The store path could not be built because project id resolution failed.
|
|
33
|
+
Project(ProjectResolveError),
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
impl BindingStoreError {
|
|
@@ -34,6 +38,10 @@ impl BindingStoreError {
|
|
|
34
38
|
match self {
|
|
35
39
|
BindingStoreError::Corrupt { .. } => "WORKTREE_BINDING_STORE_CORRUPT",
|
|
36
40
|
BindingStoreError::Io { .. } => "WORKTREE_BINDING_STORE_IO",
|
|
41
|
+
BindingStoreError::Project(ProjectResolveError::Invalid { .. }) => "PROJECT_ID_INVALID",
|
|
42
|
+
BindingStoreError::Project(ProjectResolveError::Unreadable { .. }) => {
|
|
43
|
+
"WORKTREE_PROJECT_CONFIG_UNREADABLE"
|
|
44
|
+
}
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
}
|
|
@@ -55,12 +63,19 @@ impl fmt::Display for BindingStoreError {
|
|
|
55
63
|
path.display(),
|
|
56
64
|
source
|
|
57
65
|
),
|
|
66
|
+
BindingStoreError::Project(err) => write!(f, "{err}"),
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
impl std::error::Error for BindingStoreError {}
|
|
63
72
|
|
|
73
|
+
impl From<ProjectResolveError> for BindingStoreError {
|
|
74
|
+
fn from(err: ProjectResolveError) -> Self {
|
|
75
|
+
BindingStoreError::Project(err)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
64
79
|
pub fn resolve_store_root(candidate: &Path) -> PathBuf {
|
|
65
80
|
let git_common_dir = match git_output(
|
|
66
81
|
candidate,
|
|
@@ -109,15 +124,15 @@ impl Drop for StoreLock {
|
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
impl WorktreeBindingStore {
|
|
112
|
-
pub fn new(main_worktree: impl AsRef<Path>) -> Self {
|
|
127
|
+
pub fn new(main_worktree: impl AsRef<Path>) -> Result<Self, BindingStoreError> {
|
|
113
128
|
let normalized = normalize_path(&main_worktree.as_ref().to_string_lossy());
|
|
114
129
|
let resolved = resolve_store_root(Path::new(&normalized));
|
|
115
|
-
let project_id = resolve_project_id(Some(&resolved.to_string_lossy()))
|
|
116
|
-
let path = resolved.join(store_relative_path(Some(&project_id)));
|
|
117
|
-
Self {
|
|
130
|
+
let project_id = resolve_project_id(Some(&resolved.to_string_lossy()))?;
|
|
131
|
+
let path = resolved.join(store_relative_path(Some(&project_id))?);
|
|
132
|
+
Ok(Self {
|
|
118
133
|
main_worktree: resolved,
|
|
119
134
|
path,
|
|
120
|
-
}
|
|
135
|
+
})
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
pub fn lock_path(&self) -> PathBuf {
|
|
@@ -57,15 +57,6 @@ pub enum GitInvokeError {
|
|
|
57
57
|
Exit { code: i32, stderr: String },
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
impl GitInvokeError {
|
|
61
|
-
/// `true` for failures of the git installation itself rather than of the
|
|
62
|
-
/// repository state (`Exit`). Callers use this to separate
|
|
63
|
-
/// "git is unavailable" from "not a repository / detached HEAD".
|
|
64
|
-
pub fn is_infrastructure(&self) -> bool {
|
|
65
|
-
!matches!(self, GitInvokeError::Exit { .. })
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
60
|
impl fmt::Display for GitInvokeError {
|
|
70
61
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
71
62
|
match self {
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
//! Project identifier resolution from pyproject.toml.
|
|
2
|
+
use crate::domain::paths::{normalize_path, validate_project_id, LEGACY_PROJECT_ID};
|
|
3
|
+
use crate::infrastructure::env_keys::NOVELER_REPO_ROOT_ENV;
|
|
4
|
+
use crate::infrastructure::git_adapter::git_repo_root;
|
|
5
|
+
use std::collections::HashMap;
|
|
6
|
+
use std::fmt;
|
|
7
|
+
use std::io::ErrorKind;
|
|
8
|
+
use std::path::{Path, PathBuf};
|
|
9
|
+
use std::sync::Mutex;
|
|
10
|
+
|
|
11
|
+
pub const PROJECT_CONFIG_UNREADABLE: &str = "WORKTREE_PROJECT_CONFIG_UNREADABLE";
|
|
12
|
+
|
|
13
|
+
/// A declared project id could not be used. Missing files and a missing
|
|
14
|
+
/// `project_id` key are not errors; they stay [`LEGACY_PROJECT_ID`].
|
|
15
|
+
#[derive(Debug, Clone)]
|
|
16
|
+
pub enum ProjectResolveError {
|
|
17
|
+
Invalid {
|
|
18
|
+
message: String,
|
|
19
|
+
reason_code: String,
|
|
20
|
+
},
|
|
21
|
+
Unreadable {
|
|
22
|
+
message: String,
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl ProjectResolveError {
|
|
27
|
+
pub fn reason_code(&self) -> &str {
|
|
28
|
+
match self {
|
|
29
|
+
ProjectResolveError::Invalid { reason_code, .. } => reason_code,
|
|
30
|
+
ProjectResolveError::Unreadable { .. } => PROJECT_CONFIG_UNREADABLE,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub fn message(&self) -> &str {
|
|
35
|
+
match self {
|
|
36
|
+
ProjectResolveError::Invalid { message, .. }
|
|
37
|
+
| ProjectResolveError::Unreadable { message } => message,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl fmt::Display for ProjectResolveError {
|
|
43
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
44
|
+
write!(f, "{}: {}", self.reason_code(), self.message())
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl std::error::Error for ProjectResolveError {}
|
|
49
|
+
|
|
50
|
+
pub(crate) const PROJECT_ID_TABLE_KEY: &str = "adaptive-task";
|
|
51
|
+
const PROJECT_ID_FIELD: &str = "project_id";
|
|
52
|
+
|
|
53
|
+
fn read_pyproject_project_id(normalized_root: &str) -> Result<String, ProjectResolveError> {
|
|
54
|
+
if normalized_root.is_empty() {
|
|
55
|
+
return Ok(LEGACY_PROJECT_ID.to_string());
|
|
56
|
+
}
|
|
57
|
+
let pyproject = Path::new(normalized_root).join("pyproject.toml");
|
|
58
|
+
let content = match std::fs::read_to_string(&pyproject) {
|
|
59
|
+
Ok(c) => c,
|
|
60
|
+
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(LEGACY_PROJECT_ID.to_string()),
|
|
61
|
+
Err(err) => {
|
|
62
|
+
return Err(ProjectResolveError::Unreadable {
|
|
63
|
+
message: format!(
|
|
64
|
+
"pyproject.toml at {} could not be read: {err}",
|
|
65
|
+
pyproject.display()
|
|
66
|
+
),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
let data: toml::Value = match content.parse() {
|
|
71
|
+
Ok(d) => d,
|
|
72
|
+
Err(err) => {
|
|
73
|
+
return Err(ProjectResolveError::Unreadable {
|
|
74
|
+
message: format!(
|
|
75
|
+
"pyproject.toml at {} could not be parsed: {err}",
|
|
76
|
+
pyproject.display()
|
|
77
|
+
),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
let Some(project_id) = data
|
|
82
|
+
.get("tool")
|
|
83
|
+
.and_then(|t| t.get(PROJECT_ID_TABLE_KEY))
|
|
84
|
+
.and_then(|a| a.get(PROJECT_ID_FIELD))
|
|
85
|
+
.and_then(|v| v.as_str())
|
|
86
|
+
else {
|
|
87
|
+
return Ok(LEGACY_PROJECT_ID.to_string());
|
|
88
|
+
};
|
|
89
|
+
if project_id.is_empty() {
|
|
90
|
+
return Ok(LEGACY_PROJECT_ID.to_string());
|
|
91
|
+
}
|
|
92
|
+
validate_project_id(project_id).map_err(|(message, reason_code)| ProjectResolveError::Invalid {
|
|
93
|
+
message,
|
|
94
|
+
reason_code,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static PROJECT_ID_CACHE: Mutex<Option<HashMap<String, String>>> = Mutex::new(None);
|
|
99
|
+
|
|
100
|
+
fn cached_project_id(normalized_root: &str) -> Result<String, ProjectResolveError> {
|
|
101
|
+
let mut cache = PROJECT_ID_CACHE.lock().unwrap();
|
|
102
|
+
let cache = cache.get_or_insert_with(HashMap::new);
|
|
103
|
+
if let Some(val) = cache.get(normalized_root) {
|
|
104
|
+
return Ok(val.clone());
|
|
105
|
+
}
|
|
106
|
+
let val = read_pyproject_project_id(normalized_root)?;
|
|
107
|
+
cache.insert(normalized_root.to_string(), val.clone());
|
|
108
|
+
Ok(val)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
pub fn clear_cache() {
|
|
112
|
+
let mut cache = PROJECT_ID_CACHE.lock().unwrap();
|
|
113
|
+
*cache = None;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pub(crate) fn resolve_root(repo_root: Option<&str>) -> String {
|
|
117
|
+
let root = repo_root
|
|
118
|
+
.filter(|s| !s.is_empty())
|
|
119
|
+
.map(|s| s.to_string())
|
|
120
|
+
.unwrap_or_else(|| std::env::var(NOVELER_REPO_ROOT_ENV).unwrap_or_default());
|
|
121
|
+
if root.is_empty() {
|
|
122
|
+
git_repo_root(None)
|
|
123
|
+
} else {
|
|
124
|
+
root
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
pub fn resolve_project_id(repo_root: Option<&str>) -> Result<String, ProjectResolveError> {
|
|
129
|
+
let root = resolve_root(repo_root);
|
|
130
|
+
if root.is_empty() {
|
|
131
|
+
return Ok(LEGACY_PROJECT_ID.to_string());
|
|
132
|
+
}
|
|
133
|
+
cached_project_id(&root)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Directory name for a project's state root. An id that fails
|
|
137
|
+
/// `^[a-z][a-z0-9_-]{0,63}$` is [`ProjectResolveError::Invalid`] and is never
|
|
138
|
+
/// turned into a path. An empty id stays `.{LEGACY_PROJECT_ID}`.
|
|
139
|
+
pub fn resolve_state_dir_name(project_id: Option<&str>) -> Result<String, ProjectResolveError> {
|
|
140
|
+
let raw = match project_id {
|
|
141
|
+
Some(pid) if !pid.is_empty() => pid.to_string(),
|
|
142
|
+
Some(_) => return Ok(format!(".{LEGACY_PROJECT_ID}")),
|
|
143
|
+
None => resolve_project_id(None)?,
|
|
144
|
+
};
|
|
145
|
+
let pid = validate_project_id(&raw).map_err(|(message, reason_code)| {
|
|
146
|
+
ProjectResolveError::Invalid {
|
|
147
|
+
message,
|
|
148
|
+
reason_code,
|
|
149
|
+
}
|
|
150
|
+
})?;
|
|
151
|
+
Ok(format!(".{pid}"))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
pub fn store_relative_path(project_id: Option<&str>) -> Result<PathBuf, ProjectResolveError> {
|
|
155
|
+
Ok(PathBuf::from(resolve_state_dir_name(project_id)?)
|
|
156
|
+
.join("state")
|
|
157
|
+
.join("worktree-runtime")
|
|
158
|
+
.join("bindings.json"))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pub fn state_dir(repo_root: Option<&str>) -> Result<String, ProjectResolveError> {
|
|
162
|
+
let root = resolve_root(repo_root);
|
|
163
|
+
if root.is_empty() {
|
|
164
|
+
return Ok(String::new());
|
|
165
|
+
}
|
|
166
|
+
let normalized_root = normalize_path(&root);
|
|
167
|
+
let project_id = resolve_project_id(Some(&normalized_root))?;
|
|
168
|
+
let state_name = resolve_state_dir_name(Some(&project_id))?;
|
|
169
|
+
Ok(PathBuf::from(&normalized_root)
|
|
170
|
+
.join(&state_name)
|
|
171
|
+
.to_string_lossy()
|
|
172
|
+
.to_string())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pub use crate::infrastructure::github_policy_loader::{
|
|
176
|
+
resolve_github_policy, resolve_main_branches,
|
|
177
|
+
};
|