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,461 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from campfire_cli.app.document.service.document_rule_service import DocumentRuleService
|
|
8
|
+
from campfire_cli.app.document.service.document_scanner import iter_documents
|
|
9
|
+
from campfire_cli.app.maintenance.schema.maintenance_schema import (
|
|
10
|
+
DocumentState,
|
|
11
|
+
DomainState,
|
|
12
|
+
Issue,
|
|
13
|
+
MaintenanceResult,
|
|
14
|
+
MaintenanceRunRecord,
|
|
15
|
+
SpaceState,
|
|
16
|
+
)
|
|
17
|
+
from campfire_cli.app.maintenance.service import archive_service as project_archive
|
|
18
|
+
from campfire_cli.app.maintenance.service import moc_service as governance_sync
|
|
19
|
+
from campfire_cli.app.maintenance.service.maintenance_protocol import (
|
|
20
|
+
MaintenanceRepositoryProtocol,
|
|
21
|
+
)
|
|
22
|
+
from campfire_cli.app.maintenance.service.plan_service import MaintenancePlanService
|
|
23
|
+
from campfire_cli.app.workspace.service.structure_service import DomainService
|
|
24
|
+
from campfire_cli.common.documents.markdown import parse_document
|
|
25
|
+
from campfire_cli.common.filesystem import atomic_write, safe_path
|
|
26
|
+
from campfire_cli.common.governance import (
|
|
27
|
+
capture_snapshot,
|
|
28
|
+
enrich_issue,
|
|
29
|
+
filter_issues,
|
|
30
|
+
optimistic_write_lock,
|
|
31
|
+
)
|
|
32
|
+
from campfire_cli.common.hashing import file_sha256
|
|
33
|
+
from campfire_cli.common.reports.json_report import render_json_report
|
|
34
|
+
from campfire_cli.common.reports.markdown_report import render_maintenance_report
|
|
35
|
+
from campfire_cli.config.settings import WorkspaceSettings
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MaintenanceService:
|
|
39
|
+
def __init__(
|
|
40
|
+
self, settings: WorkspaceSettings, repository: MaintenanceRepositoryProtocol
|
|
41
|
+
) -> None:
|
|
42
|
+
self._settings = settings
|
|
43
|
+
self._repository = repository
|
|
44
|
+
self._rules = DocumentRuleService(settings.document_types, settings.frontmatter_schema)
|
|
45
|
+
self._plans = MaintenancePlanService(settings, repository)
|
|
46
|
+
|
|
47
|
+
def check(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
scope: str | None = None,
|
|
51
|
+
code: str | None = None,
|
|
52
|
+
severity: str | None = None,
|
|
53
|
+
summary: bool = False,
|
|
54
|
+
) -> MaintenanceResult:
|
|
55
|
+
structure = DomainService(self._settings.vault_root, self._settings.state_root)
|
|
56
|
+
discovered_domains, _domain_issues = structure.discover()
|
|
57
|
+
discovered_spaces, _space_issues = structure.spaces.discover()
|
|
58
|
+
spaces, domains = self._topology_states(discovered_spaces, discovered_domains)
|
|
59
|
+
documents: list[DocumentState] = []
|
|
60
|
+
issues: list[Issue] = []
|
|
61
|
+
paths = self._iter_documents()
|
|
62
|
+
for path in paths:
|
|
63
|
+
relative = path.relative_to(self._settings.vault_root).as_posix()
|
|
64
|
+
text = path.read_text(encoding="utf-8")
|
|
65
|
+
parsed = parse_document(text)
|
|
66
|
+
document_type = parsed.frontmatter.get("type")
|
|
67
|
+
documents.append(
|
|
68
|
+
DocumentState(
|
|
69
|
+
path=relative,
|
|
70
|
+
content_hash=file_sha256(path),
|
|
71
|
+
document_type=document_type if isinstance(document_type, str) else None,
|
|
72
|
+
domain_id=self._nearest_domain(path),
|
|
73
|
+
status=parsed.frontmatter.get("status"),
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
issues.extend(
|
|
77
|
+
Issue.model_validate(enrich_issue(item))
|
|
78
|
+
for item in self._rules.check_document(self._settings.vault_root, path)
|
|
79
|
+
)
|
|
80
|
+
issues.extend(
|
|
81
|
+
Issue.model_validate(enrich_issue(item))
|
|
82
|
+
for item in self._rules.check_collection(self._settings.vault_root, paths)
|
|
83
|
+
)
|
|
84
|
+
issues.extend(Issue.model_validate(enrich_issue(item)) for item in self._check_inbox())
|
|
85
|
+
for skills_root in self._settings.skill_targets():
|
|
86
|
+
issues.extend(
|
|
87
|
+
Issue.model_validate(enrich_issue(item))
|
|
88
|
+
for item in self._rules.check_templates(skills_root.parent, skills_root)
|
|
89
|
+
)
|
|
90
|
+
issues = list({(item.path, item.code, item.detail): item for item in issues}.values())
|
|
91
|
+
result = MaintenanceResult(
|
|
92
|
+
status="ok" if not issues else "needs-review",
|
|
93
|
+
document_count=len(documents),
|
|
94
|
+
issue_count=len(issues),
|
|
95
|
+
total_issue_count=len(issues),
|
|
96
|
+
issues=issues,
|
|
97
|
+
issue_counts=self._issue_counts(issues),
|
|
98
|
+
space_count=len(spaces),
|
|
99
|
+
domain_count=len(domains),
|
|
100
|
+
)
|
|
101
|
+
completed_at = datetime.now(UTC)
|
|
102
|
+
run = MaintenanceRunRecord(
|
|
103
|
+
run_id=str(uuid4()),
|
|
104
|
+
status=result.status,
|
|
105
|
+
scanned_count=result.document_count,
|
|
106
|
+
issue_count=result.issue_count,
|
|
107
|
+
started_at=completed_at,
|
|
108
|
+
finished_at=completed_at,
|
|
109
|
+
)
|
|
110
|
+
observed = {document.path: document.content_hash for document in documents}
|
|
111
|
+
observed.update({f"{item.path}/_空间.md": item.source_hash for item in spaces})
|
|
112
|
+
observed.update({f"{item.path}/_领域.md": item.source_hash for item in domains})
|
|
113
|
+
with optimistic_write_lock(
|
|
114
|
+
self._settings.state_root, observed, self._settings.vault_root
|
|
115
|
+
) as changed:
|
|
116
|
+
if changed:
|
|
117
|
+
return self._concurrent_result(changed)
|
|
118
|
+
self._repository.replace_current_state(documents, issues, spaces, domains)
|
|
119
|
+
self._repository.save_run(run)
|
|
120
|
+
self._export_current_report(result, completed_at)
|
|
121
|
+
selected = filter_issues(
|
|
122
|
+
(item.model_dump() for item in issues), scope=scope, code=code, severity=severity
|
|
123
|
+
)
|
|
124
|
+
selected_issues = [Issue.model_validate(item) for item in selected]
|
|
125
|
+
scoped_documents = [
|
|
126
|
+
item
|
|
127
|
+
for item in documents
|
|
128
|
+
if scope is None or self._path_matches_scope(item.path, scope)
|
|
129
|
+
]
|
|
130
|
+
return MaintenanceResult(
|
|
131
|
+
status="ok" if not selected_issues else "needs-review",
|
|
132
|
+
document_count=len(scoped_documents),
|
|
133
|
+
issue_count=len(selected_issues),
|
|
134
|
+
total_issue_count=len(issues),
|
|
135
|
+
issues=[] if summary else selected_issues,
|
|
136
|
+
issue_counts=self._issue_counts(selected_issues),
|
|
137
|
+
scope=scope,
|
|
138
|
+
workspace_status=result.status,
|
|
139
|
+
space_count=len(spaces),
|
|
140
|
+
domain_count=len(domains),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def _export_current_report(self, result: MaintenanceResult, exported_at: datetime) -> None:
|
|
144
|
+
payload = result.model_dump(mode="json")
|
|
145
|
+
payload["exported_at"] = exported_at.isoformat()
|
|
146
|
+
report_root = self._settings.state_root / "reports"
|
|
147
|
+
atomic_write(report_root / "current.json", render_json_report(payload))
|
|
148
|
+
atomic_write(report_root / "current.md", render_maintenance_report(payload))
|
|
149
|
+
|
|
150
|
+
def plan(
|
|
151
|
+
self,
|
|
152
|
+
plan_id: str,
|
|
153
|
+
*,
|
|
154
|
+
scope: str | None = None,
|
|
155
|
+
spec_path: Path | None = None,
|
|
156
|
+
) -> MaintenanceResult:
|
|
157
|
+
return self._plans.plan(plan_id, scope=scope, spec_path=spec_path)
|
|
158
|
+
|
|
159
|
+
def show_plan(self, plan_id: str) -> MaintenanceResult:
|
|
160
|
+
return self._plans.show(plan_id)
|
|
161
|
+
|
|
162
|
+
def apply(self, plan_id: str, confirm: bool) -> MaintenanceResult:
|
|
163
|
+
return self._plans.apply(plan_id, confirm)
|
|
164
|
+
|
|
165
|
+
def verify(self, plan_id: str) -> MaintenanceResult:
|
|
166
|
+
return self._plans.verify(plan_id)
|
|
167
|
+
|
|
168
|
+
def sync(self, dry_run: bool = False, scope: str | None = None) -> MaintenanceResult:
|
|
169
|
+
domains, issues = DomainService(
|
|
170
|
+
self._settings.vault_root, self._settings.state_root
|
|
171
|
+
).discover()
|
|
172
|
+
if scope:
|
|
173
|
+
scope_path = (self._settings.vault_root / scope).resolve()
|
|
174
|
+
if not self._is_within_workspace(scope_path):
|
|
175
|
+
return self._sync_blocked("scope-outside-workspace", scope, scope)
|
|
176
|
+
domains = [
|
|
177
|
+
domain
|
|
178
|
+
for domain in domains
|
|
179
|
+
if domain.path == scope_path
|
|
180
|
+
or scope_path in domain.path.parents
|
|
181
|
+
or domain.path in scope_path.parents
|
|
182
|
+
]
|
|
183
|
+
issues = [issue for issue in issues if self._path_matches_scope(issue["path"], scope)]
|
|
184
|
+
if not domains:
|
|
185
|
+
return self._sync_blocked("scope-missing", scope, scope)
|
|
186
|
+
if issues:
|
|
187
|
+
return MaintenanceResult(
|
|
188
|
+
status="blocked",
|
|
189
|
+
document_count=0,
|
|
190
|
+
issue_count=len(issues),
|
|
191
|
+
issues=[Issue.model_validate(enrich_issue(item)) for item in issues],
|
|
192
|
+
blocked_phase="preflight",
|
|
193
|
+
blocked_scope=scope or "workspace",
|
|
194
|
+
)
|
|
195
|
+
scoped_documents = [
|
|
196
|
+
path
|
|
197
|
+
for path in self._iter_documents()
|
|
198
|
+
if any(domain.path == path.parent or domain.path in path.parents for domain in domains)
|
|
199
|
+
]
|
|
200
|
+
snapshot = capture_snapshot(self._settings.vault_root, scoped_documents)
|
|
201
|
+
marker_name = self._settings.governance.get("domain_marker", "_领域.md")
|
|
202
|
+
profile = self._settings.document_types.get("profiles", {}).get("project-docs", [])
|
|
203
|
+
type_mapping = {
|
|
204
|
+
name: self._settings.document_types["types"][name]
|
|
205
|
+
for name in profile
|
|
206
|
+
if name in self._settings.document_types["types"]
|
|
207
|
+
}
|
|
208
|
+
changes: list[tuple[Path, str]] = []
|
|
209
|
+
generated_snapshot: dict[str, str | None] = {}
|
|
210
|
+
note_count = 0
|
|
211
|
+
notes_by_domain = {
|
|
212
|
+
domain.id: governance_sync.direct_notes(domain, marker_name) for domain in domains
|
|
213
|
+
}
|
|
214
|
+
project_domain_ids = {
|
|
215
|
+
domain.id for domain in domains if domain.governance == "project-docs"
|
|
216
|
+
}
|
|
217
|
+
domain_by_note = {
|
|
218
|
+
note: domain_id
|
|
219
|
+
for domain_id, notes in notes_by_domain.items()
|
|
220
|
+
for note in notes
|
|
221
|
+
if domain_id not in project_domain_ids
|
|
222
|
+
}
|
|
223
|
+
all_notes = sorted(
|
|
224
|
+
domain_by_note,
|
|
225
|
+
key=lambda path: str(path.relative_to(self._settings.vault_root)).casefold(),
|
|
226
|
+
)
|
|
227
|
+
all_relations = governance_sync.generate_relations(
|
|
228
|
+
all_notes,
|
|
229
|
+
domain_by_note,
|
|
230
|
+
self._settings.vault_root,
|
|
231
|
+
int(self._settings.governance["related_limit"]),
|
|
232
|
+
float(self._settings.governance["related_min_score"]),
|
|
233
|
+
int(self._settings.governance["cross_domain_related_limit"]),
|
|
234
|
+
float(self._settings.governance["cross_domain_min_score"]),
|
|
235
|
+
)
|
|
236
|
+
for domain in sorted(domains, key=lambda item: item.id):
|
|
237
|
+
notes = notes_by_domain[domain.id]
|
|
238
|
+
note_count += len(notes)
|
|
239
|
+
if domain.governance == "project-docs":
|
|
240
|
+
generated = governance_sync.generate_project_domain_content(
|
|
241
|
+
domain, domains, notes, marker_name, type_mapping
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
relation_page = None
|
|
245
|
+
if notes:
|
|
246
|
+
relations = {note: all_relations[note] for note in notes}
|
|
247
|
+
relation_page = domain.path / "generated" / f"相关文档-{domain.name}.md"
|
|
248
|
+
relation_content = governance_sync.relation_markdown(domain, notes, relations)
|
|
249
|
+
existing = (
|
|
250
|
+
relation_page.read_text(encoding="utf-8") if relation_page.is_file() else ""
|
|
251
|
+
)
|
|
252
|
+
generated_snapshot[
|
|
253
|
+
relation_page.relative_to(self._settings.vault_root).as_posix()
|
|
254
|
+
] = file_sha256(relation_page) if relation_page.is_file() else None
|
|
255
|
+
if existing != relation_content:
|
|
256
|
+
changes.append((relation_page, relation_content))
|
|
257
|
+
generated = governance_sync.generate_domain_content(
|
|
258
|
+
domain, domains, notes, relation_page
|
|
259
|
+
)
|
|
260
|
+
moc = domain.path / f"{domain.moc}.md"
|
|
261
|
+
if not moc.is_file():
|
|
262
|
+
return MaintenanceResult(
|
|
263
|
+
status="blocked",
|
|
264
|
+
document_count=note_count,
|
|
265
|
+
issue_count=1,
|
|
266
|
+
issues=[
|
|
267
|
+
Issue.model_validate(
|
|
268
|
+
enrich_issue(
|
|
269
|
+
{
|
|
270
|
+
"code": "moc-missing",
|
|
271
|
+
"path": moc.relative_to(self._settings.vault_root).as_posix(),
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
],
|
|
276
|
+
blocked_phase="preflight",
|
|
277
|
+
blocked_scope=scope or "workspace",
|
|
278
|
+
)
|
|
279
|
+
current_moc = moc.read_text(encoding="utf-8")
|
|
280
|
+
generated_snapshot[moc.relative_to(self._settings.vault_root).as_posix()] = file_sha256(
|
|
281
|
+
moc
|
|
282
|
+
)
|
|
283
|
+
updated = governance_sync.replace_generated_region(current_moc, generated)
|
|
284
|
+
if updated != current_moc:
|
|
285
|
+
changes.append((moc, updated))
|
|
286
|
+
operations = [
|
|
287
|
+
{
|
|
288
|
+
"action": "update" if path.is_file() else "create",
|
|
289
|
+
"path": path.relative_to(self._settings.vault_root).as_posix(),
|
|
290
|
+
"reason": "refresh-generated-content",
|
|
291
|
+
}
|
|
292
|
+
for path, _content in changes
|
|
293
|
+
]
|
|
294
|
+
for path, expected in generated_snapshot.items():
|
|
295
|
+
snapshot.setdefault(path, expected)
|
|
296
|
+
if not dry_run and changes:
|
|
297
|
+
with optimistic_write_lock(
|
|
298
|
+
self._settings.state_root, snapshot, self._settings.vault_root
|
|
299
|
+
) as changed:
|
|
300
|
+
if changed:
|
|
301
|
+
return self._concurrent_result(changed)
|
|
302
|
+
for path, content in changes:
|
|
303
|
+
atomic_write(path, content)
|
|
304
|
+
return MaintenanceResult(
|
|
305
|
+
status="dry-run" if dry_run else "synced",
|
|
306
|
+
document_count=note_count,
|
|
307
|
+
issue_count=0,
|
|
308
|
+
generated_file_count=len(changes),
|
|
309
|
+
write_performed=bool(changes and not dry_run),
|
|
310
|
+
operations=operations,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def _sync_blocked(self, code: str, path: str, scope: str | None) -> MaintenanceResult:
|
|
314
|
+
return MaintenanceResult(
|
|
315
|
+
status="blocked",
|
|
316
|
+
document_count=0,
|
|
317
|
+
issue_count=1,
|
|
318
|
+
issues=[Issue.model_validate(enrich_issue({"code": code, "path": path}))],
|
|
319
|
+
blocked_phase="preflight",
|
|
320
|
+
blocked_scope=scope or "workspace",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def _is_within_workspace(self, path: Path) -> bool:
|
|
324
|
+
return path == self._settings.vault_root or self._settings.vault_root in path.parents
|
|
325
|
+
|
|
326
|
+
@staticmethod
|
|
327
|
+
def _path_matches_scope(path: str, scope: str) -> bool:
|
|
328
|
+
normalized_path = path.strip("/")
|
|
329
|
+
normalized_scope = scope.strip("/")
|
|
330
|
+
return normalized_path == normalized_scope or normalized_path.startswith(
|
|
331
|
+
normalized_scope + "/"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def archive(self, confirm: bool = False, scope: str | None = None) -> MaintenanceResult:
|
|
335
|
+
items, raw_issues = project_archive.collect_items(
|
|
336
|
+
self._settings.vault_root, self._settings.governance
|
|
337
|
+
)
|
|
338
|
+
if scope is not None:
|
|
339
|
+
scope_path = safe_path(self._settings.vault_root, scope)
|
|
340
|
+
items = [
|
|
341
|
+
item
|
|
342
|
+
for item in items
|
|
343
|
+
if item.source == scope_path or scope_path in item.source.parents
|
|
344
|
+
]
|
|
345
|
+
paths = {
|
|
346
|
+
item.source.relative_to(self._settings.vault_root).as_posix() for item in items
|
|
347
|
+
}
|
|
348
|
+
raw_issues = [issue for issue in raw_issues if issue.get("path") in paths]
|
|
349
|
+
applied: list[dict[str, str]] = []
|
|
350
|
+
if confirm and items:
|
|
351
|
+
snapshot = capture_snapshot(
|
|
352
|
+
self._settings.vault_root,
|
|
353
|
+
[path for item in items for path in (item.source, item.target)],
|
|
354
|
+
)
|
|
355
|
+
with optimistic_write_lock(
|
|
356
|
+
self._settings.state_root, snapshot, self._settings.vault_root
|
|
357
|
+
) as changed:
|
|
358
|
+
if changed:
|
|
359
|
+
return self._concurrent_result(changed)
|
|
360
|
+
applied = project_archive.apply_items(
|
|
361
|
+
self._settings.vault_root,
|
|
362
|
+
items,
|
|
363
|
+
raw_issues,
|
|
364
|
+
__import__("datetime").date.today().isoformat(),
|
|
365
|
+
)
|
|
366
|
+
issues = [Issue.model_validate(enrich_issue(item)) for item in raw_issues]
|
|
367
|
+
return MaintenanceResult(
|
|
368
|
+
status="issues-found" if issues else ("applied" if confirm else "ok"),
|
|
369
|
+
document_count=len(items),
|
|
370
|
+
issue_count=len(issues),
|
|
371
|
+
issues=issues,
|
|
372
|
+
issue_counts=self._issue_counts(issues),
|
|
373
|
+
changed_document_count=len(applied),
|
|
374
|
+
write_performed=bool(applied),
|
|
375
|
+
operations=applied,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def _iter_documents(self) -> list[Path]:
|
|
379
|
+
return iter_documents(self._settings.vault_root, self._settings.document_types)
|
|
380
|
+
|
|
381
|
+
def _nearest_domain(self, path: Path) -> str | None:
|
|
382
|
+
current = path.parent
|
|
383
|
+
while current == self._settings.vault_root or self._settings.vault_root in current.parents:
|
|
384
|
+
marker = current / "_领域.md"
|
|
385
|
+
if marker.is_file():
|
|
386
|
+
parsed = parse_document(marker.read_text(encoding="utf-8"))
|
|
387
|
+
value = parsed.frontmatter.get("domain_id")
|
|
388
|
+
return value if isinstance(value, str) else None
|
|
389
|
+
if current == self._settings.vault_root:
|
|
390
|
+
break
|
|
391
|
+
current = current.parent
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
def _topology_states(self, spaces, domains) -> tuple[list[SpaceState], list[DomainState]]:
|
|
395
|
+
space_states: list[SpaceState] = []
|
|
396
|
+
seen_spaces: set[str] = set()
|
|
397
|
+
for space in spaces:
|
|
398
|
+
marker = self._settings.vault_root / space.path / "_空间.md"
|
|
399
|
+
if not space.id or space.id in seen_spaces or not marker.is_file():
|
|
400
|
+
continue
|
|
401
|
+
seen_spaces.add(space.id)
|
|
402
|
+
space_states.append(
|
|
403
|
+
SpaceState(
|
|
404
|
+
space_id=space.id,
|
|
405
|
+
name=space.name,
|
|
406
|
+
path=space.path,
|
|
407
|
+
space_type=space.type,
|
|
408
|
+
status=space.status,
|
|
409
|
+
source_hash=file_sha256(marker),
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
domain_states: list[DomainState] = []
|
|
413
|
+
seen_domains: set[str] = set()
|
|
414
|
+
for domain in domains:
|
|
415
|
+
marker = domain.path / "_领域.md"
|
|
416
|
+
if not domain.id or domain.id in seen_domains or not marker.is_file():
|
|
417
|
+
continue
|
|
418
|
+
seen_domains.add(domain.id)
|
|
419
|
+
domain_states.append(
|
|
420
|
+
DomainState(
|
|
421
|
+
domain_id=domain.id,
|
|
422
|
+
space_id=domain.space_id,
|
|
423
|
+
parent_domain_id=domain.parent_domain,
|
|
424
|
+
project_id=domain.project_id,
|
|
425
|
+
name=domain.name,
|
|
426
|
+
path=domain.path.relative_to(self._settings.vault_root).as_posix(),
|
|
427
|
+
domain_type=domain.type,
|
|
428
|
+
governance=domain.governance,
|
|
429
|
+
moc=domain.moc,
|
|
430
|
+
status=domain.status,
|
|
431
|
+
source_hash=file_sha256(marker),
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
return space_states, domain_states
|
|
435
|
+
|
|
436
|
+
def _check_inbox(self) -> list[Issue]:
|
|
437
|
+
inbox = self._settings.vault_root / self._settings.governance.get("inbox", "_收件箱")
|
|
438
|
+
if not inbox.is_dir():
|
|
439
|
+
return [Issue(code="inbox-missing", path=inbox.name)]
|
|
440
|
+
return []
|
|
441
|
+
|
|
442
|
+
@staticmethod
|
|
443
|
+
def _issue_counts(issues: list[Issue]) -> dict[str, int]:
|
|
444
|
+
result: dict[str, int] = {}
|
|
445
|
+
for issue in issues:
|
|
446
|
+
result[issue.code] = result.get(issue.code, 0) + 1
|
|
447
|
+
return dict(sorted(result.items()))
|
|
448
|
+
|
|
449
|
+
@staticmethod
|
|
450
|
+
def _concurrent_result(paths: list[str]) -> MaintenanceResult:
|
|
451
|
+
issues = [
|
|
452
|
+
Issue.model_validate(enrich_issue({"code": "concurrent-change", "path": path}))
|
|
453
|
+
for path in paths
|
|
454
|
+
]
|
|
455
|
+
return MaintenanceResult(
|
|
456
|
+
status="blocked",
|
|
457
|
+
document_count=0,
|
|
458
|
+
issue_count=len(issues),
|
|
459
|
+
issues=issues,
|
|
460
|
+
issue_counts=MaintenanceService._issue_counts(issues),
|
|
461
|
+
)
|