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,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from campfire_cli.app.workspace.repository.workspace_repository import SqliteWorkspaceRepository
|
|
11
|
+
from campfire_cli.app.workspace.schema.workspace_schema import WorkspaceCreateRequest
|
|
12
|
+
from campfire_cli.app.workspace.service.workspace_service import WorkspaceService
|
|
13
|
+
from campfire_cli.common.exceptions import AppError
|
|
14
|
+
from campfire_cli.config.settings import campfire_home
|
|
15
|
+
|
|
16
|
+
workspace_cli = typer.Typer(
|
|
17
|
+
help="注册、初始化和解析多个 Workspace",
|
|
18
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def service() -> WorkspaceService:
|
|
23
|
+
root = campfire_home()
|
|
24
|
+
return WorkspaceService(root, SqliteWorkspaceRepository(root))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def emit(result: BaseModel) -> None:
|
|
28
|
+
typer.echo(json.dumps(result.model_dump(mode="json"), ensure_ascii=False, indent=2))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def invoke[ResultT: BaseModel](operation: Callable[[], ResultT]) -> ResultT:
|
|
32
|
+
try:
|
|
33
|
+
return operation()
|
|
34
|
+
except AppError as exc:
|
|
35
|
+
typer.echo(
|
|
36
|
+
json.dumps({"status": "error", "message": str(exc)}, ensure_ascii=False), err=True
|
|
37
|
+
)
|
|
38
|
+
raise typer.Exit(exc.exit_code) from exc
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def initialize(workspace_id: str, path: Path, make_default: bool) -> BaseModel:
|
|
42
|
+
return invoke(
|
|
43
|
+
lambda: service().add(
|
|
44
|
+
WorkspaceCreateRequest(workspace_id=workspace_id, path=path, make_default=make_default)
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def setup_workspace(path: Path, make_default: bool) -> BaseModel:
|
|
50
|
+
return invoke(lambda: service().setup(path, make_default))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@workspace_cli.command("add")
|
|
54
|
+
def add(
|
|
55
|
+
workspace_id: str = typer.Option(..., "--id"),
|
|
56
|
+
path: Path = typer.Option(..., "--path"),
|
|
57
|
+
make_default: bool = typer.Option(False, "--default"),
|
|
58
|
+
) -> None:
|
|
59
|
+
"""注册并初始化一个已存在的 Workspace。"""
|
|
60
|
+
emit(initialize(workspace_id, path, make_default))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@workspace_cli.command("attach")
|
|
64
|
+
def attach(
|
|
65
|
+
path: Path = typer.Option(..., "--path"),
|
|
66
|
+
make_default: bool = typer.Option(False, "--default"),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""根据 Vault 根目录的 .campfire.yaml 接入本机。"""
|
|
69
|
+
emit(setup_workspace(path, make_default))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@workspace_cli.command("create")
|
|
73
|
+
def create(
|
|
74
|
+
workspace_id: str = typer.Option(..., "--id"),
|
|
75
|
+
path: Path = typer.Option(..., "--path"),
|
|
76
|
+
make_default: bool = typer.Option(False, "--default"),
|
|
77
|
+
) -> None:
|
|
78
|
+
"""从零创建 Workspace 基础目录,并注册和初始化治理能力。"""
|
|
79
|
+
request = WorkspaceCreateRequest(
|
|
80
|
+
workspace_id=workspace_id, path=path, make_default=make_default
|
|
81
|
+
)
|
|
82
|
+
emit(invoke(lambda: service().create(request)))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@workspace_cli.command("list")
|
|
86
|
+
def list_workspaces() -> None:
|
|
87
|
+
"""列出所有注册的 Workspace。"""
|
|
88
|
+
emit(invoke(service().list))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@workspace_cli.command("show")
|
|
92
|
+
def show(workspace_id: str) -> None:
|
|
93
|
+
"""显示一个 Workspace 的路径和本机状态目录。"""
|
|
94
|
+
emit(invoke(lambda: service().show(workspace_id)))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@workspace_cli.command("resolve")
|
|
98
|
+
def resolve(selector: str | None = typer.Option(None, "--workspace")) -> None:
|
|
99
|
+
"""按显式选择、当前目录或默认值解析 Workspace。"""
|
|
100
|
+
emit(invoke(lambda: service().resolve(selector, Path.cwd())))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@workspace_cli.command("set-default")
|
|
104
|
+
def set_default(workspace_id: str) -> None:
|
|
105
|
+
"""设置默认 Workspace。"""
|
|
106
|
+
emit(invoke(lambda: service().set_default(workspace_id)))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@workspace_cli.command("rebuild")
|
|
110
|
+
def rebuild(
|
|
111
|
+
workspace: str | None = typer.Option(None, "--workspace"),
|
|
112
|
+
confirm: bool = typer.Option(False, "--confirm"),
|
|
113
|
+
) -> None:
|
|
114
|
+
"""从 Manifest 和 Markdown SSOT 重建本机派生索引。"""
|
|
115
|
+
if not confirm:
|
|
116
|
+
typer.echo(
|
|
117
|
+
json.dumps(
|
|
118
|
+
{
|
|
119
|
+
"status": "ready",
|
|
120
|
+
"operation": "replace-workspace-derived-indexes",
|
|
121
|
+
"write_performed": False,
|
|
122
|
+
},
|
|
123
|
+
ensure_ascii=False,
|
|
124
|
+
indent=2,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
from campfire_cli.container import AppContainer
|
|
129
|
+
|
|
130
|
+
emit(invoke(lambda: AppContainer.build(workspace).maintenance.check(summary=True)))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@workspace_cli.command("export")
|
|
134
|
+
def export_registry(output: Path = typer.Option(..., "--output")) -> None:
|
|
135
|
+
"""将 Workspace 与 Project 注册数据导出为 JSON 备份。"""
|
|
136
|
+
emit(invoke(lambda: service().export_registry(output)))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@workspace_cli.command("import")
|
|
140
|
+
def import_registry(
|
|
141
|
+
input_path: Path = typer.Option(..., "--input"),
|
|
142
|
+
confirm: bool = typer.Option(False, "--confirm"),
|
|
143
|
+
) -> None:
|
|
144
|
+
"""校验注册数据备份;追加 --confirm 后替换当前注册数据。"""
|
|
145
|
+
emit(invoke(lambda: service().import_registry(input_path, confirm)))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Workspace registry filesystem implementation."""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import select
|
|
6
|
+
from sqlalchemy.orm import Session
|
|
7
|
+
|
|
8
|
+
from campfire_cli.app.workspace.schema.adoption_schema import AdoptionBatchState
|
|
9
|
+
from campfire_cli.common.database.models import AdoptionBatch
|
|
10
|
+
from campfire_cli.common.exceptions import ConfigurationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SqliteAdoptionRepository:
|
|
14
|
+
def __init__(self, session: Session, workspace_id: str) -> None:
|
|
15
|
+
self._session = session
|
|
16
|
+
self._workspace_id = workspace_id
|
|
17
|
+
|
|
18
|
+
def save(self, state: AdoptionBatchState) -> None:
|
|
19
|
+
row = self._row(state.batch)
|
|
20
|
+
if row is None:
|
|
21
|
+
row = AdoptionBatch(workspace_id=self._workspace_id, batch_name=state.batch)
|
|
22
|
+
self._session.add(row)
|
|
23
|
+
row.source_path = state.source_path
|
|
24
|
+
row.source_kind = state.source_kind
|
|
25
|
+
row.staging_path = state.staging_path
|
|
26
|
+
row.status = state.status
|
|
27
|
+
row.inventory_json = json.dumps(
|
|
28
|
+
[item.model_dump(mode="json") for item in state.inventory], ensure_ascii=False
|
|
29
|
+
)
|
|
30
|
+
row.plan_json = state.plan.model_dump_json() if state.plan else None
|
|
31
|
+
self._session.commit()
|
|
32
|
+
|
|
33
|
+
def load(self, batch: str) -> AdoptionBatchState:
|
|
34
|
+
row = self._row(batch)
|
|
35
|
+
if row is None:
|
|
36
|
+
raise ConfigurationError(f"接管批次不存在:{batch}")
|
|
37
|
+
return AdoptionBatchState(
|
|
38
|
+
batch=row.batch_name,
|
|
39
|
+
source_path=row.source_path,
|
|
40
|
+
source_kind=row.source_kind,
|
|
41
|
+
staging_path=row.staging_path,
|
|
42
|
+
status=row.status,
|
|
43
|
+
inventory=json.loads(row.inventory_json),
|
|
44
|
+
plan=json.loads(row.plan_json) if row.plan_json else None,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def _row(self, batch: str) -> AdoptionBatch | None:
|
|
48
|
+
if not batch or "/" in batch or "\\" in batch or batch in {".", ".."}:
|
|
49
|
+
raise ConfigurationError("非法接管批次名称")
|
|
50
|
+
return self._session.scalar(
|
|
51
|
+
select(AdoptionBatch).where(
|
|
52
|
+
AdoptionBatch.workspace_id == self._workspace_id,
|
|
53
|
+
AdoptionBatch.batch_name == batch,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from campfire_cli.app.workspace.schema.workspace_schema import WorkspaceManifest
|
|
8
|
+
from campfire_cli.common.exceptions import ConfigurationError
|
|
9
|
+
from campfire_cli.common.filesystem import atomic_write
|
|
10
|
+
|
|
11
|
+
MANIFEST_FILENAME = ".campfire.yaml"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WorkspaceManifestRepository:
|
|
15
|
+
"""Read and write the portable Workspace metadata stored with a Vault."""
|
|
16
|
+
|
|
17
|
+
def path(self, workspace: Path) -> Path:
|
|
18
|
+
return workspace / MANIFEST_FILENAME
|
|
19
|
+
|
|
20
|
+
def load(self, workspace: Path) -> WorkspaceManifest | None:
|
|
21
|
+
target = self.path(workspace)
|
|
22
|
+
if not target.is_file():
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
payload = yaml.safe_load(target.read_text(encoding="utf-8")) or {}
|
|
26
|
+
return WorkspaceManifest.model_validate(payload)
|
|
27
|
+
except (yaml.YAMLError, ValueError) as exc:
|
|
28
|
+
raise ConfigurationError(f"Manifest 无效:{target}:{exc}") from exc
|
|
29
|
+
|
|
30
|
+
def save(self, workspace: Path, manifest: WorkspaceManifest) -> Path:
|
|
31
|
+
target = self.path(workspace)
|
|
32
|
+
content = yaml.safe_dump(
|
|
33
|
+
manifest.model_dump(mode="json", exclude_none=True),
|
|
34
|
+
allow_unicode=True,
|
|
35
|
+
sort_keys=False,
|
|
36
|
+
)
|
|
37
|
+
atomic_write(target, content)
|
|
38
|
+
return target
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import select
|
|
7
|
+
from sqlalchemy.orm import Session
|
|
8
|
+
|
|
9
|
+
from campfire_cli.app.workspace.schema.restructure_schema import (
|
|
10
|
+
InventoryItem,
|
|
11
|
+
RestructurePlan,
|
|
12
|
+
RestructureResult,
|
|
13
|
+
)
|
|
14
|
+
from campfire_cli.common.database.models import RestructureBatch
|
|
15
|
+
from campfire_cli.common.filesystem import atomic_write
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SqliteRestructureRepository:
|
|
19
|
+
def __init__(self, session: Session, state_root: Path, workspace_id: str) -> None:
|
|
20
|
+
self._session = session
|
|
21
|
+
self._state_root = state_root
|
|
22
|
+
self._workspace_id = workspace_id
|
|
23
|
+
|
|
24
|
+
def save_batch(self, batch_uuid: str, batch: str, scope: str, config_hash: str) -> None:
|
|
25
|
+
existing = self._session.scalar(
|
|
26
|
+
select(RestructureBatch).where(
|
|
27
|
+
RestructureBatch.workspace_id == self._workspace_id,
|
|
28
|
+
RestructureBatch.batch_name == batch,
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
if existing is None:
|
|
32
|
+
self._session.add(
|
|
33
|
+
RestructureBatch(
|
|
34
|
+
workspace_id=self._workspace_id,
|
|
35
|
+
batch_uuid=batch_uuid,
|
|
36
|
+
batch_name=batch,
|
|
37
|
+
scope=scope,
|
|
38
|
+
config_hash=config_hash,
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
existing.scope = scope
|
|
43
|
+
existing.config_hash = config_hash
|
|
44
|
+
self._session.commit()
|
|
45
|
+
|
|
46
|
+
def save_inventory(self, batch: str, items: list[InventoryItem]) -> None:
|
|
47
|
+
payload = {
|
|
48
|
+
"schema_version": 1,
|
|
49
|
+
"batch": batch,
|
|
50
|
+
"items": [item.model_dump() for item in items],
|
|
51
|
+
}
|
|
52
|
+
self._write(batch, "inventory.json", payload)
|
|
53
|
+
|
|
54
|
+
def load_inventory(self, batch: str) -> tuple[str, str, list[InventoryItem]]:
|
|
55
|
+
row = self._session.scalar(
|
|
56
|
+
select(RestructureBatch).where(
|
|
57
|
+
RestructureBatch.workspace_id == self._workspace_id,
|
|
58
|
+
RestructureBatch.batch_name == batch,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
if row is None:
|
|
62
|
+
raise ValueError(f"批次不存在:{batch}")
|
|
63
|
+
payload = self._read(batch, "inventory.json")
|
|
64
|
+
return (
|
|
65
|
+
row.scope,
|
|
66
|
+
row.config_hash,
|
|
67
|
+
[InventoryItem.model_validate(item) for item in payload["items"]],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def save_plan(self, plan: RestructurePlan) -> None:
|
|
71
|
+
self._write(plan.batch, "plan.json", plan.model_dump(mode="json"))
|
|
72
|
+
|
|
73
|
+
def load_plan(self, batch: str) -> RestructurePlan:
|
|
74
|
+
return RestructurePlan.model_validate(self._read(batch, "plan.json"))
|
|
75
|
+
|
|
76
|
+
def save_execution(self, result: RestructureResult) -> None:
|
|
77
|
+
self._write(result.batch, "execution.json", result.model_dump(mode="json"))
|
|
78
|
+
|
|
79
|
+
def save_verification(self, result: RestructureResult) -> None:
|
|
80
|
+
self._write(result.batch, "verification.json", result.model_dump(mode="json"))
|
|
81
|
+
|
|
82
|
+
def _path(self, batch: str, name: str) -> Path:
|
|
83
|
+
if not batch or "/" in batch or "\\" in batch or batch in {".", ".."}:
|
|
84
|
+
raise ValueError("非法批次名称")
|
|
85
|
+
return self._state_root / "batches" / batch / name
|
|
86
|
+
|
|
87
|
+
def _write(self, batch: str, name: str, payload: dict[str, object]) -> None:
|
|
88
|
+
atomic_write(
|
|
89
|
+
self._path(batch, name), json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _read(self, batch: str, name: str) -> dict[str, object]:
|
|
93
|
+
return json.loads(self._path(batch, name).read_text(encoding="utf-8"))
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import delete, select
|
|
6
|
+
|
|
7
|
+
from campfire_cli.app.workspace.schema.workspace_schema import (
|
|
8
|
+
ProjectEntry,
|
|
9
|
+
WorkspaceEntry,
|
|
10
|
+
WorkspaceRegistry,
|
|
11
|
+
)
|
|
12
|
+
from campfire_cli.common.database import create_sqlite_engine, open_session, upgrade_database
|
|
13
|
+
from campfire_cli.common.database.models import Project, Workspace
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SqliteWorkspaceRepository:
|
|
17
|
+
"""Persist the user-level Workspace and Project registry in SQLite."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, governance_root: Path) -> None:
|
|
20
|
+
self._root = governance_root
|
|
21
|
+
database_path = governance_root / "campfire.db"
|
|
22
|
+
upgrade_database(database_path)
|
|
23
|
+
self._engine = create_sqlite_engine(database_path)
|
|
24
|
+
|
|
25
|
+
def load_registry(self) -> WorkspaceRegistry:
|
|
26
|
+
with open_session(self._engine) as session:
|
|
27
|
+
rows = session.scalars(select(Workspace).order_by(Workspace.id)).all()
|
|
28
|
+
default = next((row.id for row in rows if row.is_default), None)
|
|
29
|
+
return WorkspaceRegistry(
|
|
30
|
+
default_workspace=default,
|
|
31
|
+
workspaces={
|
|
32
|
+
row.id: WorkspaceEntry(path=row.path, status=row.status, default=row.is_default)
|
|
33
|
+
for row in rows
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def save_registry(self, registry: WorkspaceRegistry) -> None:
|
|
38
|
+
with open_session(self._engine) as session, session.begin():
|
|
39
|
+
existing = {row.id: row for row in session.scalars(select(Workspace)).all()}
|
|
40
|
+
for workspace_id, entry in registry.workspaces.items():
|
|
41
|
+
row = existing.pop(workspace_id, None)
|
|
42
|
+
if row is None:
|
|
43
|
+
row = Workspace(id=workspace_id, path=entry.path)
|
|
44
|
+
session.add(row)
|
|
45
|
+
row.path = entry.path
|
|
46
|
+
row.status = entry.status
|
|
47
|
+
row.is_default = workspace_id == registry.default_workspace
|
|
48
|
+
for row in existing.values():
|
|
49
|
+
session.delete(row)
|
|
50
|
+
|
|
51
|
+
def list_projects(self, workspace_id: str | None = None) -> list[ProjectEntry]:
|
|
52
|
+
statement = select(Project).order_by(Project.workspace_id, Project.id)
|
|
53
|
+
if workspace_id is not None:
|
|
54
|
+
statement = statement.where(Project.workspace_id == workspace_id)
|
|
55
|
+
with open_session(self._engine) as session:
|
|
56
|
+
rows = session.scalars(statement).all()
|
|
57
|
+
return [self._project_entry(row) for row in rows]
|
|
58
|
+
|
|
59
|
+
def get_project(self, project_id: str) -> ProjectEntry | None:
|
|
60
|
+
with open_session(self._engine) as session:
|
|
61
|
+
row = session.get(Project, project_id)
|
|
62
|
+
return self._project_entry(row) if row else None
|
|
63
|
+
|
|
64
|
+
def save_project(self, project: ProjectEntry) -> str:
|
|
65
|
+
with open_session(self._engine) as session, session.begin():
|
|
66
|
+
row = session.get(Project, project.id)
|
|
67
|
+
operation = "updated" if row else "created"
|
|
68
|
+
if row is None:
|
|
69
|
+
row = Project(id=project.id, workspace_id=project.workspace_id)
|
|
70
|
+
session.add(row)
|
|
71
|
+
row.workspace_id = project.workspace_id
|
|
72
|
+
row.name = project.name
|
|
73
|
+
row.document_domain = project.document_domain
|
|
74
|
+
row.git_remote_url = project.git_remote_url
|
|
75
|
+
row.local_path = project.local_path
|
|
76
|
+
row.default_branch = project.default_branch
|
|
77
|
+
row.status = project.status
|
|
78
|
+
return operation
|
|
79
|
+
|
|
80
|
+
def replace_registry(self, registry: WorkspaceRegistry, projects: list[ProjectEntry]) -> None:
|
|
81
|
+
with open_session(self._engine) as session, session.begin():
|
|
82
|
+
session.execute(delete(Project))
|
|
83
|
+
session.execute(delete(Workspace))
|
|
84
|
+
for workspace_id, entry in registry.workspaces.items():
|
|
85
|
+
session.add(
|
|
86
|
+
Workspace(
|
|
87
|
+
id=workspace_id,
|
|
88
|
+
path=entry.path,
|
|
89
|
+
status=entry.status,
|
|
90
|
+
is_default=workspace_id == registry.default_workspace,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
session.flush()
|
|
94
|
+
for project in projects:
|
|
95
|
+
session.add(
|
|
96
|
+
Project(
|
|
97
|
+
id=project.id,
|
|
98
|
+
workspace_id=project.workspace_id,
|
|
99
|
+
name=project.name,
|
|
100
|
+
document_domain=project.document_domain,
|
|
101
|
+
git_remote_url=project.git_remote_url,
|
|
102
|
+
local_path=project.local_path,
|
|
103
|
+
default_branch=project.default_branch,
|
|
104
|
+
status=project.status,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def create_scaffold(root: Path, directories: tuple[str, ...]) -> list[str]:
|
|
110
|
+
for relative in directories:
|
|
111
|
+
(root / relative).mkdir(parents=True, exist_ok=True)
|
|
112
|
+
return list(directories)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _project_entry(row: Project) -> ProjectEntry:
|
|
116
|
+
return ProjectEntry(
|
|
117
|
+
id=row.id,
|
|
118
|
+
workspace_id=row.workspace_id,
|
|
119
|
+
name=row.name,
|
|
120
|
+
document_domain=row.document_domain,
|
|
121
|
+
git_remote_url=row.git_remote_url,
|
|
122
|
+
local_path=row.local_path,
|
|
123
|
+
default_branch=row.default_branch,
|
|
124
|
+
status=row.status,
|
|
125
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Workspace application schemas."""
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AdoptionInventoryItem(BaseModel):
|
|
9
|
+
path: str
|
|
10
|
+
sha256: str
|
|
11
|
+
size: int
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AdoptionPlan(BaseModel):
|
|
15
|
+
batch: str
|
|
16
|
+
target_path: str
|
|
17
|
+
domain_id: str
|
|
18
|
+
name: str
|
|
19
|
+
space_id: str
|
|
20
|
+
domain_type: str
|
|
21
|
+
governance: str
|
|
22
|
+
parent_domain: str | None = None
|
|
23
|
+
project_id: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AdoptionBatchState(BaseModel):
|
|
27
|
+
batch: str
|
|
28
|
+
source_path: str
|
|
29
|
+
source_kind: str
|
|
30
|
+
staging_path: str | None = None
|
|
31
|
+
status: str
|
|
32
|
+
inventory: list[AdoptionInventoryItem]
|
|
33
|
+
plan: AdoptionPlan | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AdoptionResult(BaseModel):
|
|
37
|
+
status: str
|
|
38
|
+
batch: str
|
|
39
|
+
source: str
|
|
40
|
+
source_kind: str
|
|
41
|
+
staging_path: str | None = None
|
|
42
|
+
target_path: str | None = None
|
|
43
|
+
item_count: int = 0
|
|
44
|
+
copied_count: int = 0
|
|
45
|
+
operations: list[dict[str, str]] = Field(default_factory=list)
|
|
46
|
+
issues: list[dict[str, Any]] = Field(default_factory=list)
|
|
47
|
+
write_performed: bool = False
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InventoryItem(BaseModel):
|
|
9
|
+
path: str
|
|
10
|
+
sha256: str
|
|
11
|
+
size: int
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RestructurePlanItem(BaseModel):
|
|
15
|
+
item_id: str
|
|
16
|
+
source: str
|
|
17
|
+
target: str
|
|
18
|
+
source_sha256: str
|
|
19
|
+
action: str
|
|
20
|
+
proposed_type: str | None = None
|
|
21
|
+
frontmatter: dict[str, Any] = Field(default_factory=dict)
|
|
22
|
+
reason: str
|
|
23
|
+
confidence: str
|
|
24
|
+
approved: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RestructurePlan(BaseModel):
|
|
28
|
+
schema_version: int = 1
|
|
29
|
+
batch: str
|
|
30
|
+
scope: str
|
|
31
|
+
config_hash: str
|
|
32
|
+
items: list[RestructurePlanItem] = Field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RestructureIntentItem(BaseModel):
|
|
36
|
+
source: str
|
|
37
|
+
target: str | None = None
|
|
38
|
+
frontmatter: dict[str, Any] = Field(default_factory=dict)
|
|
39
|
+
reason: str
|
|
40
|
+
approved: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RestructureIntentSpec(BaseModel):
|
|
44
|
+
schema_version: int = 1
|
|
45
|
+
operations: list[RestructureIntentItem]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RestructureResult(BaseModel):
|
|
49
|
+
status: str
|
|
50
|
+
batch: str
|
|
51
|
+
item_count: int
|
|
52
|
+
applied_count: int = 0
|
|
53
|
+
issues: list[dict[str, Any]] = Field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DomainRestructureResult(BaseModel):
|
|
57
|
+
status: str
|
|
58
|
+
domain_id: str
|
|
59
|
+
name: str
|
|
60
|
+
path: str
|
|
61
|
+
operations: list[dict[str, str]] = Field(default_factory=list)
|
|
62
|
+
affected_projects: list[str] = Field(default_factory=list)
|
|
63
|
+
write_performed: bool = False
|
|
64
|
+
issues: list[dict[str, Any]] = Field(default_factory=list)
|