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.
Files changed (154) hide show
  1. campfire_cli/__init__.py +3 -0
  2. campfire_cli/alembic/__init__.py +1 -0
  3. campfire_cli/alembic/env.py +37 -0
  4. campfire_cli/alembic/script.py.mako +22 -0
  5. campfire_cli/alembic/versions/001_initial_schema.py +81 -0
  6. campfire_cli/alembic/versions/002_workspace_project_registry.py +47 -0
  7. campfire_cli/alembic/versions/003_unified_workspace_state.py +113 -0
  8. campfire_cli/alembic/versions/004_restructure_workspace_domain.py +23 -0
  9. campfire_cli/alembic/versions/005_decision_channel.py +71 -0
  10. campfire_cli/alembic/versions/006_workspace_topology_index.py +61 -0
  11. campfire_cli/alembic/versions/007_workspace_adoption.py +38 -0
  12. campfire_cli/alembic/versions/__init__.py +1 -0
  13. campfire_cli/app/SPEC.md +12 -0
  14. campfire_cli/app/__init__.py +1 -0
  15. campfire_cli/app/base/SPEC.md +3 -0
  16. campfire_cli/app/base/__init__.py +1 -0
  17. campfire_cli/app/base/cli/base_cli.py +37 -0
  18. campfire_cli/app/base/repository/base_repository.py +17 -0
  19. campfire_cli/app/base/schema/base_schema.py +18 -0
  20. campfire_cli/app/base/service/base_service.py +129 -0
  21. campfire_cli/app/decision/SPEC.md +16 -0
  22. campfire_cli/app/decision/__init__.py +1 -0
  23. campfire_cli/app/decision/cli/decision_cli.py +112 -0
  24. campfire_cli/app/decision/repository/decision_repository.py +199 -0
  25. campfire_cli/app/decision/schema/decision_schema.py +56 -0
  26. campfire_cli/app/decision/service/decision_projection_service.py +151 -0
  27. campfire_cli/app/decision/service/decision_service.py +124 -0
  28. campfire_cli/app/document/SPEC.md +51 -0
  29. campfire_cli/app/document/__init__.py +1 -0
  30. campfire_cli/app/document/cli/__init__.py +1 -0
  31. campfire_cli/app/document/cli/document_cli.py +63 -0
  32. campfire_cli/app/document/cli/profile_cli.py +65 -0
  33. campfire_cli/app/document/cli/type_cli.py +49 -0
  34. campfire_cli/app/document/repository/__init__.py +1 -0
  35. campfire_cli/app/document/repository/document_profile_repository.py +17 -0
  36. campfire_cli/app/document/repository/document_type_repository.py +17 -0
  37. campfire_cli/app/document/service/__init__.py +1 -0
  38. campfire_cli/app/document/service/document_profile_service.py +66 -0
  39. campfire_cli/app/document/service/document_rule_service.py +316 -0
  40. campfire_cli/app/document/service/document_scanner.py +38 -0
  41. campfire_cli/app/document/service/document_service.py +139 -0
  42. campfire_cli/app/document/service/document_type_service.py +28 -0
  43. campfire_cli/app/document/service/frontmatter_apply.py +98 -0
  44. campfire_cli/app/document/service/frontmatter_formatter.py +104 -0
  45. campfire_cli/app/document/service/frontmatter_plan.py +75 -0
  46. campfire_cli/app/document/service/profile_registry.py +166 -0
  47. campfire_cli/app/document/service/type_apply.py +185 -0
  48. campfire_cli/app/document/service/type_plan.py +80 -0
  49. campfire_cli/app/maintenance/SPEC.md +22 -0
  50. campfire_cli/app/maintenance/__init__.py +1 -0
  51. campfire_cli/app/maintenance/cli/maintenance_cli.py +103 -0
  52. campfire_cli/app/maintenance/repository/maintenance_repository.py +115 -0
  53. campfire_cli/app/maintenance/schema/maintenance_schema.py +109 -0
  54. campfire_cli/app/maintenance/service/archive_service.py +255 -0
  55. campfire_cli/app/maintenance/service/link_service.py +91 -0
  56. campfire_cli/app/maintenance/service/maintenance_protocol.py +25 -0
  57. campfire_cli/app/maintenance/service/maintenance_service.py +461 -0
  58. campfire_cli/app/maintenance/service/moc_service.py +310 -0
  59. campfire_cli/app/maintenance/service/plan_service.py +411 -0
  60. campfire_cli/app/skill/SPEC.md +3 -0
  61. campfire_cli/app/skill/__init__.py +1 -0
  62. campfire_cli/app/skill/cli/skill_cli.py +44 -0
  63. campfire_cli/app/skill/repository/skill_repository.py +33 -0
  64. campfire_cli/app/skill/schema/skill_schema.py +18 -0
  65. campfire_cli/app/skill/service/skill_service.py +146 -0
  66. campfire_cli/app/workspace/SPEC.md +25 -0
  67. campfire_cli/app/workspace/__init__.py +1 -0
  68. campfire_cli/app/workspace/cli/__init__.py +1 -0
  69. campfire_cli/app/workspace/cli/adoption_cli.py +83 -0
  70. campfire_cli/app/workspace/cli/config_cli.py +30 -0
  71. campfire_cli/app/workspace/cli/domain_cli.py +101 -0
  72. campfire_cli/app/workspace/cli/project_cli.py +135 -0
  73. campfire_cli/app/workspace/cli/restructure_cli.py +133 -0
  74. campfire_cli/app/workspace/cli/space_cli.py +65 -0
  75. campfire_cli/app/workspace/cli/workspace_cli.py +145 -0
  76. campfire_cli/app/workspace/repository/__init__.py +1 -0
  77. campfire_cli/app/workspace/repository/adoption_repository.py +55 -0
  78. campfire_cli/app/workspace/repository/manifest_repository.py +38 -0
  79. campfire_cli/app/workspace/repository/restructure_repository.py +93 -0
  80. campfire_cli/app/workspace/repository/workspace_repository.py +125 -0
  81. campfire_cli/app/workspace/schema/__init__.py +1 -0
  82. campfire_cli/app/workspace/schema/adoption_schema.py +47 -0
  83. campfire_cli/app/workspace/schema/restructure_schema.py +64 -0
  84. campfire_cli/app/workspace/schema/workspace_schema.py +225 -0
  85. campfire_cli/app/workspace/service/__init__.py +1 -0
  86. campfire_cli/app/workspace/service/adoption_protocol.py +11 -0
  87. campfire_cli/app/workspace/service/adoption_service.py +364 -0
  88. campfire_cli/app/workspace/service/config_service.py +189 -0
  89. campfire_cli/app/workspace/service/domain_restructure_service.py +316 -0
  90. campfire_cli/app/workspace/service/project_service.py +389 -0
  91. campfire_cli/app/workspace/service/restructure_protocol.py +19 -0
  92. campfire_cli/app/workspace/service/restructure_service.py +392 -0
  93. campfire_cli/app/workspace/service/restructure_verifier.py +83 -0
  94. campfire_cli/app/workspace/service/structure_service.py +494 -0
  95. campfire_cli/app/workspace/service/workspace_protocol.py +24 -0
  96. campfire_cli/app/workspace/service/workspace_service.py +304 -0
  97. campfire_cli/common/SPEC.md +12 -0
  98. campfire_cli/common/__init__.py +1 -0
  99. campfire_cli/common/database/__init__.py +4 -0
  100. campfire_cli/common/database/migrations.py +18 -0
  101. campfire_cli/common/database/models.py +215 -0
  102. campfire_cli/common/database/session.py +30 -0
  103. campfire_cli/common/documents/__init__.py +1 -0
  104. campfire_cli/common/documents/document_types.py +119 -0
  105. campfire_cli/common/documents/frontmatter_schema.py +42 -0
  106. campfire_cli/common/documents/markdown.py +66 -0
  107. campfire_cli/common/exceptions/__init__.py +7 -0
  108. campfire_cli/common/exceptions/base.py +19 -0
  109. campfire_cli/common/filesystem/__init__.py +6 -0
  110. campfire_cli/common/filesystem/atomic.py +30 -0
  111. campfire_cli/common/filesystem/locking.py +55 -0
  112. campfire_cli/common/governance/__init__.py +13 -0
  113. campfire_cli/common/governance/issues.py +38 -0
  114. campfire_cli/common/governance/locking.py +31 -0
  115. campfire_cli/common/governance/snapshots.py +26 -0
  116. campfire_cli/common/hashing.py +10 -0
  117. campfire_cli/common/reports/__init__.py +1 -0
  118. campfire_cli/common/reports/json_report.py +8 -0
  119. campfire_cli/common/reports/markdown_report.py +29 -0
  120. campfire_cli/config/SPEC.md +5 -0
  121. campfire_cli/config/defaults.py +64 -0
  122. campfire_cli/config/settings.py +50 -0
  123. campfire_cli/container.py +130 -0
  124. campfire_cli/main.py +133 -0
  125. campfire_cli/resources/__init__.py +1 -0
  126. campfire_cli/resources/bases/Agent/346/211/247/350/241/214/345/267/245/344/275/234/345/217/260.base +35 -0
  127. campfire_cli/resources/bases/SPEC.md +3 -0
  128. campfire_cli/resources/bases//344/273/273/345/212/241/345/267/245/344/275/234/345/217/260.base +49 -0
  129. campfire_cli/resources/bases//345/206/263/347/255/226/345/267/245/344/275/234/345/217/260.base +40 -0
  130. campfire_cli/resources/bases//345/276/205/345/275/222/346/241/243/346/226/207/346/241/243.base +28 -0
  131. campfire_cli/resources/bases//351/241/271/347/233/256/346/226/207/346/241/243.base +35 -0
  132. campfire_cli/resources/defaults/__init__.py +1 -0
  133. campfire_cli/resources/defaults/config.yml +458 -0
  134. campfire_cli/resources/skills/SPEC.md +3 -0
  135. campfire_cli/resources/skills/campfire-context-bootstrap/SKILL.md +62 -0
  136. campfire_cli/resources/skills/campfire-context-bootstrap/agents/openai.yaml +3 -0
  137. campfire_cli/resources/skills/campfire-conversation-router/SKILL.md +53 -0
  138. campfire_cli/resources/skills/campfire-conversation-router/agents/openai.yaml +3 -0
  139. campfire_cli/resources/skills/campfire-conversation-router/references/inbox-handoff.md +19 -0
  140. campfire_cli/resources/skills/campfire-conversation-router/references/learning-intake.md +31 -0
  141. campfire_cli/resources/skills/campfire-conversation-router/references/task-intake.md +28 -0
  142. campfire_cli/resources/skills/campfire-document-capture/SKILL.md +109 -0
  143. campfire_cli/resources/skills/campfire-inbox-triage/SKILL.md +23 -0
  144. campfire_cli/resources/skills/campfire-weekly-report-writing/SKILL.md +21 -0
  145. campfire_cli/resources/skills/campfire-weekly-report-writing/agents/openai.yaml +3 -0
  146. campfire_cli/resources/skills/campfire-workspace-adoption/SKILL.md +42 -0
  147. campfire_cli/resources/skills/campfire-workspace-maintenance/SKILL.md +74 -0
  148. 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
  149. campfire_cli/resources/skills/campfire-workspace-restructure/SKILL.md +52 -0
  150. campfire_cli-0.1.0.dist-info/METADATA +141 -0
  151. campfire_cli-0.1.0.dist-info/RECORD +154 -0
  152. campfire_cli-0.1.0.dist-info/WHEEL +4 -0
  153. campfire_cli-0.1.0.dist-info/entry_points.txt +2 -0
  154. 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)