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,304 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from campfire_cli.app.workspace.repository.manifest_repository import WorkspaceManifestRepository
|
|
8
|
+
from campfire_cli.app.workspace.schema.workspace_schema import (
|
|
9
|
+
ManifestProject,
|
|
10
|
+
ManifestWorkspace,
|
|
11
|
+
ProjectEntry,
|
|
12
|
+
RegistryExport,
|
|
13
|
+
RegistryTransferResult,
|
|
14
|
+
Space,
|
|
15
|
+
WorkspaceCreateRequest,
|
|
16
|
+
WorkspaceDefaultResult,
|
|
17
|
+
WorkspaceEntry,
|
|
18
|
+
WorkspaceListResult,
|
|
19
|
+
WorkspaceManifest,
|
|
20
|
+
WorkspaceRegistry,
|
|
21
|
+
WorkspaceResolution,
|
|
22
|
+
WorkspaceResult,
|
|
23
|
+
WorkspaceSetupResult,
|
|
24
|
+
)
|
|
25
|
+
from campfire_cli.app.workspace.service.structure_service import SpaceService
|
|
26
|
+
from campfire_cli.app.workspace.service.workspace_protocol import WorkspaceRepositoryProtocol
|
|
27
|
+
from campfire_cli.common.exceptions import ConfigurationError
|
|
28
|
+
from campfire_cli.common.filesystem import atomic_write, workspace_write_lock
|
|
29
|
+
from campfire_cli.config.defaults import config_section
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WorkspaceService:
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
governance_root: Path,
|
|
36
|
+
repository: WorkspaceRepositoryProtocol,
|
|
37
|
+
manifest_repository: WorkspaceManifestRepository | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
self._root = governance_root
|
|
40
|
+
self._repository = repository
|
|
41
|
+
self._manifests = manifest_repository or WorkspaceManifestRepository()
|
|
42
|
+
|
|
43
|
+
def add(self, request: WorkspaceCreateRequest) -> WorkspaceResult:
|
|
44
|
+
root = request.path.expanduser().resolve()
|
|
45
|
+
if not root.is_dir():
|
|
46
|
+
raise ConfigurationError(f"Workspace 不存在:{root}")
|
|
47
|
+
result = self._initialize(request.workspace_id, root, request.make_default)
|
|
48
|
+
if self._manifests.load(root) is None:
|
|
49
|
+
self._manifests.save(
|
|
50
|
+
root,
|
|
51
|
+
WorkspaceManifest(
|
|
52
|
+
workspace=ManifestWorkspace(id=request.workspace_id, name=request.workspace_id)
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
def create(self, request: WorkspaceCreateRequest) -> WorkspaceResult:
|
|
58
|
+
root = request.path.expanduser().resolve()
|
|
59
|
+
self._validate_id(request.workspace_id)
|
|
60
|
+
if root.exists():
|
|
61
|
+
raise ConfigurationError(
|
|
62
|
+
f"目标路径已存在;接入现有 Workspace 请使用 workspace add:{root}"
|
|
63
|
+
)
|
|
64
|
+
template = config_section("workspace")
|
|
65
|
+
spaces = tuple(Space.model_validate(item) for item in template["spaces"])
|
|
66
|
+
scaffold = (*template["system_directories"], *(space.path for space in spaces))
|
|
67
|
+
directories = self._repository.create_scaffold(root, scaffold)
|
|
68
|
+
for space in spaces:
|
|
69
|
+
atomic_write(
|
|
70
|
+
root / space.path / "_空间.md",
|
|
71
|
+
SpaceService.render_marker(space),
|
|
72
|
+
)
|
|
73
|
+
result = self._initialize(request.workspace_id, root, request.make_default)
|
|
74
|
+
result.created_directories = directories
|
|
75
|
+
self._manifests.save(
|
|
76
|
+
root,
|
|
77
|
+
WorkspaceManifest(
|
|
78
|
+
workspace=ManifestWorkspace(id=request.workspace_id, name=request.workspace_id)
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
def setup(self, path: Path, make_default: bool = False) -> WorkspaceSetupResult:
|
|
84
|
+
"""Attach an existing portable Workspace or create its first Manifest."""
|
|
85
|
+
root = path.expanduser().resolve()
|
|
86
|
+
if not root.is_dir():
|
|
87
|
+
raise ConfigurationError(f"Workspace 不存在:{root}")
|
|
88
|
+
manifest = self._manifests.load(root)
|
|
89
|
+
manifest_operation = "preserved"
|
|
90
|
+
if manifest is None:
|
|
91
|
+
workspace_id = self._registered_id_for_path(root)
|
|
92
|
+
projects = self._repository.list_projects(workspace_id)
|
|
93
|
+
manifest = WorkspaceManifest(
|
|
94
|
+
workspace=ManifestWorkspace(id=workspace_id, name=workspace_id),
|
|
95
|
+
projects=[
|
|
96
|
+
ManifestProject.model_validate(
|
|
97
|
+
project.model_dump(exclude={"workspace_id", "local_path"})
|
|
98
|
+
)
|
|
99
|
+
for project in projects
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
self._manifests.save(root, manifest)
|
|
103
|
+
manifest_operation = "created"
|
|
104
|
+
result = self._initialize(manifest.workspace.id, root, make_default)
|
|
105
|
+
imported: list[str] = []
|
|
106
|
+
unbound: list[str] = []
|
|
107
|
+
for portable in manifest.projects:
|
|
108
|
+
existing = self._repository.get_project(portable.id)
|
|
109
|
+
if existing and existing.workspace_id != manifest.workspace.id:
|
|
110
|
+
raise ConfigurationError(f"Project id 已由其他 Workspace 使用:{portable.id}")
|
|
111
|
+
project = ProjectEntry(
|
|
112
|
+
**portable.model_dump(),
|
|
113
|
+
workspace_id=manifest.workspace.id,
|
|
114
|
+
local_path=existing.local_path if existing else None,
|
|
115
|
+
)
|
|
116
|
+
self._repository.save_project(project)
|
|
117
|
+
imported.append(project.id)
|
|
118
|
+
if not project.local_path:
|
|
119
|
+
unbound.append(project.id)
|
|
120
|
+
return WorkspaceSetupResult(
|
|
121
|
+
**result.model_dump(),
|
|
122
|
+
manifest=str(self._manifests.path(root)),
|
|
123
|
+
manifest_operation=manifest_operation,
|
|
124
|
+
imported_projects=imported,
|
|
125
|
+
unbound_projects=unbound,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def list(self) -> WorkspaceListResult:
|
|
129
|
+
registry = self._repository.load_registry()
|
|
130
|
+
return WorkspaceListResult(**registry.model_dump())
|
|
131
|
+
|
|
132
|
+
def show(self, selector: str) -> WorkspaceResolution:
|
|
133
|
+
workspace_id, path = self._resolve(selector)
|
|
134
|
+
return WorkspaceResolution(
|
|
135
|
+
workspace_id=workspace_id,
|
|
136
|
+
workspace=str(path),
|
|
137
|
+
state_root=str(self._root / "workspaces" / workspace_id),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def resolve(self, selector: str | None, cwd: Path) -> WorkspaceResolution:
|
|
141
|
+
workspace_id, path = self._resolve(selector, cwd)
|
|
142
|
+
return WorkspaceResolution(
|
|
143
|
+
workspace_id=workspace_id,
|
|
144
|
+
workspace=str(path),
|
|
145
|
+
state_root=str(self._root / "workspaces" / workspace_id),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def set_default(self, workspace_id: str) -> WorkspaceDefaultResult:
|
|
149
|
+
with workspace_write_lock(self._root):
|
|
150
|
+
registry = self._repository.load_registry()
|
|
151
|
+
if workspace_id not in registry.workspaces:
|
|
152
|
+
raise ConfigurationError(f"Workspace 未注册:{workspace_id}")
|
|
153
|
+
registry.default_workspace = workspace_id
|
|
154
|
+
self._repository.save_registry(registry)
|
|
155
|
+
return WorkspaceDefaultResult(default_workspace=workspace_id)
|
|
156
|
+
|
|
157
|
+
def export_registry(self, target: Path) -> RegistryTransferResult:
|
|
158
|
+
registry = self._repository.load_registry()
|
|
159
|
+
projects = self._repository.list_projects()
|
|
160
|
+
payload = RegistryExport(
|
|
161
|
+
default_workspace=registry.default_workspace,
|
|
162
|
+
workspaces=registry.workspaces,
|
|
163
|
+
projects=projects,
|
|
164
|
+
)
|
|
165
|
+
destination = target.expanduser().resolve()
|
|
166
|
+
atomic_write(
|
|
167
|
+
destination,
|
|
168
|
+
json.dumps(payload.model_dump(mode="json"), ensure_ascii=False, indent=2) + "\n",
|
|
169
|
+
)
|
|
170
|
+
return RegistryTransferResult(
|
|
171
|
+
status="exported",
|
|
172
|
+
path=str(destination),
|
|
173
|
+
workspace_count=len(registry.workspaces),
|
|
174
|
+
project_count=len(projects),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def import_registry(self, source: Path, confirm: bool = False) -> RegistryTransferResult:
|
|
178
|
+
source = source.expanduser().resolve()
|
|
179
|
+
if not source.is_file():
|
|
180
|
+
raise ConfigurationError(f"导入文件不存在:{source}")
|
|
181
|
+
payload = RegistryExport.model_validate_json(source.read_text(encoding="utf-8"))
|
|
182
|
+
self._validate_import(payload)
|
|
183
|
+
if confirm:
|
|
184
|
+
with workspace_write_lock(self._root):
|
|
185
|
+
self._repository.replace_registry(
|
|
186
|
+
WorkspaceRegistry(
|
|
187
|
+
schema_version=payload.schema_version,
|
|
188
|
+
default_workspace=payload.default_workspace,
|
|
189
|
+
workspaces=payload.workspaces,
|
|
190
|
+
),
|
|
191
|
+
payload.projects,
|
|
192
|
+
)
|
|
193
|
+
return RegistryTransferResult(
|
|
194
|
+
status="imported" if confirm else "planned",
|
|
195
|
+
path=str(source),
|
|
196
|
+
workspace_count=len(payload.workspaces),
|
|
197
|
+
project_count=len(payload.projects),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _initialize(self, workspace_id: str, root: Path, make_default: bool) -> WorkspaceResult:
|
|
201
|
+
self._validate_id(workspace_id)
|
|
202
|
+
with workspace_write_lock(self._root):
|
|
203
|
+
config_path = self._root / "config.yml"
|
|
204
|
+
config_created = False
|
|
205
|
+
if not config_path.exists():
|
|
206
|
+
atomic_write(
|
|
207
|
+
config_path,
|
|
208
|
+
"version: 1\n# 只写需要覆盖的 Campfire 默认配置。\n",
|
|
209
|
+
)
|
|
210
|
+
config_created = True
|
|
211
|
+
registry = self._repository.load_registry()
|
|
212
|
+
existing = registry.workspaces.get(workspace_id)
|
|
213
|
+
if existing and Path(existing.path).expanduser().resolve() != root:
|
|
214
|
+
raise ConfigurationError(f"Workspace id 已指向其他路径:{workspace_id}")
|
|
215
|
+
duplicate = next(
|
|
216
|
+
(
|
|
217
|
+
other_id
|
|
218
|
+
for other_id, entry in registry.workspaces.items()
|
|
219
|
+
if other_id != workspace_id and Path(entry.path).expanduser().resolve() == root
|
|
220
|
+
),
|
|
221
|
+
None,
|
|
222
|
+
)
|
|
223
|
+
if duplicate:
|
|
224
|
+
raise ConfigurationError(f"Workspace 路径已由其他 id 注册:{duplicate}")
|
|
225
|
+
registry.workspaces[workspace_id] = WorkspaceEntry(path=str(root))
|
|
226
|
+
if make_default or not registry.default_workspace:
|
|
227
|
+
registry.default_workspace = workspace_id
|
|
228
|
+
self._repository.save_registry(registry)
|
|
229
|
+
return WorkspaceResult(
|
|
230
|
+
status="initialized",
|
|
231
|
+
workspace_id=workspace_id,
|
|
232
|
+
workspace=str(root),
|
|
233
|
+
state_root=str(self._root / "workspaces" / workspace_id),
|
|
234
|
+
default=registry.default_workspace == workspace_id,
|
|
235
|
+
created=[str(config_path)] if config_created else [],
|
|
236
|
+
preserved=[] if config_created else [str(config_path)],
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def _resolve(self, selector: str | None, cwd: Path | None = None) -> tuple[str, Path]:
|
|
240
|
+
registry = self._repository.load_registry()
|
|
241
|
+
if selector is not None:
|
|
242
|
+
if selector in registry.workspaces:
|
|
243
|
+
return selector, Path(registry.workspaces[selector].path).expanduser().resolve()
|
|
244
|
+
candidate = Path(selector).expanduser().resolve()
|
|
245
|
+
for workspace_id, entry in registry.workspaces.items():
|
|
246
|
+
if Path(entry.path).expanduser().resolve() == candidate:
|
|
247
|
+
return workspace_id, candidate
|
|
248
|
+
raise ConfigurationError(
|
|
249
|
+
f"Workspace 未注册:{selector};请先运行 campfire workspace add"
|
|
250
|
+
)
|
|
251
|
+
current = (cwd or Path.cwd()).resolve()
|
|
252
|
+
matches = [
|
|
253
|
+
(workspace_id, Path(entry.path).expanduser().resolve())
|
|
254
|
+
for workspace_id, entry in registry.workspaces.items()
|
|
255
|
+
if current == Path(entry.path).expanduser().resolve()
|
|
256
|
+
or Path(entry.path).expanduser().resolve() in current.parents
|
|
257
|
+
]
|
|
258
|
+
if matches:
|
|
259
|
+
return max(matches, key=lambda item: len(item[1].parts))
|
|
260
|
+
if registry.default_workspace in registry.workspaces:
|
|
261
|
+
entry = registry.workspaces[registry.default_workspace]
|
|
262
|
+
return registry.default_workspace, Path(entry.path).expanduser().resolve()
|
|
263
|
+
raise ConfigurationError(
|
|
264
|
+
"没有可用 Workspace;已有 Vault(含 .campfire.yaml)运行 "
|
|
265
|
+
"campfire setup --workspace <path> --default,"
|
|
266
|
+
"全新目录运行 campfire workspace add --id <id> --path <path> --default"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _registered_id_for_path(self, root: Path) -> str:
|
|
270
|
+
registry = self._repository.load_registry()
|
|
271
|
+
matches = [
|
|
272
|
+
workspace_id
|
|
273
|
+
for workspace_id, entry in registry.workspaces.items()
|
|
274
|
+
if Path(entry.path).expanduser().resolve() == root
|
|
275
|
+
]
|
|
276
|
+
if len(matches) != 1:
|
|
277
|
+
raise ConfigurationError(
|
|
278
|
+
"Workspace 尚无 Manifest,且无法从本机注册表唯一推断 id;"
|
|
279
|
+
"请先运行 campfire workspace add --id <id> --path <path>"
|
|
280
|
+
)
|
|
281
|
+
return matches[0]
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def _validate_import(cls, payload: RegistryExport) -> None:
|
|
285
|
+
if payload.default_workspace and payload.default_workspace not in payload.workspaces:
|
|
286
|
+
raise ConfigurationError("default_workspace 未出现在 workspaces 中")
|
|
287
|
+
for workspace_id, entry in payload.workspaces.items():
|
|
288
|
+
cls._validate_id(workspace_id)
|
|
289
|
+
if not Path(entry.path).expanduser().is_absolute():
|
|
290
|
+
raise ConfigurationError(f"Workspace path 必须是绝对路径:{workspace_id}")
|
|
291
|
+
project_ids: set[str] = set()
|
|
292
|
+
for project in payload.projects:
|
|
293
|
+
if project.id in project_ids:
|
|
294
|
+
raise ConfigurationError(f"Project id 重复:{project.id}")
|
|
295
|
+
project_ids.add(project.id)
|
|
296
|
+
if project.workspace_id not in payload.workspaces:
|
|
297
|
+
raise ConfigurationError(
|
|
298
|
+
f"Project 引用了未注册 Workspace:{project.id}/{project.workspace_id}"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
@staticmethod
|
|
302
|
+
def _validate_id(workspace_id: str) -> None:
|
|
303
|
+
if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,62}", workspace_id):
|
|
304
|
+
raise ConfigurationError("id 只能使用小写字母、数字和连字符")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Common SPEC
|
|
2
|
+
|
|
3
|
+
本目录放 Workspace 与 Maintenance 等业务模块共同使用的原子能力。
|
|
4
|
+
|
|
5
|
+
- `documents/`:Markdown、Frontmatter、文档类型、链接、领域、关系和 MOC。
|
|
6
|
+
- `archive/`:归档规则与计划能力。
|
|
7
|
+
- `database/`:SQLite、模型和数据库迁移机制。
|
|
8
|
+
- `filesystem/`:安全路径、原子写入和锁。
|
|
9
|
+
- `reports/`:稳定 JSON 与 Markdown 报告渲染。
|
|
10
|
+
- `governance/`:跨用例共享的规则引擎、Issue 目录和乐观并发快照;治理规则不得散落在 Service。
|
|
11
|
+
|
|
12
|
+
`common/` 不承载完整用例流程;检查、计划、执行与验收的编排仍由 `app/` Service 负责。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Protocol-neutral infrastructure mechanisms."""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.resources import as_file, files
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from alembic.config import Config
|
|
7
|
+
|
|
8
|
+
from alembic import command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def upgrade_database(database_path: Path) -> None:
|
|
12
|
+
database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
resource = files("campfire_cli").joinpath("alembic")
|
|
14
|
+
with as_file(resource) as script_location:
|
|
15
|
+
config = Config()
|
|
16
|
+
config.set_main_option("script_location", str(script_location))
|
|
17
|
+
config.set_main_option("sqlalchemy.url", f"sqlite:///{database_path}")
|
|
18
|
+
command.upgrade(config, "head")
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
|
|
6
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Base(DeclarativeBase):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def utc_now() -> datetime:
|
|
14
|
+
return datetime.now(UTC).replace(tzinfo=None)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Workspace(Base):
|
|
18
|
+
__tablename__ = "workspaces"
|
|
19
|
+
id: Mapped[str] = mapped_column(String(63), primary_key=True)
|
|
20
|
+
path: Mapped[str] = mapped_column(Text, unique=True)
|
|
21
|
+
is_default: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
|
22
|
+
status: Mapped[str] = mapped_column(String(24), default="active")
|
|
23
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now)
|
|
24
|
+
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Project(Base):
|
|
28
|
+
__tablename__ = "projects"
|
|
29
|
+
id: Mapped[str] = mapped_column(String(63), primary_key=True)
|
|
30
|
+
workspace_id: Mapped[str] = mapped_column(
|
|
31
|
+
ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
32
|
+
)
|
|
33
|
+
name: Mapped[str] = mapped_column(String(160))
|
|
34
|
+
document_domain: Mapped[str] = mapped_column(Text)
|
|
35
|
+
git_remote_url: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
36
|
+
local_path: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
37
|
+
default_branch: Mapped[str | None] = mapped_column(String(160), nullable=True)
|
|
38
|
+
status: Mapped[str] = mapped_column(String(24), default="active")
|
|
39
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now)
|
|
40
|
+
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class WorkspaceSpace(Base):
|
|
44
|
+
__tablename__ = "spaces"
|
|
45
|
+
__table_args__ = (UniqueConstraint("workspace_id", "space_id"),)
|
|
46
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
47
|
+
workspace_id: Mapped[str] = mapped_column(
|
|
48
|
+
ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
49
|
+
)
|
|
50
|
+
space_id: Mapped[str] = mapped_column(String(128))
|
|
51
|
+
name: Mapped[str] = mapped_column(String(160))
|
|
52
|
+
path: Mapped[str] = mapped_column(Text)
|
|
53
|
+
space_type: Mapped[str] = mapped_column(String(64))
|
|
54
|
+
status: Mapped[str] = mapped_column(String(24))
|
|
55
|
+
source_hash: Mapped[str] = mapped_column(String(64))
|
|
56
|
+
indexed_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class WorkspaceDomain(Base):
|
|
60
|
+
__tablename__ = "domains"
|
|
61
|
+
__table_args__ = (UniqueConstraint("workspace_id", "domain_id"),)
|
|
62
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
63
|
+
workspace_id: Mapped[str] = mapped_column(
|
|
64
|
+
ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
65
|
+
)
|
|
66
|
+
domain_id: Mapped[str] = mapped_column(String(128))
|
|
67
|
+
space_id: Mapped[str] = mapped_column(String(128), index=True)
|
|
68
|
+
parent_domain_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
69
|
+
project_id: Mapped[str | None] = mapped_column(String(63), nullable=True)
|
|
70
|
+
name: Mapped[str] = mapped_column(String(160))
|
|
71
|
+
path: Mapped[str] = mapped_column(Text)
|
|
72
|
+
domain_type: Mapped[str] = mapped_column(String(64))
|
|
73
|
+
governance: Mapped[str] = mapped_column(String(64))
|
|
74
|
+
moc: Mapped[str] = mapped_column(Text)
|
|
75
|
+
status: Mapped[str] = mapped_column(String(24))
|
|
76
|
+
source_hash: Mapped[str] = mapped_column(String(64))
|
|
77
|
+
indexed_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Document(Base):
|
|
81
|
+
__tablename__ = "documents"
|
|
82
|
+
__table_args__ = (UniqueConstraint("workspace_id", "path"),)
|
|
83
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
84
|
+
workspace_id: Mapped[str] = mapped_column(
|
|
85
|
+
ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
86
|
+
)
|
|
87
|
+
path: Mapped[str] = mapped_column(Text, index=True)
|
|
88
|
+
content_hash: Mapped[str] = mapped_column(String(64))
|
|
89
|
+
document_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
90
|
+
domain_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
91
|
+
status: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
92
|
+
exists: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
93
|
+
indexed_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class GovernanceIssue(Base):
|
|
97
|
+
__tablename__ = "governance_issues"
|
|
98
|
+
__table_args__ = (UniqueConstraint("workspace_id", "path", "code", "detail"),)
|
|
99
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
100
|
+
workspace_id: Mapped[str] = mapped_column(
|
|
101
|
+
ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
102
|
+
)
|
|
103
|
+
path: Mapped[str] = mapped_column(Text)
|
|
104
|
+
code: Mapped[str] = mapped_column(String(96), index=True)
|
|
105
|
+
detail: Mapped[str] = mapped_column(Text, default="")
|
|
106
|
+
severity: Mapped[str] = mapped_column(String(16), default="error")
|
|
107
|
+
status: Mapped[str] = mapped_column(String(16), default="open")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class MaintenanceRun(Base):
|
|
111
|
+
__tablename__ = "maintenance_runs"
|
|
112
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
113
|
+
workspace_id: Mapped[str] = mapped_column(
|
|
114
|
+
ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
115
|
+
)
|
|
116
|
+
run_id: Mapped[str] = mapped_column(String(36), unique=True)
|
|
117
|
+
status: Mapped[str] = mapped_column(String(24))
|
|
118
|
+
scanned_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
119
|
+
issue_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
120
|
+
started_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now)
|
|
121
|
+
finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Decision(Base):
|
|
125
|
+
__tablename__ = "decisions"
|
|
126
|
+
__table_args__ = (UniqueConstraint("workspace_id", "dedupe_key"),)
|
|
127
|
+
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
|
128
|
+
workspace_id: Mapped[str] = mapped_column(
|
|
129
|
+
ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
130
|
+
)
|
|
131
|
+
dedupe_key: Mapped[str] = mapped_column(String(160))
|
|
132
|
+
question: Mapped[str] = mapped_column(Text)
|
|
133
|
+
context: Mapped[str] = mapped_column(Text, default="")
|
|
134
|
+
recommendation: Mapped[str] = mapped_column(Text, default="")
|
|
135
|
+
options_json: Mapped[str] = mapped_column(Text, default="[]")
|
|
136
|
+
related_documents_json: Mapped[str] = mapped_column(Text, default="[]")
|
|
137
|
+
source_type: Mapped[str] = mapped_column(String(64))
|
|
138
|
+
source_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
|
|
139
|
+
session_provider: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
140
|
+
session_id: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
|
141
|
+
status: Mapped[str] = mapped_column(String(24), default="pending", index=True)
|
|
142
|
+
answer: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
143
|
+
answered_by: Mapped[str | None] = mapped_column(String(160), nullable=True)
|
|
144
|
+
cancellation_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
145
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now)
|
|
146
|
+
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now)
|
|
147
|
+
answered_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
148
|
+
closed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class DecisionEvent(Base):
|
|
152
|
+
__tablename__ = "decision_events"
|
|
153
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
154
|
+
workspace_id: Mapped[str] = mapped_column(
|
|
155
|
+
ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
156
|
+
)
|
|
157
|
+
decision_id: Mapped[str] = mapped_column(
|
|
158
|
+
ForeignKey("decisions.id", ondelete="CASCADE"), index=True
|
|
159
|
+
)
|
|
160
|
+
event_type: Mapped[str] = mapped_column(String(64), index=True)
|
|
161
|
+
actor: Mapped[str | None] = mapped_column(String(160), nullable=True)
|
|
162
|
+
payload_json: Mapped[str] = mapped_column(Text, default="{}")
|
|
163
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class RestructureBatch(Base):
|
|
167
|
+
__tablename__ = "restructure_batches"
|
|
168
|
+
__table_args__ = (UniqueConstraint("workspace_id", "batch_name"),)
|
|
169
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
170
|
+
workspace_id: Mapped[str] = mapped_column(
|
|
171
|
+
ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
172
|
+
)
|
|
173
|
+
batch_uuid: Mapped[str] = mapped_column(String(36), unique=True)
|
|
174
|
+
batch_name: Mapped[str] = mapped_column(String(128))
|
|
175
|
+
scope: Mapped[str] = mapped_column(Text)
|
|
176
|
+
status: Mapped[str] = mapped_column(String(24), default="inventoried")
|
|
177
|
+
config_hash: Mapped[str] = mapped_column(String(64))
|
|
178
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class RestructureItem(Base):
|
|
182
|
+
__tablename__ = "restructure_items"
|
|
183
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
184
|
+
workspace_id: Mapped[str] = mapped_column(
|
|
185
|
+
ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
186
|
+
)
|
|
187
|
+
batch_id: Mapped[int] = mapped_column(ForeignKey("restructure_batches.id"), index=True)
|
|
188
|
+
item_uuid: Mapped[str] = mapped_column(String(36), unique=True)
|
|
189
|
+
source_path: Mapped[str] = mapped_column(Text)
|
|
190
|
+
target_path: Mapped[str] = mapped_column(Text)
|
|
191
|
+
source_hash: Mapped[str] = mapped_column(String(64))
|
|
192
|
+
action: Mapped[str] = mapped_column(String(32))
|
|
193
|
+
proposed_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
194
|
+
reason: Mapped[str] = mapped_column(Text, default="")
|
|
195
|
+
confidence: Mapped[str] = mapped_column(String(16), default="low")
|
|
196
|
+
approved: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
197
|
+
execution_status: Mapped[str] = mapped_column(String(24), default="pending")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class AdoptionBatch(Base):
|
|
201
|
+
__tablename__ = "adoption_batches"
|
|
202
|
+
__table_args__ = (UniqueConstraint("workspace_id", "batch_name"),)
|
|
203
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
204
|
+
workspace_id: Mapped[str] = mapped_column(
|
|
205
|
+
ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
206
|
+
)
|
|
207
|
+
batch_name: Mapped[str] = mapped_column(String(128))
|
|
208
|
+
source_path: Mapped[str] = mapped_column(Text)
|
|
209
|
+
source_kind: Mapped[str] = mapped_column(String(16))
|
|
210
|
+
staging_path: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
211
|
+
status: Mapped[str] = mapped_column(String(24))
|
|
212
|
+
inventory_json: Mapped[str] = mapped_column(Text)
|
|
213
|
+
plan_json: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
214
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now)
|
|
215
|
+
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import create_engine, event
|
|
4
|
+
from sqlalchemy.engine import Engine
|
|
5
|
+
from sqlalchemy.orm import Session
|
|
6
|
+
|
|
7
|
+
from campfire_cli.common.database.models import Base
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_sqlite_engine(database_path: Path) -> Engine:
|
|
11
|
+
database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
engine = create_engine(f"sqlite:///{database_path}")
|
|
13
|
+
|
|
14
|
+
@event.listens_for(engine, "connect")
|
|
15
|
+
def configure_sqlite(dbapi_connection, _connection_record) -> None:
|
|
16
|
+
cursor = dbapi_connection.cursor()
|
|
17
|
+
cursor.execute("PRAGMA foreign_keys=ON")
|
|
18
|
+
cursor.close()
|
|
19
|
+
|
|
20
|
+
with engine.connect() as connection:
|
|
21
|
+
connection.exec_driver_sql("PRAGMA journal_mode=WAL")
|
|
22
|
+
return engine
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def initialize_database(engine: Engine) -> None:
|
|
26
|
+
Base.metadata.create_all(engine)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def open_session(engine: Engine) -> Session:
|
|
30
|
+
return Session(engine)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Stateless Markdown parsing and Workspace document primitives."""
|