campfire-cli 0.1.0__py3-none-any.whl
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.
- campfire_cli/__init__.py +3 -0
- campfire_cli/alembic/__init__.py +1 -0
- campfire_cli/alembic/env.py +37 -0
- campfire_cli/alembic/script.py.mako +22 -0
- campfire_cli/alembic/versions/001_initial_schema.py +81 -0
- campfire_cli/alembic/versions/002_workspace_project_registry.py +47 -0
- campfire_cli/alembic/versions/003_unified_workspace_state.py +113 -0
- campfire_cli/alembic/versions/004_restructure_workspace_domain.py +23 -0
- campfire_cli/alembic/versions/005_decision_channel.py +71 -0
- campfire_cli/alembic/versions/006_workspace_topology_index.py +61 -0
- campfire_cli/alembic/versions/007_workspace_adoption.py +38 -0
- campfire_cli/alembic/versions/__init__.py +1 -0
- campfire_cli/app/SPEC.md +12 -0
- campfire_cli/app/__init__.py +1 -0
- campfire_cli/app/base/SPEC.md +3 -0
- campfire_cli/app/base/__init__.py +1 -0
- campfire_cli/app/base/cli/base_cli.py +37 -0
- campfire_cli/app/base/repository/base_repository.py +17 -0
- campfire_cli/app/base/schema/base_schema.py +18 -0
- campfire_cli/app/base/service/base_service.py +129 -0
- campfire_cli/app/decision/SPEC.md +16 -0
- campfire_cli/app/decision/__init__.py +1 -0
- campfire_cli/app/decision/cli/decision_cli.py +112 -0
- campfire_cli/app/decision/repository/decision_repository.py +199 -0
- campfire_cli/app/decision/schema/decision_schema.py +56 -0
- campfire_cli/app/decision/service/decision_projection_service.py +151 -0
- campfire_cli/app/decision/service/decision_service.py +124 -0
- campfire_cli/app/document/SPEC.md +51 -0
- campfire_cli/app/document/__init__.py +1 -0
- campfire_cli/app/document/cli/__init__.py +1 -0
- campfire_cli/app/document/cli/document_cli.py +63 -0
- campfire_cli/app/document/cli/profile_cli.py +65 -0
- campfire_cli/app/document/cli/type_cli.py +49 -0
- campfire_cli/app/document/repository/__init__.py +1 -0
- campfire_cli/app/document/repository/document_profile_repository.py +17 -0
- campfire_cli/app/document/repository/document_type_repository.py +17 -0
- campfire_cli/app/document/service/__init__.py +1 -0
- campfire_cli/app/document/service/document_profile_service.py +66 -0
- campfire_cli/app/document/service/document_rule_service.py +316 -0
- campfire_cli/app/document/service/document_scanner.py +38 -0
- campfire_cli/app/document/service/document_service.py +139 -0
- campfire_cli/app/document/service/document_type_service.py +28 -0
- campfire_cli/app/document/service/frontmatter_apply.py +98 -0
- campfire_cli/app/document/service/frontmatter_formatter.py +104 -0
- campfire_cli/app/document/service/frontmatter_plan.py +75 -0
- campfire_cli/app/document/service/profile_registry.py +166 -0
- campfire_cli/app/document/service/type_apply.py +185 -0
- campfire_cli/app/document/service/type_plan.py +80 -0
- campfire_cli/app/maintenance/SPEC.md +22 -0
- campfire_cli/app/maintenance/__init__.py +1 -0
- campfire_cli/app/maintenance/cli/maintenance_cli.py +103 -0
- campfire_cli/app/maintenance/repository/maintenance_repository.py +115 -0
- campfire_cli/app/maintenance/schema/maintenance_schema.py +109 -0
- campfire_cli/app/maintenance/service/archive_service.py +255 -0
- campfire_cli/app/maintenance/service/link_service.py +91 -0
- campfire_cli/app/maintenance/service/maintenance_protocol.py +25 -0
- campfire_cli/app/maintenance/service/maintenance_service.py +461 -0
- campfire_cli/app/maintenance/service/moc_service.py +310 -0
- campfire_cli/app/maintenance/service/plan_service.py +411 -0
- campfire_cli/app/skill/SPEC.md +3 -0
- campfire_cli/app/skill/__init__.py +1 -0
- campfire_cli/app/skill/cli/skill_cli.py +44 -0
- campfire_cli/app/skill/repository/skill_repository.py +33 -0
- campfire_cli/app/skill/schema/skill_schema.py +18 -0
- campfire_cli/app/skill/service/skill_service.py +146 -0
- campfire_cli/app/workspace/SPEC.md +25 -0
- campfire_cli/app/workspace/__init__.py +1 -0
- campfire_cli/app/workspace/cli/__init__.py +1 -0
- campfire_cli/app/workspace/cli/adoption_cli.py +83 -0
- campfire_cli/app/workspace/cli/config_cli.py +30 -0
- campfire_cli/app/workspace/cli/domain_cli.py +101 -0
- campfire_cli/app/workspace/cli/project_cli.py +135 -0
- campfire_cli/app/workspace/cli/restructure_cli.py +133 -0
- campfire_cli/app/workspace/cli/space_cli.py +65 -0
- campfire_cli/app/workspace/cli/workspace_cli.py +145 -0
- campfire_cli/app/workspace/repository/__init__.py +1 -0
- campfire_cli/app/workspace/repository/adoption_repository.py +55 -0
- campfire_cli/app/workspace/repository/manifest_repository.py +38 -0
- campfire_cli/app/workspace/repository/restructure_repository.py +93 -0
- campfire_cli/app/workspace/repository/workspace_repository.py +125 -0
- campfire_cli/app/workspace/schema/__init__.py +1 -0
- campfire_cli/app/workspace/schema/adoption_schema.py +47 -0
- campfire_cli/app/workspace/schema/restructure_schema.py +64 -0
- campfire_cli/app/workspace/schema/workspace_schema.py +225 -0
- campfire_cli/app/workspace/service/__init__.py +1 -0
- campfire_cli/app/workspace/service/adoption_protocol.py +11 -0
- campfire_cli/app/workspace/service/adoption_service.py +364 -0
- campfire_cli/app/workspace/service/config_service.py +189 -0
- campfire_cli/app/workspace/service/domain_restructure_service.py +316 -0
- campfire_cli/app/workspace/service/project_service.py +389 -0
- campfire_cli/app/workspace/service/restructure_protocol.py +19 -0
- campfire_cli/app/workspace/service/restructure_service.py +392 -0
- campfire_cli/app/workspace/service/restructure_verifier.py +83 -0
- campfire_cli/app/workspace/service/structure_service.py +494 -0
- campfire_cli/app/workspace/service/workspace_protocol.py +24 -0
- campfire_cli/app/workspace/service/workspace_service.py +304 -0
- campfire_cli/common/SPEC.md +12 -0
- campfire_cli/common/__init__.py +1 -0
- campfire_cli/common/database/__init__.py +4 -0
- campfire_cli/common/database/migrations.py +18 -0
- campfire_cli/common/database/models.py +215 -0
- campfire_cli/common/database/session.py +30 -0
- campfire_cli/common/documents/__init__.py +1 -0
- campfire_cli/common/documents/document_types.py +119 -0
- campfire_cli/common/documents/frontmatter_schema.py +42 -0
- campfire_cli/common/documents/markdown.py +66 -0
- campfire_cli/common/exceptions/__init__.py +7 -0
- campfire_cli/common/exceptions/base.py +19 -0
- campfire_cli/common/filesystem/__init__.py +6 -0
- campfire_cli/common/filesystem/atomic.py +30 -0
- campfire_cli/common/filesystem/locking.py +55 -0
- campfire_cli/common/governance/__init__.py +13 -0
- campfire_cli/common/governance/issues.py +38 -0
- campfire_cli/common/governance/locking.py +31 -0
- campfire_cli/common/governance/snapshots.py +26 -0
- campfire_cli/common/hashing.py +10 -0
- campfire_cli/common/reports/__init__.py +1 -0
- campfire_cli/common/reports/json_report.py +8 -0
- campfire_cli/common/reports/markdown_report.py +29 -0
- campfire_cli/config/SPEC.md +5 -0
- campfire_cli/config/defaults.py +64 -0
- campfire_cli/config/settings.py +50 -0
- campfire_cli/container.py +130 -0
- campfire_cli/main.py +133 -0
- campfire_cli/resources/__init__.py +1 -0
- campfire_cli/resources/bases/Agent/346/211/247/350/241/214/345/267/245/344/275/234/345/217/260.base +35 -0
- campfire_cli/resources/bases/SPEC.md +3 -0
- campfire_cli/resources/bases//344/273/273/345/212/241/345/267/245/344/275/234/345/217/260.base +49 -0
- campfire_cli/resources/bases//345/206/263/347/255/226/345/267/245/344/275/234/345/217/260.base +40 -0
- campfire_cli/resources/bases//345/276/205/345/275/222/346/241/243/346/226/207/346/241/243.base +28 -0
- campfire_cli/resources/bases//351/241/271/347/233/256/346/226/207/346/241/243.base +35 -0
- campfire_cli/resources/defaults/__init__.py +1 -0
- campfire_cli/resources/defaults/config.yml +458 -0
- campfire_cli/resources/skills/SPEC.md +3 -0
- campfire_cli/resources/skills/campfire-context-bootstrap/SKILL.md +62 -0
- campfire_cli/resources/skills/campfire-context-bootstrap/agents/openai.yaml +3 -0
- campfire_cli/resources/skills/campfire-conversation-router/SKILL.md +53 -0
- campfire_cli/resources/skills/campfire-conversation-router/agents/openai.yaml +3 -0
- campfire_cli/resources/skills/campfire-conversation-router/references/inbox-handoff.md +19 -0
- campfire_cli/resources/skills/campfire-conversation-router/references/learning-intake.md +31 -0
- campfire_cli/resources/skills/campfire-conversation-router/references/task-intake.md +28 -0
- campfire_cli/resources/skills/campfire-document-capture/SKILL.md +109 -0
- campfire_cli/resources/skills/campfire-inbox-triage/SKILL.md +23 -0
- campfire_cli/resources/skills/campfire-weekly-report-writing/SKILL.md +21 -0
- campfire_cli/resources/skills/campfire-weekly-report-writing/agents/openai.yaml +3 -0
- campfire_cli/resources/skills/campfire-workspace-adoption/SKILL.md +42 -0
- campfire_cli/resources/skills/campfire-workspace-maintenance/SKILL.md +74 -0
- campfire_cli/resources/skills/campfire-workspace-maintenance/references//344/273/273/345/212/241/346/255/243/346/226/207/347/273/223/346/236/204.md +23 -0
- campfire_cli/resources/skills/campfire-workspace-restructure/SKILL.md +52 -0
- campfire_cli-0.1.0.dist-info/METADATA +141 -0
- campfire_cli-0.1.0.dist-info/RECORD +154 -0
- campfire_cli-0.1.0.dist-info/WHEEL +4 -0
- campfire_cli-0.1.0.dist-info/entry_points.txt +2 -0
- campfire_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WorkspaceEntry(BaseModel):
|
|
10
|
+
path: str
|
|
11
|
+
status: str = "active"
|
|
12
|
+
default: bool = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WorkspaceRegistry(BaseModel):
|
|
16
|
+
schema_version: int = 1
|
|
17
|
+
default_workspace: str | None = None
|
|
18
|
+
workspaces: dict[str, WorkspaceEntry] = Field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WorkspaceResult(BaseModel):
|
|
22
|
+
status: str
|
|
23
|
+
workspace_id: str
|
|
24
|
+
workspace: str
|
|
25
|
+
state_root: str
|
|
26
|
+
default: bool = False
|
|
27
|
+
created: list[str] = Field(default_factory=list)
|
|
28
|
+
preserved: list[str] = Field(default_factory=list)
|
|
29
|
+
created_directories: list[str] = Field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WorkspaceResolution(BaseModel):
|
|
33
|
+
status: str = "ok"
|
|
34
|
+
workspace_id: str
|
|
35
|
+
workspace: str
|
|
36
|
+
state_root: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class WorkspaceListResult(BaseModel):
|
|
40
|
+
status: str = "ok"
|
|
41
|
+
schema_version: int = 1
|
|
42
|
+
default_workspace: str | None = None
|
|
43
|
+
workspaces: dict[str, WorkspaceEntry] = Field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class WorkspaceDefaultResult(BaseModel):
|
|
47
|
+
status: str = "updated"
|
|
48
|
+
default_workspace: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class WorkspaceCreateRequest(BaseModel):
|
|
52
|
+
workspace_id: str
|
|
53
|
+
path: Path
|
|
54
|
+
make_default: bool = False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ManifestWorkspace(BaseModel):
|
|
58
|
+
id: str
|
|
59
|
+
name: str
|
|
60
|
+
governance_version: int = 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ManifestProject(BaseModel):
|
|
64
|
+
id: str
|
|
65
|
+
name: str
|
|
66
|
+
document_domain: str
|
|
67
|
+
git_remote_url: str | None = None
|
|
68
|
+
default_branch: str | None = None
|
|
69
|
+
status: str = "active"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class WorkspaceManifest(BaseModel):
|
|
73
|
+
schema_version: int = 1
|
|
74
|
+
workspace: ManifestWorkspace
|
|
75
|
+
projects: list[ManifestProject] = Field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class WorkspaceSetupResult(WorkspaceResult):
|
|
79
|
+
manifest: str
|
|
80
|
+
manifest_operation: str
|
|
81
|
+
imported_projects: list[str] = Field(default_factory=list)
|
|
82
|
+
unbound_projects: list[str] = Field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Space(BaseModel):
|
|
86
|
+
id: str
|
|
87
|
+
name: str
|
|
88
|
+
path: str
|
|
89
|
+
type: str
|
|
90
|
+
status: str = "active"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class SpaceListResult(BaseModel):
|
|
94
|
+
status: str = "ok"
|
|
95
|
+
spaces: list[Space] = Field(default_factory=list)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SpaceCheckResult(BaseModel):
|
|
99
|
+
status: str
|
|
100
|
+
spaces: list[Space] = Field(default_factory=list)
|
|
101
|
+
issues: list[dict[str, Any]] = Field(default_factory=list)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class SpaceCreateResult(BaseModel):
|
|
105
|
+
status: str
|
|
106
|
+
space: Space
|
|
107
|
+
operations: list[dict[str, str]] = Field(default_factory=list)
|
|
108
|
+
write_performed: bool = False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Domain(BaseModel):
|
|
112
|
+
id: str
|
|
113
|
+
name: str
|
|
114
|
+
path: Path
|
|
115
|
+
space_id: str
|
|
116
|
+
type: str
|
|
117
|
+
governance: str
|
|
118
|
+
moc: str
|
|
119
|
+
parent_domain: str | None = None
|
|
120
|
+
project_id: str | None = None
|
|
121
|
+
status: str = "active"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class DomainListResult(BaseModel):
|
|
125
|
+
status: str = "ok"
|
|
126
|
+
domains: list[Domain] = Field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class DomainCheckResult(BaseModel):
|
|
130
|
+
status: str
|
|
131
|
+
domains: list[Domain] = Field(default_factory=list)
|
|
132
|
+
issues: list[dict[str, Any]] = Field(default_factory=list)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class DomainCreateResult(BaseModel):
|
|
136
|
+
status: str
|
|
137
|
+
domain: Domain
|
|
138
|
+
operations: list[dict[str, str]] = Field(default_factory=list)
|
|
139
|
+
write_performed: bool = False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class WorkspaceConfigCheckResult(BaseModel):
|
|
143
|
+
status: str
|
|
144
|
+
checked: list[str] = Field(default_factory=list)
|
|
145
|
+
issues: list[dict[str, Any]] = Field(default_factory=list)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ProjectEntry(BaseModel):
|
|
149
|
+
id: str
|
|
150
|
+
workspace_id: str
|
|
151
|
+
name: str
|
|
152
|
+
document_domain: str
|
|
153
|
+
git_remote_url: str | None = None
|
|
154
|
+
local_path: str | None = None
|
|
155
|
+
default_branch: str | None = None
|
|
156
|
+
status: str = "active"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class ProjectUpsertRequest(BaseModel):
|
|
160
|
+
project_id: str
|
|
161
|
+
workspace_id: str
|
|
162
|
+
name: str
|
|
163
|
+
document_domain: str
|
|
164
|
+
git_remote_url: str | None = None
|
|
165
|
+
local_path: Path | None = None
|
|
166
|
+
default_branch: str | None = None
|
|
167
|
+
status: str = "active"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ProjectResult(ProjectEntry):
|
|
171
|
+
operation: str = "saved"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class ProjectBindResult(BaseModel):
|
|
175
|
+
status: str = "bound"
|
|
176
|
+
project: ProjectEntry
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ProjectListResult(BaseModel):
|
|
180
|
+
status: str = "ok"
|
|
181
|
+
projects: list[ProjectEntry] = Field(default_factory=list)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class ProjectMatch(BaseModel):
|
|
185
|
+
project: ProjectEntry
|
|
186
|
+
match_basis: list[str] = Field(default_factory=list)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ProjectResolutionResult(BaseModel):
|
|
190
|
+
status: str
|
|
191
|
+
query_path: str
|
|
192
|
+
git_root: str | None = None
|
|
193
|
+
git_remote_url: str | None = None
|
|
194
|
+
matches: list[ProjectMatch] = Field(default_factory=list)
|
|
195
|
+
remote_matches: list[ProjectMatch] = Field(default_factory=list)
|
|
196
|
+
hint: str | None = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ProjectCheckResult(BaseModel):
|
|
200
|
+
status: str
|
|
201
|
+
project: ProjectEntry
|
|
202
|
+
observed: dict[str, Any] = Field(default_factory=dict)
|
|
203
|
+
issues: list[dict[str, Any]] = Field(default_factory=list)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ProjectCreateResult(BaseModel):
|
|
207
|
+
status: str
|
|
208
|
+
project: ProjectEntry
|
|
209
|
+
document_domain_path: str
|
|
210
|
+
operations: list[dict[str, str]] = Field(default_factory=list)
|
|
211
|
+
write_performed: bool = False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class RegistryExport(BaseModel):
|
|
215
|
+
schema_version: int = 1
|
|
216
|
+
default_workspace: str | None = None
|
|
217
|
+
workspaces: dict[str, WorkspaceEntry] = Field(default_factory=dict)
|
|
218
|
+
projects: list[ProjectEntry] = Field(default_factory=list)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class RegistryTransferResult(BaseModel):
|
|
222
|
+
status: str
|
|
223
|
+
path: str
|
|
224
|
+
workspace_count: int
|
|
225
|
+
project_count: int
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Workspace application services and protocols."""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from campfire_cli.app.workspace.schema.adoption_schema import AdoptionBatchState
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AdoptionRepositoryProtocol(Protocol):
|
|
9
|
+
def save(self, state: AdoptionBatchState) -> None: ...
|
|
10
|
+
|
|
11
|
+
def load(self, batch: str) -> AdoptionBatchState: ...
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from campfire_cli.app.maintenance.service.maintenance_service import MaintenanceService
|
|
7
|
+
from campfire_cli.app.workspace.repository.manifest_repository import (
|
|
8
|
+
WorkspaceManifestRepository,
|
|
9
|
+
)
|
|
10
|
+
from campfire_cli.app.workspace.schema.adoption_schema import (
|
|
11
|
+
AdoptionBatchState,
|
|
12
|
+
AdoptionInventoryItem,
|
|
13
|
+
AdoptionPlan,
|
|
14
|
+
AdoptionResult,
|
|
15
|
+
)
|
|
16
|
+
from campfire_cli.app.workspace.schema.workspace_schema import Domain, ManifestProject
|
|
17
|
+
from campfire_cli.app.workspace.service.adoption_protocol import AdoptionRepositoryProtocol
|
|
18
|
+
from campfire_cli.app.workspace.service.structure_service import (
|
|
19
|
+
DOMAIN_MARKER,
|
|
20
|
+
ID_RE,
|
|
21
|
+
DomainService,
|
|
22
|
+
reserved_directories,
|
|
23
|
+
)
|
|
24
|
+
from campfire_cli.app.workspace.service.workspace_protocol import WorkspaceRepositoryProtocol
|
|
25
|
+
from campfire_cli.common.exceptions import ConfigurationError
|
|
26
|
+
from campfire_cli.common.filesystem import atomic_write, safe_path, workspace_write_lock
|
|
27
|
+
from campfire_cli.common.hashing import file_sha256
|
|
28
|
+
from campfire_cli.config.settings import WorkspaceSettings
|
|
29
|
+
|
|
30
|
+
IGNORED_NAMES = {".git", ".obsidian", ".DS_Store", "__pycache__"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AdoptionService:
|
|
34
|
+
"""Safely stage and adopt one existing folder as a managed Domain."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
settings: WorkspaceSettings,
|
|
39
|
+
repository: AdoptionRepositoryProtocol,
|
|
40
|
+
workspace_repository: WorkspaceRepositoryProtocol,
|
|
41
|
+
maintenance: MaintenanceService,
|
|
42
|
+
manifest_repository: WorkspaceManifestRepository | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
self._settings = settings
|
|
45
|
+
self._repository = repository
|
|
46
|
+
self._workspaces = workspace_repository
|
|
47
|
+
self._maintenance = maintenance
|
|
48
|
+
self._manifests = manifest_repository or WorkspaceManifestRepository()
|
|
49
|
+
self._domains = DomainService(settings.vault_root, settings.state_root)
|
|
50
|
+
|
|
51
|
+
def inventory(self, batch: str, source: Path, confirm: bool = False) -> AdoptionResult:
|
|
52
|
+
root = source.expanduser().resolve()
|
|
53
|
+
if not root.is_dir():
|
|
54
|
+
raise ConfigurationError(f"待接管目录不存在:{root}")
|
|
55
|
+
if root == self._settings.vault_root:
|
|
56
|
+
raise ConfigurationError("不能把整个 Workspace 作为一个接管批次")
|
|
57
|
+
source_kind = "internal" if self._settings.vault_root in root.parents else "external"
|
|
58
|
+
inventory, issues = self._scan(root)
|
|
59
|
+
staging = (
|
|
60
|
+
self._settings.vault_root / "_收件箱" / "待接管" / batch
|
|
61
|
+
if source_kind == "external"
|
|
62
|
+
else None
|
|
63
|
+
)
|
|
64
|
+
state = AdoptionBatchState(
|
|
65
|
+
batch=batch,
|
|
66
|
+
source_path=str(root),
|
|
67
|
+
source_kind=source_kind,
|
|
68
|
+
staging_path=(
|
|
69
|
+
staging.relative_to(self._settings.vault_root).as_posix() if staging else None
|
|
70
|
+
),
|
|
71
|
+
status="blocked" if issues else "inventoried",
|
|
72
|
+
inventory=inventory,
|
|
73
|
+
)
|
|
74
|
+
operations = []
|
|
75
|
+
copied = 0
|
|
76
|
+
if source_kind == "external":
|
|
77
|
+
operations = [
|
|
78
|
+
{
|
|
79
|
+
"action": "copy",
|
|
80
|
+
"source": str(root / item.path),
|
|
81
|
+
"path": f"{state.staging_path}/{item.path}",
|
|
82
|
+
}
|
|
83
|
+
for item in inventory
|
|
84
|
+
]
|
|
85
|
+
if not issues and confirm:
|
|
86
|
+
copy_issues = self._copy_to_staging(root, staging, inventory)
|
|
87
|
+
issues.extend(copy_issues)
|
|
88
|
+
if not issues:
|
|
89
|
+
copied = len(inventory)
|
|
90
|
+
state.status = "staged"
|
|
91
|
+
self._repository.save(state)
|
|
92
|
+
return AdoptionResult(
|
|
93
|
+
status=(
|
|
94
|
+
"blocked"
|
|
95
|
+
if issues
|
|
96
|
+
else "staged"
|
|
97
|
+
if source_kind == "external" and confirm
|
|
98
|
+
else "inventoried"
|
|
99
|
+
),
|
|
100
|
+
batch=batch,
|
|
101
|
+
source=str(root),
|
|
102
|
+
source_kind=source_kind,
|
|
103
|
+
staging_path=state.staging_path,
|
|
104
|
+
item_count=len(inventory),
|
|
105
|
+
copied_count=copied,
|
|
106
|
+
operations=operations,
|
|
107
|
+
issues=issues,
|
|
108
|
+
write_performed=bool(copied),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def plan(
|
|
112
|
+
self,
|
|
113
|
+
batch: str,
|
|
114
|
+
*,
|
|
115
|
+
target_path: str,
|
|
116
|
+
domain_id: str,
|
|
117
|
+
name: str,
|
|
118
|
+
space_id: str,
|
|
119
|
+
domain_type: str,
|
|
120
|
+
governance: str,
|
|
121
|
+
parent_domain: str | None = None,
|
|
122
|
+
project_id: str | None = None,
|
|
123
|
+
) -> AdoptionResult:
|
|
124
|
+
state = self._repository.load(batch)
|
|
125
|
+
if state.source_kind == "external" and state.status != "staged":
|
|
126
|
+
return self._blocked(state, "adoption-source-not-staged", state.source_path)
|
|
127
|
+
if not ID_RE.fullmatch(domain_id):
|
|
128
|
+
raise ConfigurationError("Domain id 只能使用小写字母、数字和连字符")
|
|
129
|
+
if any(item.id == domain_id for item in self._domains.discover()[0]):
|
|
130
|
+
raise ConfigurationError(f"Domain id 已存在:{domain_id}")
|
|
131
|
+
target = self._validate_target(target_path, space_id)
|
|
132
|
+
source = self._active_source(state)
|
|
133
|
+
if target != source and target.exists():
|
|
134
|
+
raise ConfigurationError(f"目标路径已存在:{target_path}")
|
|
135
|
+
if (source / DOMAIN_MARKER).exists():
|
|
136
|
+
raise ConfigurationError("来源目录已经是受管 Domain")
|
|
137
|
+
if governance == "project-docs" and not project_id:
|
|
138
|
+
raise ConfigurationError("project-docs Domain 必须绑定 Project id")
|
|
139
|
+
if project_id and self._workspaces.get_project(project_id) is None:
|
|
140
|
+
raise ConfigurationError(f"Project 未注册:{project_id}")
|
|
141
|
+
if parent_domain:
|
|
142
|
+
parent = self._domains.show(parent_domain)
|
|
143
|
+
parent_path = self._settings.vault_root / parent.path
|
|
144
|
+
if parent_path not in target.parents:
|
|
145
|
+
raise ConfigurationError("目标路径必须位于指定父 Domain 下")
|
|
146
|
+
if parent.governance != governance:
|
|
147
|
+
raise ConfigurationError("子 Domain 必须继承父 Domain governance")
|
|
148
|
+
plan = AdoptionPlan(
|
|
149
|
+
batch=batch,
|
|
150
|
+
target_path=target.relative_to(self._settings.vault_root).as_posix(),
|
|
151
|
+
domain_id=domain_id,
|
|
152
|
+
name=name.strip(),
|
|
153
|
+
space_id=space_id,
|
|
154
|
+
domain_type=domain_type,
|
|
155
|
+
governance=governance,
|
|
156
|
+
parent_domain=parent_domain,
|
|
157
|
+
project_id=project_id,
|
|
158
|
+
)
|
|
159
|
+
if not plan.name:
|
|
160
|
+
raise ConfigurationError("Domain name 不能为空")
|
|
161
|
+
state.plan = plan
|
|
162
|
+
state.status = "planned"
|
|
163
|
+
self._repository.save(state)
|
|
164
|
+
operations = []
|
|
165
|
+
if target != source:
|
|
166
|
+
operations.append(
|
|
167
|
+
{
|
|
168
|
+
"action": "move-directory",
|
|
169
|
+
"source": self._display_path(source),
|
|
170
|
+
"path": plan.target_path,
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
operations.extend(
|
|
174
|
+
[
|
|
175
|
+
{"action": "create-domain-marker", "path": f"{plan.target_path}/_领域.md"},
|
|
176
|
+
{
|
|
177
|
+
"action": "create-moc",
|
|
178
|
+
"path": f"{plan.target_path}/_总览/MOC-{plan.name}总览.md",
|
|
179
|
+
},
|
|
180
|
+
]
|
|
181
|
+
)
|
|
182
|
+
return self._result(state, "planned", operations=operations)
|
|
183
|
+
|
|
184
|
+
def apply(self, batch: str, confirm: bool = False) -> AdoptionResult:
|
|
185
|
+
state = self._repository.load(batch)
|
|
186
|
+
if state.plan is None:
|
|
187
|
+
return self._blocked(state, "adoption-plan-missing", batch)
|
|
188
|
+
source = self._active_source(state)
|
|
189
|
+
target = safe_path(self._settings.vault_root, state.plan.target_path)
|
|
190
|
+
issues = self._verify_files(source, state.inventory)
|
|
191
|
+
if target != source and target.exists():
|
|
192
|
+
issues.append({"code": "target-exists", "path": state.plan.target_path})
|
|
193
|
+
moc = target / "_总览" / f"MOC-{state.plan.name}总览.md"
|
|
194
|
+
if target == source and ((target / DOMAIN_MARKER).exists() or moc.exists()):
|
|
195
|
+
issues.append({"code": "adoption-declaration-exists", "path": state.plan.target_path})
|
|
196
|
+
if issues or not confirm:
|
|
197
|
+
result = self._result(state, "blocked" if issues else "ready")
|
|
198
|
+
result.issues = issues
|
|
199
|
+
return result
|
|
200
|
+
with workspace_write_lock(self._settings.state_root):
|
|
201
|
+
locked_issues = self._verify_files(source, state.inventory)
|
|
202
|
+
if locked_issues:
|
|
203
|
+
result = self._result(state, "blocked")
|
|
204
|
+
result.issues = locked_issues
|
|
205
|
+
return result
|
|
206
|
+
if target != source:
|
|
207
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
208
|
+
source.rename(target)
|
|
209
|
+
domain = Domain(
|
|
210
|
+
id=state.plan.domain_id,
|
|
211
|
+
name=state.plan.name,
|
|
212
|
+
path=target,
|
|
213
|
+
space_id=state.plan.space_id,
|
|
214
|
+
type=state.plan.domain_type,
|
|
215
|
+
governance=state.plan.governance,
|
|
216
|
+
moc=f"_总览/MOC-{state.plan.name}总览",
|
|
217
|
+
parent_domain=state.plan.parent_domain,
|
|
218
|
+
project_id=state.plan.project_id,
|
|
219
|
+
)
|
|
220
|
+
atomic_write(target / DOMAIN_MARKER, DomainService.render_marker(domain))
|
|
221
|
+
atomic_write(target / f"{domain.moc}.md", DomainService.render_moc(domain))
|
|
222
|
+
if state.plan.project_id:
|
|
223
|
+
project = self._workspaces.get_project(state.plan.project_id)
|
|
224
|
+
if project:
|
|
225
|
+
self._workspaces.save_project(
|
|
226
|
+
project.model_copy(update={"document_domain": state.plan.target_path})
|
|
227
|
+
)
|
|
228
|
+
self._sync_manifest()
|
|
229
|
+
state.status = "applied"
|
|
230
|
+
self._repository.save(state)
|
|
231
|
+
sync = self._maintenance.sync(scope=state.plan.target_path)
|
|
232
|
+
health = self._maintenance.check(scope=state.plan.target_path, summary=True)
|
|
233
|
+
result = self._result(state, "applied")
|
|
234
|
+
result.write_performed = True
|
|
235
|
+
result.operations = [
|
|
236
|
+
{str(key): str(value) for key, value in operation.items()}
|
|
237
|
+
for operation in sync.operations
|
|
238
|
+
]
|
|
239
|
+
result.issues = [issue.model_dump(mode="json") for issue in health.issues]
|
|
240
|
+
if sync.status == "blocked":
|
|
241
|
+
result.status = "needs-review"
|
|
242
|
+
result.issues.extend(issue.model_dump(mode="json") for issue in sync.issues)
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
def verify(self, batch: str) -> AdoptionResult:
|
|
246
|
+
state = self._repository.load(batch)
|
|
247
|
+
if state.plan is None:
|
|
248
|
+
return self._blocked(state, "adoption-plan-missing", batch)
|
|
249
|
+
target = safe_path(self._settings.vault_root, state.plan.target_path)
|
|
250
|
+
issues = self._verify_files(target, state.inventory)
|
|
251
|
+
if not (target / DOMAIN_MARKER).is_file():
|
|
252
|
+
issues.append({"code": "domain-marker-missing", "path": state.plan.target_path})
|
|
253
|
+
status = "ok" if not issues else "needs-review"
|
|
254
|
+
result = self._result(state, status)
|
|
255
|
+
result.issues = issues
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
def _scan(self, root: Path) -> tuple[list[AdoptionInventoryItem], list[dict[str, str]]]:
|
|
259
|
+
items: list[AdoptionInventoryItem] = []
|
|
260
|
+
issues: list[dict[str, str]] = []
|
|
261
|
+
for path in sorted(root.rglob("*")):
|
|
262
|
+
relative = path.relative_to(root)
|
|
263
|
+
if any(part in IGNORED_NAMES or part.startswith(".") for part in relative.parts):
|
|
264
|
+
continue
|
|
265
|
+
if path.is_symlink():
|
|
266
|
+
issues.append({"code": "adoption-symlink", "path": relative.as_posix()})
|
|
267
|
+
elif path.is_file():
|
|
268
|
+
items.append(
|
|
269
|
+
AdoptionInventoryItem(
|
|
270
|
+
path=relative.as_posix(),
|
|
271
|
+
sha256=file_sha256(path),
|
|
272
|
+
size=path.stat().st_size,
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
return items, issues
|
|
276
|
+
|
|
277
|
+
def _copy_to_staging(
|
|
278
|
+
self, source: Path, staging: Path, items: list[AdoptionInventoryItem]
|
|
279
|
+
) -> list[dict[str, str]]:
|
|
280
|
+
issues: list[dict[str, str]] = []
|
|
281
|
+
for item in items:
|
|
282
|
+
target = staging / item.path
|
|
283
|
+
if target.exists() and (not target.is_file() or file_sha256(target) != item.sha256):
|
|
284
|
+
issues.append({"code": "adoption-staging-conflict", "path": item.path})
|
|
285
|
+
if issues:
|
|
286
|
+
return issues
|
|
287
|
+
for item in items:
|
|
288
|
+
target = staging / item.path
|
|
289
|
+
if target.is_file():
|
|
290
|
+
continue
|
|
291
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
shutil.copy2(source / item.path, target)
|
|
293
|
+
return []
|
|
294
|
+
|
|
295
|
+
def _validate_target(self, value: str, space_id: str) -> Path:
|
|
296
|
+
target = safe_path(self._settings.vault_root, value)
|
|
297
|
+
space = self._domains.spaces.show(space_id)
|
|
298
|
+
space_root = self._settings.vault_root / space.path
|
|
299
|
+
if space_root not in target.parents:
|
|
300
|
+
raise ConfigurationError("目标路径必须位于指定 Space 下")
|
|
301
|
+
if any(
|
|
302
|
+
part in reserved_directories() or part.startswith(".")
|
|
303
|
+
for part in target.relative_to(space_root).parts
|
|
304
|
+
):
|
|
305
|
+
raise ConfigurationError("目标路径不能使用保留目录")
|
|
306
|
+
return target
|
|
307
|
+
|
|
308
|
+
def _active_source(self, state: AdoptionBatchState) -> Path:
|
|
309
|
+
if state.source_kind == "external":
|
|
310
|
+
if not state.staging_path:
|
|
311
|
+
raise ConfigurationError("外部接管批次缺少暂存路径")
|
|
312
|
+
return safe_path(self._settings.vault_root, state.staging_path)
|
|
313
|
+
return Path(state.source_path).resolve()
|
|
314
|
+
|
|
315
|
+
@staticmethod
|
|
316
|
+
def _verify_files(root: Path, inventory: list[AdoptionInventoryItem]) -> list[dict[str, str]]:
|
|
317
|
+
issues: list[dict[str, str]] = []
|
|
318
|
+
for item in inventory:
|
|
319
|
+
path = root / item.path
|
|
320
|
+
if not path.is_file():
|
|
321
|
+
issues.append({"code": "adoption-file-missing", "path": item.path})
|
|
322
|
+
elif file_sha256(path) != item.sha256:
|
|
323
|
+
issues.append({"code": "source-hash-changed", "path": item.path})
|
|
324
|
+
return issues
|
|
325
|
+
|
|
326
|
+
def _sync_manifest(self) -> None:
|
|
327
|
+
manifest = self._manifests.load(self._settings.vault_root)
|
|
328
|
+
if manifest is None:
|
|
329
|
+
raise ConfigurationError("Workspace 缺少 .campfire.yaml")
|
|
330
|
+
manifest.projects = [
|
|
331
|
+
ManifestProject.model_validate(
|
|
332
|
+
project.model_dump(exclude={"workspace_id", "local_path"})
|
|
333
|
+
)
|
|
334
|
+
for project in self._workspaces.list_projects(self._settings.workspace_id)
|
|
335
|
+
]
|
|
336
|
+
self._manifests.save(self._settings.vault_root, manifest)
|
|
337
|
+
|
|
338
|
+
def _result(
|
|
339
|
+
self,
|
|
340
|
+
state: AdoptionBatchState,
|
|
341
|
+
status: str,
|
|
342
|
+
*,
|
|
343
|
+
operations: list[dict[str, str]] | None = None,
|
|
344
|
+
) -> AdoptionResult:
|
|
345
|
+
return AdoptionResult(
|
|
346
|
+
status=status,
|
|
347
|
+
batch=state.batch,
|
|
348
|
+
source=state.source_path,
|
|
349
|
+
source_kind=state.source_kind,
|
|
350
|
+
staging_path=state.staging_path,
|
|
351
|
+
target_path=state.plan.target_path if state.plan else None,
|
|
352
|
+
item_count=len(state.inventory),
|
|
353
|
+
operations=operations or [],
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def _blocked(self, state: AdoptionBatchState, code: str, path: str) -> AdoptionResult:
|
|
357
|
+
result = self._result(state, "blocked")
|
|
358
|
+
result.issues = [{"code": code, "path": path}]
|
|
359
|
+
return result
|
|
360
|
+
|
|
361
|
+
def _display_path(self, path: Path) -> str:
|
|
362
|
+
if self._settings.vault_root in path.parents:
|
|
363
|
+
return path.relative_to(self._settings.vault_root).as_posix()
|
|
364
|
+
return str(path)
|