campfire-cli 0.1.18__tar.gz → 0.1.20__tar.gz

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 (232) hide show
  1. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/.github/workflows/release.yml +2 -0
  2. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/PKG-INFO +3 -1
  3. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/README.md +2 -0
  4. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/SPEC.md +6 -1
  5. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/pyproject.toml +1 -1
  6. campfire_cli-0.1.20/scripts/release_check.py +84 -0
  7. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/__init__.py +1 -1
  8. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/base/service/base_service.py +1 -1
  9. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/SPEC.md +17 -21
  10. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/cli/document_cli.py +12 -0
  11. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/cli/profile_cli.py +8 -4
  12. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/cli/type_cli.py +1 -1
  13. campfire_cli-0.1.20/src/campfire_cli/app/document/repository/document_index_repository.py +265 -0
  14. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/schema/document_schema.py +5 -8
  15. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/service/document_scanner.py +5 -1
  16. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/service/document_service.py +15 -18
  17. campfire_cli-0.1.20/src/campfire_cli/app/document/service/index/__init__.py +1 -0
  18. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/index}/document_index_builder.py +14 -106
  19. campfire_cli-0.1.20/src/campfire_cli/app/document/service/index/document_index_protocol.py +48 -0
  20. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/index}/document_index_service.py +139 -75
  21. campfire_cli-0.1.20/src/campfire_cli/app/document/service/mutation/__init__.py +1 -0
  22. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/mutation}/document_apply_service.py +64 -13
  23. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/mutation}/document_move_service.py +63 -26
  24. campfire_cli-0.1.20/src/campfire_cli/app/document/service/mutation/document_relocation.py +85 -0
  25. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/mutation}/frontmatter_apply.py +1 -1
  26. campfire_cli-0.1.20/src/campfire_cli/app/document/service/rules/__init__.py +1 -0
  27. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/rules}/document_patch_values.py +1 -1
  28. campfire_cli-0.1.20/src/campfire_cli/app/document/service/rules/document_profile_service.py +102 -0
  29. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/rules}/document_rule_service.py +37 -1
  30. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/rules}/frontmatter_formatter.py +1 -1
  31. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/rules}/frontmatter_plan.py +1 -1
  32. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/rules}/profile_registry.py +3 -0
  33. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/maintenance/SPEC.md +3 -0
  34. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/maintenance/repository/maintenance_repository.py +18 -15
  35. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/maintenance/service/maintenance_protocol.py +4 -1
  36. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/maintenance/service/maintenance_service.py +64 -16
  37. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/maintenance/service/moc_service.py +5 -168
  38. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/SPEC.md +2 -0
  39. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/cli/domain_cli.py +10 -2
  40. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/schema/restructure_schema.py +3 -0
  41. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/service/config_service.py +0 -4
  42. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/service/domain_restructure_service.py +167 -46
  43. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/service/restructure_service.py +5 -28
  44. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/service/structure_service.py +8 -3
  45. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/agent_hints.py +2 -1
  46. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/documents/document_types.py +4 -3
  47. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/documents/markdown.py +8 -6
  48. campfire_cli-0.1.20/src/campfire_cli/common/documents/related_docs.py +107 -0
  49. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/filesystem/change_set.py +28 -14
  50. campfire_cli-0.1.20/src/campfire_cli/common/filesystem/plan.py +32 -0
  51. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/container.py +1 -1
  52. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/main.py +1 -0
  53. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/defaults/config.yml +1 -4
  54. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/SPEC.md +3 -0
  55. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-context-bootstrap/SKILL.md +2 -2
  56. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-document-capture/SKILL.md +15 -3
  57. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-document-capture/references//345/267/262/346/234/211/346/226/207/346/241/243/346/233/264/346/226/260.md +2 -1
  58. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-kanban-board/SKILL.md +1 -1
  59. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-task-management/SKILL.md +15 -2
  60. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-workspace-maintenance/SKILL.md +2 -2
  61. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-workspace-restructure/SKILL.md +12 -4
  62. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/integration/test_cli.py +61 -0
  63. campfire_cli-0.1.20/tests/integration/test_domain_rename_sync.py +302 -0
  64. campfire_cli-0.1.20/tests/integration/test_profile_resolution.py +98 -0
  65. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/integration/test_skill_examples.py +7 -0
  66. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/perf/test_read_counts.py +0 -21
  67. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_document_apply.py +148 -3
  68. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_document_index.py +138 -139
  69. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_file_change_executor.py +21 -0
  70. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_kanban_service.py +1 -1
  71. campfire_cli-0.1.20/tests/unit/test_related_docs.py +61 -0
  72. campfire_cli-0.1.20/tests/unit/test_release_check.py +37 -0
  73. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/uv.lock +1 -1
  74. campfire_cli-0.1.18/src/campfire_cli/app/document/repository/document_index_repository.py +0 -156
  75. campfire_cli-0.1.18/src/campfire_cli/app/document/service/document_index_protocol.py +0 -26
  76. campfire_cli-0.1.18/src/campfire_cli/app/document/service/document_profile_service.py +0 -70
  77. campfire_cli-0.1.18/src/campfire_cli/app/document/service/document_relocation.py +0 -57
  78. campfire_cli-0.1.18/src/campfire_cli/app/document/service/type_apply.py +0 -205
  79. campfire_cli-0.1.18/src/campfire_cli/app/maintenance/service/link_service.py +0 -72
  80. campfire_cli-0.1.18/src/campfire_cli/common/documents/links.py +0 -142
  81. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/.github/workflows/ci.yml +0 -0
  82. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/.gitignore +0 -0
  83. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/.python-version +0 -0
  84. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/AGENTS.md +0 -0
  85. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/ARCHITECTURE.md +0 -0
  86. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/COMMAND.md +0 -0
  87. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/DECISIONS.md +0 -0
  88. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/LICENSE +0 -0
  89. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/alembic/SPEC.md +0 -0
  90. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/alembic/env.py +0 -0
  91. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/alembic/script.py.mako +0 -0
  92. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/alembic/versions/001_initial_schema.py +0 -0
  93. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/alembic.ini +0 -0
  94. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/scripts/SPEC.md +0 -0
  95. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/scripts/pypi_verify.py +0 -0
  96. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/scripts/quality_check.py +0 -0
  97. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/__init__.py +0 -0
  98. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/env.py +0 -0
  99. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/script.py.mako +0 -0
  100. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/001_initial_schema.py +0 -0
  101. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/002_workspace_project_registry.py +0 -0
  102. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/003_unified_workspace_state.py +0 -0
  103. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/004_restructure_workspace_domain.py +0 -0
  104. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/005_decision_channel.py +0 -0
  105. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/006_workspace_topology_index.py +0 -0
  106. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/007_workspace_adoption.py +0 -0
  107. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/008_remove_adoption_batches.py +0 -0
  108. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/009_document_query_index.py +0 -0
  109. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/010_project_domain_identity.py +0 -0
  110. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/011_document_state_model.py +0 -0
  111. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/012_remove_decision_channel.py +0 -0
  112. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/alembic/versions/__init__.py +0 -0
  113. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/SPEC.md +0 -0
  114. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/__init__.py +0 -0
  115. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/base/SPEC.md +0 -0
  116. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/base/__init__.py +0 -0
  117. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/base/cli/base_cli.py +0 -0
  118. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/base/repository/base_repository.py +0 -0
  119. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/base/schema/base_schema.py +0 -0
  120. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/base/schema/operation_schema.py +0 -0
  121. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/__init__.py +0 -0
  122. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/cli/__init__.py +0 -0
  123. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/repository/__init__.py +0 -0
  124. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/repository/document_profile_repository.py +0 -0
  125. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/repository/document_type_repository.py +0 -0
  126. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/schema/__init__.py +0 -0
  127. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/document/service/__init__.py +0 -0
  128. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/rules}/document_type_service.py +0 -0
  129. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/rules}/kanban_service.py +0 -0
  130. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/rules}/profile_candidates.py +0 -0
  131. {campfire_cli-0.1.18/src/campfire_cli/app/document/service → campfire_cli-0.1.20/src/campfire_cli/app/document/service/rules}/type_plan.py +0 -0
  132. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/maintenance/__init__.py +0 -0
  133. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/maintenance/cli/maintenance_cli.py +0 -0
  134. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/maintenance/schema/maintenance_schema.py +0 -0
  135. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/skill/SPEC.md +0 -0
  136. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/skill/__init__.py +0 -0
  137. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/skill/cli/skill_cli.py +0 -0
  138. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/skill/repository/skill_repository.py +0 -0
  139. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/skill/schema/skill_schema.py +0 -0
  140. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/skill/service/skill_service.py +0 -0
  141. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/__init__.py +0 -0
  142. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/cli/__init__.py +0 -0
  143. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/cli/config_cli.py +0 -0
  144. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/cli/project_cli.py +0 -0
  145. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/cli/restructure_cli.py +0 -0
  146. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/cli/space_cli.py +0 -0
  147. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/cli/workspace_cli.py +0 -0
  148. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/repository/__init__.py +0 -0
  149. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/repository/manifest_repository.py +0 -0
  150. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/repository/restructure_repository.py +0 -0
  151. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/repository/workspace_repository.py +0 -0
  152. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/schema/__init__.py +0 -0
  153. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/schema/adoption_schema.py +0 -0
  154. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/schema/workspace_schema.py +0 -0
  155. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/service/__init__.py +0 -0
  156. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/service/adoption_service.py +0 -0
  157. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/service/project_service.py +0 -0
  158. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/service/restructure_protocol.py +0 -0
  159. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/service/restructure_verifier.py +0 -0
  160. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/service/workspace_protocol.py +0 -0
  161. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/app/workspace/service/workspace_service.py +0 -0
  162. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/SPEC.md +0 -0
  163. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/__init__.py +0 -0
  164. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/cli_output.py +0 -0
  165. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/database/__init__.py +0 -0
  166. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/database/migrations.py +0 -0
  167. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/database/models.py +0 -0
  168. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/database/session.py +0 -0
  169. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/documents/__init__.py +0 -0
  170. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/documents/domain_context.py +0 -0
  171. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/documents/frontmatter_format.py +0 -0
  172. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/documents/frontmatter_schema.py +0 -0
  173. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/exceptions/__init__.py +0 -0
  174. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/exceptions/base.py +0 -0
  175. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/filesystem/__init__.py +0 -0
  176. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/filesystem/atomic.py +0 -0
  177. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/filesystem/cwd.py +0 -0
  178. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/filesystem/locking.py +0 -0
  179. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/governance/__init__.py +0 -0
  180. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/governance/issues.py +0 -0
  181. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/governance/locking.py +0 -0
  182. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/governance/snapshots.py +0 -0
  183. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/hashing.py +0 -0
  184. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/package_version.py +0 -0
  185. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/reports/__init__.py +0 -0
  186. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/reports/json_report.py +0 -0
  187. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/common/reports/markdown_report.py +0 -0
  188. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/config/SPEC.md +0 -0
  189. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/config/defaults.py +0 -0
  190. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/config/settings.py +0 -0
  191. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/__init__.py +0 -0
  192. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/bases/SPEC.md +0 -0
  193. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/bases//344/273/273/345/212/241/345/267/245/344/275/234/345/217/260-/344/270/252/344/272/272.base" +0 -0
  194. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/bases//344/273/273/345/212/241/345/267/245/344/275/234/345/217/260-/351/241/271/347/233/256.base" +0 -0
  195. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/bases//345/275/222/346/241/243/346/226/207/346/241/243/345/267/245/344/275/234/345/217/260.base" +0 -0
  196. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/bases//345/276/205/344/272/272/345/206/263/347/255/226/345/267/245/344/275/234/345/217/260.base" +0 -0
  197. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/bases//346/226/207/346/241/243/345/267/245/344/275/234/345/217/260.base" +0 -0
  198. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/defaults/__init__.py +0 -0
  199. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/TEMPLATE.md +0 -0
  200. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-context-bootstrap/agents/openai.yaml +0 -0
  201. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-conversation-router/SKILL.md +0 -0
  202. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-conversation-router/agents/openai.yaml +0 -0
  203. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-conversation-router/references/inbox-handoff.md +0 -0
  204. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-conversation-router/references/learning-intake.md +0 -0
  205. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-conversation-router/references/task-intake.md +0 -0
  206. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-document-capture/references//344/273/273/345/212/241/344/270/216/350/256/241/345/210/222/346/262/211/346/267/200.md" +0 -0
  207. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-document-capture/references//351/200/232/347/224/250/347/237/245/350/257/206/346/262/211/346/267/200.md" +0 -0
  208. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-document-capture/references//351/227/256/351/242/230/344/270/216/345/206/263/347/255/226/346/262/211/346/267/200.md" +0 -0
  209. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-document-capture/references//351/241/271/347/233/256/345/206/205/345/256/271/346/262/211/346/267/200.md" +0 -0
  210. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-inbox-triage/SKILL.md +0 -0
  211. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-weekly-report-writing/SKILL.md +0 -0
  212. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-weekly-report-writing/agents/openai.yaml +0 -0
  213. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-workspace-adoption/SKILL.md +0 -0
  214. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-workspace-onboarding/SKILL.md +0 -0
  215. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-workspace-onboarding/references//346/240/207/345/207/206/345/267/245/344/275/234/345/214/272/347/273/223/346/236/204.md" +0 -0
  216. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/src/campfire_cli/resources/skills/campfire-workspace-restructure/references/restructure-spec.md +0 -0
  217. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/SPEC.md +0 -0
  218. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/conftest.py +0 -0
  219. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/perf/test_resolve_subprocess_count.py +0 -0
  220. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_agent_hints.py +0 -0
  221. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_atomic_write.py +0 -0
  222. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_base_service.py +0 -0
  223. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_config.py +0 -0
  224. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_domain_context.py +0 -0
  225. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_frontmatter.py +0 -0
  226. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_generated_regions.py +0 -0
  227. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_migrations.py +0 -0
  228. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_operation_schema.py +0 -0
  229. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_package_version.py +0 -0
  230. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_pypi_verify.py +0 -0
  231. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_repository_boundaries.py +0 -0
  232. {campfire_cli-0.1.18 → campfire_cli-0.1.20}/tests/unit/test_workspace_manifest.py +0 -0
@@ -1,6 +1,7 @@
1
1
  name: release
2
2
 
3
3
  on:
4
+ workflow_dispatch:
4
5
  push:
5
6
  tags: ["v*"]
6
7
 
@@ -36,6 +37,7 @@ jobs:
36
37
  if-no-files-found: error
37
38
 
38
39
  publish:
40
+ if: github.event_name == 'push'
39
41
  needs: quality
40
42
  runs-on: ubuntu-latest
41
43
  permissions:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: campfire-cli
3
- Version: 0.1.18
3
+ Version: 0.1.20
4
4
  Summary: Local-first shared context and governance CLI for humans and agents
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -150,6 +150,8 @@ campfire workspace restructure verify --batch move-001
150
150
 
151
151
  仓库 Skill 编写规范见 `src/campfire_cli/resources/skills/SPEC.md`。本地与 CI 共用 `uv run python scripts/quality_check.py`;发布前使用 `--release` 增加构建、wheel 隔离安装与冒烟验证。测试日志包含最慢 10 项耗时。Release 的公共 PyPI 安装验证最多等待 3 分钟、间隔 15 秒重试,不重试上传;安装后版本不符直接失败。
152
152
 
153
+ 发布准备先执行 `uv run python scripts/release_check.py --tag <目标标签>`(默认检查 HEAD,可用 `--target <提交>` 指定)。这是只读检查,不创建或推送标签。远端标签不存在时按正常流程创建;已有标签一致时复用;对象不同或提交不同时停止核对,不强推、不重建。远端已有而本地缺失时先获取原标签再检查。IDE 拉取时的标签冲突不等于源码合并冲突,也不代表分支拉取已完成;不要全局开启“始终替换本地标记”。检查通过不替代质量门禁或用户发布授权。
154
+
153
155
  ## 许可
154
156
 
155
157
  MIT。
@@ -134,6 +134,8 @@ campfire workspace restructure verify --batch move-001
134
134
 
135
135
  仓库 Skill 编写规范见 `src/campfire_cli/resources/skills/SPEC.md`。本地与 CI 共用 `uv run python scripts/quality_check.py`;发布前使用 `--release` 增加构建、wheel 隔离安装与冒烟验证。测试日志包含最慢 10 项耗时。Release 的公共 PyPI 安装验证最多等待 3 分钟、间隔 15 秒重试,不重试上传;安装后版本不符直接失败。
136
136
 
137
+ 发布准备先执行 `uv run python scripts/release_check.py --tag <目标标签>`(默认检查 HEAD,可用 `--target <提交>` 指定)。这是只读检查,不创建或推送标签。远端标签不存在时按正常流程创建;已有标签一致时复用;对象不同或提交不同时停止核对,不强推、不重建。远端已有而本地缺失时先获取原标签再检查。IDE 拉取时的标签冲突不等于源码合并冲突,也不代表分支拉取已完成;不要全局开启“始终替换本地标记”。检查通过不替代质量门禁或用户发布授权。
138
+
137
139
  ## 许可
138
140
 
139
141
  MIT。
@@ -81,6 +81,7 @@ Campfire 使用语义化版本:
81
81
  ```
82
82
 
83
83
  - 已发布版本不可原地修改;同一版本号必须对应唯一、可重复构建的代码和资源。
84
+ - 已公开标签对象不可变。发布准备必须执行 `scripts/release_check.py --tag <tag>` 对比本地、远端对象及目标提交;对象或提交不一致即停止,禁止重建、强推或自动覆盖本地标签。发布重试复用原标签,不以提交相同为理由替换附注对象。
84
85
  - 只修复既有行为时增加补丁版本,例如 `0.17.0 -> 0.17.1`。
85
86
  - 新增 CLI 能力、改变治理契约或增加可选公共字段时增加次版本,例如 `0.17.x -> 0.18.0`。
86
87
  - 删除或重命名公共命令、改变不可兼容的数据格式时增加主版本;`0.x` 阶段仍应显式记录破坏性变化。
@@ -103,6 +104,7 @@ Campfire 使用语义化版本:
103
104
 
104
105
  - `domain_id`、`name` 和 `path` 分别表示稳定身份、显示名称和物理位置,不得隐式绑定。
105
106
  - 普通 rename 和 move 不得改变 `domain_id`;只有显式 `domain rekey` 可以修改稳定身份。
107
+ - rename 的 `--name` 与 `--folder-name` 分别修改显示名称与原父级内目录名;只有显式传入才修改。目录改名确认带回 `expected_plan`,复用共享文件变更集摘要与补偿机制;不解析正文或 Canvas,不承诺崩溃原子性。
106
108
  - 修改领域名称不得修改 Project 名称;Project 展示名称只能由 `workspace project update --name` 显式修改。
107
109
  - `.campfire.yaml` 的 `projects[].document_domain_id` 是 Project 到根 Domain 绑定的唯一事实;Domain 声明不保存 Project 字段。
108
110
  - Space/Domain 声明 Frontmatter 和 `AUTO-GENERATED` 标记区域由 CLI 管理;标记外 Markdown 正文允许人和 Agent 直接编辑,formatter 必须完整保留。
@@ -114,12 +116,15 @@ Campfire 使用语义化版本:
114
116
 
115
117
  ## Workspace 索引
116
118
 
119
+ - Markdown Frontmatter 的 `related_docs` 是文档关系唯一 SSOT,采用 `[[Workspace相对路径.md]]`。不解析、索引或自动改写正文链接;历史正文保留,SQLite 和可见关系页均可从该字段重建。
120
+ - 改名、移动与类型联动改名只定向更新 `related_docs` 引用方;预览输出 `expected_plan`,确认复核整个变更集摘要和文件哈希。文件提交后的索引失败必须报告已写入事实,并使索引可在下次查询恢复。
121
+
117
122
  - SQLite `spaces`、`domains`、`documents` 只允许作为可重建本机投影,不得反向覆盖 Markdown SSOT。
118
123
  - Document App 拥有文档记录、确定关系、集合查询和单篇关系查询;Repository 只持久化投影,不解释 Profile、Domain 或链接语义。
119
124
  - 文档索引保存 schema version、parser version、有效配置 hash、拓扑 hash 和 generation;Domain 声明、Project 文档根映射等任一解释输入变化都必须自动完整重建。
120
125
  - `document list` 与 `document inspect` 查询前必须轻量 reconcile。文件 stat 只筛选变化候选,content hash 表示内容版本;调用方不需要先运行 Maintenance。
121
126
  - 候选文档与关系快照必须在一个 SQLite 事务中切换,失败继续保留上一完整 generation。
122
- - `document list` 只做结构化精确筛选;`document inspect` 只返回显式关联和链接形成的确定关系。全文检索、模糊匹配、相关性排序和相似建议不得混入这两个契约。
127
+ - `document list` 只做结构化精确筛选;`document inspect` 只返回 `related_docs` 形成的确定关系。全文检索、模糊匹配、相关性排序和相似建议不得混入这两个契约。
123
128
  - `setup` 与 `maintenance check` 必须自动刷新完整拓扑和文档索引。
124
129
  - 索引 reconcile 是读取命令内部保障,不得作为 follow-up 暴露给 Agent。写命令只在实际写入成功且可见派生状态可能变化时返回零或一个可直接执行的 scoped `maintenance sync` follow-up;Domain 内部路径必须归一化为对应 Domain,预览和阻塞结果必须返回空列表;
125
130
  调用方只执行实际返回的 follow-up,不固定追加 check 或全量扫描。
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "campfire-cli"
3
- version = "0.1.18"
3
+ version = "0.1.20"
4
4
  description = "Local-first shared context and governance CLI for humans and agents"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1,84 @@
1
+ """
2
+ SPEC:
3
+ name: release_check
4
+ purpose: 发布前只读比较本地与远端标签对象、目标提交,禁止静默替换
5
+ idempotent: true
6
+ side_effects: []
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import subprocess
14
+ from pathlib import Path
15
+
16
+
17
+ def git(root: Path, *args: str) -> str:
18
+ return subprocess.run(
19
+ ["git", *args], cwd=root, check=True, capture_output=True, text=True, timeout=30
20
+ ).stdout.strip()
21
+
22
+
23
+ def check_release(root: Path, tag: str, remote: str = "origin", target: str = "HEAD") -> dict:
24
+ git(root, "check-ref-format", f"refs/tags/{tag}")
25
+ commit = git(root, "rev-parse", "--verify", f"{target}^{{commit}}")
26
+ local_ref = git(root, "for-each-ref", "--format=%(refname)", f"refs/tags/{tag}")
27
+ local = None
28
+ if f"refs/tags/{tag}" in local_ref.splitlines():
29
+ local = {
30
+ "object": git(root, "rev-parse", f"refs/tags/{tag}"),
31
+ "commit": git(root, "rev-parse", f"refs/tags/{tag}^{{commit}}"),
32
+ }
33
+ refs = {}
34
+ for line in git(
35
+ root, "ls-remote", remote, f"refs/tags/{tag}", f"refs/tags/{tag}^{{}}"
36
+ ).splitlines():
37
+ oid, ref = line.split()
38
+ refs[ref] = oid
39
+ remote_object = refs.get(f"refs/tags/{tag}")
40
+ remote_tag = (
41
+ {"object": remote_object, "commit": refs.get(f"refs/tags/{tag}^{{}}", remote_object)}
42
+ if remote_object
43
+ else None
44
+ )
45
+ code = "tag-ready"
46
+ action = "reuse" if remote_tag else "push-existing" if local else "create"
47
+ if any(item and item["commit"] != commit for item in (local, remote_tag)):
48
+ code, action = "tag-commit-conflict", "stop"
49
+ elif remote_tag and not local:
50
+ code, action = "tag-local-missing", "stop"
51
+ elif remote_tag and local and remote_tag["object"] != local["object"]:
52
+ code, action = "tag-object-conflict", "stop"
53
+ return {
54
+ "status": "blocked" if action == "stop" else "ok",
55
+ "code": code,
56
+ "tag": tag,
57
+ "target_commit": commit,
58
+ "local": local,
59
+ "remote": remote_tag,
60
+ "action": action,
61
+ "write_performed": False,
62
+ "hint": "冲突时核对指定标签,不重建或强推;远端已有但本地缺失时先获取原标签。",
63
+ }
64
+
65
+
66
+ def main() -> None:
67
+ parser = argparse.ArgumentParser(description="只读发布标签检查,不创建、推送或覆盖标签")
68
+ parser.add_argument("--tag", required=True)
69
+ parser.add_argument("--remote", default="origin")
70
+ parser.add_argument("--target", default="HEAD")
71
+ args = parser.parse_args()
72
+ try:
73
+ result = check_release(
74
+ Path(__file__).resolve().parents[1], args.tag, args.remote, args.target
75
+ )
76
+ except (subprocess.SubprocessError, OSError, ValueError) as exc:
77
+ print(json.dumps({"status": "error", "code": "tag-check-failed", "message": str(exc)}))
78
+ raise SystemExit(1) from exc
79
+ print(json.dumps(result, ensure_ascii=False, indent=2))
80
+ raise SystemExit(0 if result["status"] == "ok" else 1)
81
+
82
+
83
+ if __name__ == "__main__":
84
+ main()
@@ -1,3 +1,3 @@
1
1
  """Campfire CLI package."""
2
2
 
3
- __version__ = "0.1.18"
3
+ __version__ = "0.1.20"
@@ -18,7 +18,7 @@ class BaseService:
18
18
  _FIELD_REFERENCE = re.compile(
19
19
  r"(?<![.\w])([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?)\s*(?:==|!=|\.isEmpty\(|\.contains\()"
20
20
  )
21
- _ENUM_COMPARISON = re.compile(r'''\b([A-Za-z_]\w*)\s*(?:==|!=)\s*["']([^"']+)["']''')
21
+ _ENUM_COMPARISON = re.compile(r"""\b([A-Za-z_]\w*)\s*(?:==|!=)\s*["']([^"']+)["']""")
22
22
 
23
23
  def __init__(self, settings: WorkspaceSettings, repository: BaseRepository) -> None:
24
24
  self._settings = settings
@@ -15,22 +15,11 @@ document/
15
15
  ├── schema/
16
16
  │ └── document_schema.py
17
17
  ├── service/
18
- │ ├── document_apply_service.py
19
- │ ├── document_move_service.py
20
- │ ├── document_profile_service.py
21
- │ ├── document_type_service.py
22
- │ ├── document_service.py
23
- │ ├── document_index_service.py
24
- │ ├── document_index_builder.py
25
- │ ├── document_index_protocol.py
26
- │ ├── document_rule_service.py
27
- │ ├── document_scanner.py
28
- │ ├── profile_registry.py
29
- │ ├── type_plan.py
30
- │ ├── type_apply.py
31
- │ ├── frontmatter_plan.py
32
- │ ├── frontmatter_apply.py
33
- │ └── frontmatter_formatter.py
18
+ │ ├── document_service.py # 公开应用入口
19
+ │ ├── document_scanner.py # 统一受管范围
20
+ │ ├── rules/ # Profile、字段校验与格式
21
+ │ ├── index/ # 新鲜度、投影构建与查询
22
+ │ └── mutation/ # 写入、移动与关系改写
34
23
  └── repository/
35
24
  ├── document_index_repository.py
36
25
  ├── document_profile_repository.py
@@ -38,25 +27,32 @@ document/
38
27
  ```
39
28
 
40
29
  - `profile`:定义并解析不同文档的属性契约。
30
+ - `profile resolve --type` 用于新建前查询,`--path` 用于已有文档,两者互斥;和 apply 共用 ProfileRegistry。`show` 只按 Profile 名查询,不把 type 当作同义参数,不新增领域覆盖机制。
41
31
  - `type`:检查、规划和执行单选类型与文件命名治理。
42
32
  - `frontmatter`:检查、规划、执行和格式化文档属性。
43
33
  - `rule`:统一解释类型、Profile、枚举与跨字段不变量。
44
- - `index`:Service 只编排新鲜度、generation 和查询,Builder 只把 Markdown、Domain/Project 上下文与显式链接转换为确定性投影;配置、拓扑或解析器变化时完整重建,普通文件变化时调用时 reconcile。
34
+ - `index`:Service 只编排新鲜度、generation 和查询,Builder 只把 Markdown 属性、Domain/Project 上下文与 `related_docs`转换为确定性投影;配置、拓扑或解析器变化时完整重建,普通文件变化时调用时 reconcile。
45
35
  - `list`:按 Project、Domain、类型和生命周期对索引做精确 AND 筛选;返回全部生命周期,不读取或复制正文。
46
- - `inspect`:向 Agent 返回单篇文档的 Domain、类型、有效 Profile、具体问题,以及 declared、outgoing、incoming、unresolved 四类确定关系。
36
+ - `inspect`:向 Agent 返回单篇文档的 Domain、类型、有效 Profile、具体问题,以及 outgoing、incoming、unresolved 及入度、出度。
47
37
  - `apply`:根据目标路径和类型解析 Profile,不存在时生成文档,已有 Frontmatter 时只应用显式补丁,无 Frontmatter 时保留正文并补齐完整契约;先完整渲染并校验,追加 `--confirm` 后原子写入。
48
38
  - `template`:只继承 base Profile,必须位于所属 Domain 的 `_模板/`;Domain 归属由物理路径解析,不在 Frontmatter 复制模板作用域或继承关系。
49
- - `move`:在任意已声明 Domain 之间移动一篇文档,按目标 Profile 对齐可确定的 `domain`/`project` 归属,并更新可确定解析的 Wiki、Markdown 和路径引用。目标 Profile 缺少业务字段时返回 `needs-input`,不猜测。
39
+ - `move`:在任意已声明 Domain 之间移动一篇文档,按目标 Profile 对齐可确定的 `domain`/`project` 归属,并只更新 `related_docs` 的精确路径引用。目标 Profile 缺少业务字段时返回 `needs-input`,不猜测。
50
40
  - `rename`:保持一篇文档的物理目录与 Domain 归属不变,根据逻辑标题重算类型前缀文件名,并在同一变更集中更新 `name` 与可确定引用。目标存在时阻止,不覆盖或自动编号。
51
41
  - `scanner`:统一解释受管根、忽略目录和豁免文件;Document 与 Maintenance 共用。
52
42
 
53
43
  Document Service 可以被 Workspace Restructure 编排,但不得反向依赖 Maintenance。批量计划文件、运行记录、MOC、Base、归档和重构批次不属于 Document App。
54
44
 
55
- Markdown 是内容与关系事实源,SQLite 是按 Workspace 隔离的查询投影。Document Index Repository 不解析 Markdown、Profile 或 Domain,只原子持久化文档、关系和 generation 快照。索引不得复制正文;相似度、全文关键词和相关性排序不属于当前契约,未来只能以独立 suggestions/search 契约扩展。
45
+ Markdown 是内容事实源,Frontmatter `related_docs` 是文档关系唯一 SSOT,SQLite 是按 Workspace 隔离的查询投影。Document Index Repository 不解析 Markdown、Profile 或 Domain,只原子持久化文档、关系和 generation 快照。索引不得复制正文;相似度、全文关键词和相关性排序不属于当前契约,未来只能以独立 suggestions/search 契约扩展。
56
46
 
57
47
  `list` 与 `inspect` 自行保障查询新鲜度,不返回索引维护 follow-up。文件 size/mtime 只用于发现变化候选,content hash 表示内容版本;Space/Domain 声明、Project 根 Domain 映射或有效治理配置变化必须触发完整重建。Project 归属只从 Manifest 的稳定根 Domain id 与 Domain 祖先拓扑解析,不读取 Domain 声明或文档 Frontmatter 中的重复值。MOC 与其他生成文档不作为普通内容节点。
58
48
 
59
- `apply` 与 `move` 只保证目标文档操作自身有效,不触发 Maintenance。结果只在派生状态可能变化时返回一个最小 scope 的 `maintenance sync`;调用方不得固定追加 check。所有写入先生成完整 ChangeSet,再经共享 Executor 在乐观锁内提交;多文件操作失败时恢复提交前内容。
49
+ `related_docs` 可选,省略或空数组均表示无出向关联。每项为完整 Workspace 相对 POSIX Wiki 路径并保留 `.md`;拒绝别名、锚点、越界、无效目标、自引用和重复项。目标删除保留 unresolved 路径,恢复后重新解析;来源删除移除出向边。正文链接、Canvas 和相似度不参与此协议。
50
+
51
+ 索引增量提交只更新变化文档与对应来源边;目标可用性变化只更新受影响目标边。查询在 SQL 层筛选,反向查询按 target 索引,索引 generation 通过事务复核,避免旧快照覆盖新投影。
52
+
53
+ 改路径的确认必须带预览返回的 `expected_plan`,摘要覆盖来源、目标和全部引用方的预期哈希与写入内容。可捕获文件异常回滚;文件成功但索引失败返回 write_performed 与修复提示,dirty 索引在后续读取重建。不承诺跨文件写入具备断电原子性。
54
+
55
+ `apply` 与 `move` 保证目标及受影响的字段引用在同一文件变更集提交,不触发 Maintenance。结果只在派生状态可能变化时返回一个最小 scope 的 `maintenance sync`;调用方不得固定追加 check。所有写入先生成完整 ChangeSet,再经共享 Executor 在乐观锁内提交;多文件操作失败时恢复提交前内容。
60
56
 
61
57
  包内 `config.yml` 的默认 Profile 与用户级 `~/.campfire/config.yml` 覆盖共同构成当前治理契约。
62
58
 
@@ -126,6 +126,9 @@ def apply_document(
126
126
  document_type: str | None = typer.Option(None, "--type"),
127
127
  set_values: list[str] | None = typer.Option(None, "--set", help=SET_OPTION_HELP),
128
128
  expected_hash: str | None = typer.Option(None, "--expected-hash"),
129
+ expected_plan: str | None = typer.Option(
130
+ None, "--expected-plan", help="改名或移动时传入预览返回的计划摘要"
131
+ ),
129
132
  confirm: bool = typer.Option(False, "--confirm"),
130
133
  ) -> None:
131
134
  """预览或应用一篇 Profile 合法文档的创建、补丁与显式类型变更。
@@ -145,6 +148,7 @@ def apply_document(
145
148
  document_type=document_type,
146
149
  values=values,
147
150
  expected_hash=expected_hash,
151
+ expected_plan=expected_plan,
148
152
  confirm=confirm,
149
153
  )
150
154
  )
@@ -161,6 +165,9 @@ def move_document(
161
165
  set_values: list[str] | None = typer.Option(None, "--set", help=SET_OPTION_HELP),
162
166
  unset_fields: list[str] | None = typer.Option(None, "--unset"),
163
167
  expected_hash: str | None = typer.Option(None, "--expected-hash"),
168
+ expected_plan: str | None = typer.Option(
169
+ None, "--expected-plan", help="改名或移动时传入预览返回的计划摘要"
170
+ ),
164
171
  confirm: bool = typer.Option(False, "--confirm"),
165
172
  ) -> None:
166
173
  """预览或移动一篇文档,并按目标 Domain 契约更新归属与引用。"""
@@ -174,6 +181,7 @@ def move_document(
174
181
  values=values,
175
182
  unset_fields=tuple(unset_fields or []),
176
183
  expected_hash=expected_hash,
184
+ expected_plan=expected_plan,
177
185
  confirm=confirm,
178
186
  )
179
187
  )
@@ -186,6 +194,9 @@ def rename_document(
186
194
  source: str = typer.Option(..., "--path", help=PATH_OPTION_HELP),
187
195
  name: str = typer.Option(..., "--name", help="新标题;不含类型前缀和 .md 后缀"),
188
196
  expected_hash: str | None = typer.Option(None, "--expected-hash"),
197
+ expected_plan: str | None = typer.Option(
198
+ None, "--expected-plan", help="改名或移动时传入预览返回的计划摘要"
199
+ ),
189
200
  confirm: bool = typer.Option(False, "--confirm"),
190
201
  ) -> None:
191
202
  """预览或原地改名一篇文档,并同步标题、文件名与受管引用。"""
@@ -195,6 +206,7 @@ def rename_document(
195
206
  source,
196
207
  name,
197
208
  expected_hash=expected_hash,
209
+ expected_plan=expected_plan,
198
210
  confirm=confirm,
199
211
  )
200
212
  )
@@ -7,7 +7,7 @@ import typer
7
7
  from campfire_cli.app.document.repository.document_profile_repository import (
8
8
  DocumentProfileRepository,
9
9
  )
10
- from campfire_cli.app.document.service.document_profile_service import (
10
+ from campfire_cli.app.document.service.rules.document_profile_service import (
11
11
  DocumentProfileService,
12
12
  )
13
13
  from campfire_cli.app.workspace.repository.workspace_repository import SqliteWorkspaceRepository
@@ -50,6 +50,10 @@ def show_profile(ctx: typer.Context, name: str) -> None:
50
50
 
51
51
 
52
52
  @profile_cli.command("resolve")
53
- def resolve(ctx: typer.Context, path: str = typer.Option(..., "--path")) -> None:
54
- """解析一篇文档最终使用的 Profile。"""
55
- emit(invoke(lambda: service(ctx).resolve(path)))
53
+ def resolve(
54
+ ctx: typer.Context,
55
+ path: str | None = typer.Option(None, "--path", help="已有文档的 Workspace 相对或绝对路径"),
56
+ document_type: str | None = typer.Option(None, "--type", help="新建文档类型,与 --path 二选一"),
57
+ ) -> None:
58
+ """按已有文档路径或新建文档类型解析有效 Profile。"""
59
+ emit(invoke(lambda: service(ctx).resolve(path, document_type)))
@@ -7,7 +7,7 @@ import typer
7
7
  from campfire_cli.app.document.repository.document_type_repository import (
8
8
  DocumentTypeRepository,
9
9
  )
10
- from campfire_cli.app.document.service.document_type_service import DocumentTypeService
10
+ from campfire_cli.app.document.service.rules.document_type_service import DocumentTypeService
11
11
  from campfire_cli.app.workspace.repository.workspace_repository import SqliteWorkspaceRepository
12
12
  from campfire_cli.app.workspace.service.workspace_service import WorkspaceService
13
13
  from campfire_cli.common.cli_output import emit, invoke
@@ -0,0 +1,265 @@
1
+ """SQLite persistence for the rebuildable document query projection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from sqlalchemy import delete, func, select, update
8
+ from sqlalchemy.exc import SQLAlchemyError
9
+ from sqlalchemy.orm import Session
10
+
11
+ from campfire_cli.app.document.schema import (
12
+ DocumentEdgeRecord,
13
+ DocumentIndexMetadata,
14
+ DocumentIndexRecord,
15
+ )
16
+ from campfire_cli.common.database.models import Document, DocumentEdge, DocumentIndexState
17
+ from campfire_cli.common.exceptions import AppError, GovernanceBlockedError
18
+
19
+
20
+ class SqliteDocumentIndexRepository:
21
+ def __init__(self, session: Session, workspace_id: str) -> None:
22
+ self._session = session
23
+ self._workspace_id = workspace_id
24
+
25
+ def load_documents(self, paths: set[str] | None = None) -> list[DocumentIndexRecord]:
26
+ statement = select(Document).where(Document.workspace_id == self._workspace_id)
27
+ if paths is not None:
28
+ statement = statement.where(Document.path.in_(paths))
29
+ rows = self._session.scalars(statement.order_by(Document.path)).all()
30
+ return [self._document(row) for row in rows]
31
+
32
+ def load_fingerprints(self) -> list[DocumentIndexRecord]:
33
+ rows = self._session.execute(
34
+ select(
35
+ Document.path,
36
+ Document.content_hash,
37
+ Document.source_size,
38
+ Document.source_mtime_ns,
39
+ Document.queryable,
40
+ ).where(Document.workspace_id == self._workspace_id)
41
+ ).mappings()
42
+ return [DocumentIndexRecord(**row) for row in rows]
43
+
44
+ def load_edges(
45
+ self, *, source: str | None = None, target: str | None = None
46
+ ) -> list[DocumentEdgeRecord]:
47
+ statement = select(DocumentEdge).where(DocumentEdge.workspace_id == self._workspace_id)
48
+ if source is not None:
49
+ statement = statement.where(DocumentEdge.source_path == source)
50
+ if target is not None:
51
+ statement = statement.where(DocumentEdge.target_path == target)
52
+ rows = self._session.scalars(
53
+ statement.order_by(DocumentEdge.source_path, DocumentEdge.line, DocumentEdge.raw_target)
54
+ ).all()
55
+ return [self._edge(row) for row in rows]
56
+
57
+ def load_state(self) -> DocumentIndexMetadata | None:
58
+ row = self._session.get(DocumentIndexState, self._workspace_id, populate_existing=True)
59
+ if row is None:
60
+ return None
61
+ return DocumentIndexMetadata(
62
+ schema_version=row.schema_version,
63
+ parser_version=row.parser_version,
64
+ config_hash=row.config_hash,
65
+ topology_hash=row.topology_hash,
66
+ generation=row.generation,
67
+ status=row.status,
68
+ rebuilt_at=row.rebuilt_at,
69
+ indexed_at=row.indexed_at,
70
+ )
71
+
72
+ def query_documents(
73
+ self, filters: dict[str, str], limit: int | None = None
74
+ ) -> tuple[list[DocumentIndexRecord], int]:
75
+ fields = {
76
+ "project": Document.project_id,
77
+ "domain": Document.domain_id,
78
+ "type": Document.document_type,
79
+ "document_status": Document.document_status,
80
+ "task_status": Document.task_status,
81
+ }
82
+ statement = select(Document).where(
83
+ Document.workspace_id == self._workspace_id, Document.queryable.is_(True)
84
+ )
85
+ for field, value in filters.items():
86
+ statement = statement.where(fields[field] == value)
87
+ total = self._session.scalar(select(func.count()).select_from(statement.subquery())) or 0
88
+ rows = self._session.scalars(statement.order_by(Document.path).limit(limit)).all()
89
+ return [self._document(row) for row in rows], total
90
+
91
+ def counts(self) -> tuple[int, int]:
92
+ return tuple(
93
+ self._session.scalar(
94
+ select(func.count())
95
+ .select_from(model)
96
+ .where(model.workspace_id == self._workspace_id)
97
+ )
98
+ or 0
99
+ for model in (Document, DocumentEdge)
100
+ )
101
+
102
+ def mark_dirty(self) -> None:
103
+ self._session.execute(
104
+ update(DocumentIndexState)
105
+ .where(DocumentIndexState.workspace_id == self._workspace_id)
106
+ .values(status="dirty")
107
+ )
108
+ self._session.commit()
109
+
110
+ def apply_delta(
111
+ self,
112
+ documents: list[DocumentIndexRecord],
113
+ edges: list[DocumentEdgeRecord],
114
+ metadata: DocumentIndexMetadata,
115
+ deleted: set[str],
116
+ edge_sources: set[str],
117
+ affected_targets: dict[str, bool],
118
+ ) -> None:
119
+ self.replace_snapshot(
120
+ documents,
121
+ edges,
122
+ metadata,
123
+ deleted=deleted,
124
+ edge_sources=edge_sources,
125
+ affected_targets=affected_targets,
126
+ )
127
+
128
+ def replace_snapshot(
129
+ self,
130
+ documents: list[DocumentIndexRecord],
131
+ edges: list[DocumentEdgeRecord],
132
+ metadata: DocumentIndexMetadata,
133
+ *,
134
+ deleted: set[str] | None = None,
135
+ edge_sources: set[str] | None = None,
136
+ affected_targets: dict[str, bool] | None = None,
137
+ ) -> None:
138
+ try:
139
+ if metadata.generation > 1:
140
+ advanced = self._session.execute(
141
+ update(DocumentIndexState)
142
+ .where(
143
+ DocumentIndexState.workspace_id == self._workspace_id,
144
+ DocumentIndexState.generation == metadata.generation - 1,
145
+ )
146
+ .values(generation=metadata.generation)
147
+ )
148
+ if advanced.rowcount != 1:
149
+ raise GovernanceBlockedError("索引已被其他操作更新,请重新查询")
150
+ edge_delete = delete(DocumentEdge).where(
151
+ DocumentEdge.workspace_id == self._workspace_id
152
+ )
153
+ document_delete = delete(Document).where(Document.workspace_id == self._workspace_id)
154
+ if deleted is not None:
155
+ edge_delete = edge_delete.where(
156
+ DocumentEdge.source_path.in_((edge_sources or set()) | deleted)
157
+ )
158
+ document_delete = document_delete.where(
159
+ Document.path.in_(deleted | {item.path for item in documents})
160
+ )
161
+ self._session.execute(edge_delete)
162
+ self._session.execute(document_delete)
163
+ for target, exists in (affected_targets or {}).items():
164
+ self._session.execute(
165
+ update(DocumentEdge)
166
+ .where(
167
+ DocumentEdge.workspace_id == self._workspace_id,
168
+ DocumentEdge.target_path == target,
169
+ DocumentEdge.resolution.in_(["resolved", "missing"]),
170
+ )
171
+ .values(resolution="resolved" if exists else "missing")
172
+ )
173
+ self._session.add_all(
174
+ [
175
+ Document(
176
+ workspace_id=self._workspace_id,
177
+ path=item.path,
178
+ content_hash=item.content_hash,
179
+ name=item.name,
180
+ document_type=item.document_type,
181
+ domain_id=item.domain_id,
182
+ project_id=item.project_id,
183
+ document_status=item.document_status,
184
+ lifecycle=item.lifecycle,
185
+ task_status=item.task_status,
186
+ priority=item.priority,
187
+ assignee_json=json.dumps(item.assignee, ensure_ascii=False),
188
+ due=item.due,
189
+ source_updated=item.source_updated,
190
+ source_size=item.source_size,
191
+ source_mtime_ns=item.source_mtime_ns,
192
+ queryable=item.queryable,
193
+ exists=True,
194
+ )
195
+ for item in documents
196
+ ]
197
+ )
198
+ self._session.add_all(
199
+ [
200
+ DocumentEdge(
201
+ workspace_id=self._workspace_id,
202
+ source_path=item.source_path,
203
+ target_path=item.target_path,
204
+ raw_target=item.raw_target,
205
+ relation_type=item.relation_type,
206
+ resolution=item.resolution,
207
+ candidates_json=json.dumps(item.candidates, ensure_ascii=False),
208
+ line=item.line,
209
+ )
210
+ for item in edges
211
+ ]
212
+ )
213
+ row = self._session.get(DocumentIndexState, self._workspace_id)
214
+ if row is None:
215
+ row = DocumentIndexState(workspace_id=self._workspace_id)
216
+ self._session.add(row)
217
+ row.schema_version = metadata.schema_version
218
+ row.parser_version = metadata.parser_version
219
+ row.config_hash = metadata.config_hash
220
+ row.topology_hash = metadata.topology_hash
221
+ row.generation = metadata.generation
222
+ row.status = metadata.status
223
+ row.rebuilt_at = metadata.rebuilt_at
224
+ row.indexed_at = metadata.indexed_at
225
+ self._session.commit()
226
+ except SQLAlchemyError as exc:
227
+ self._session.rollback()
228
+ raise AppError("文档索引提交失败,已回滚", code="document-index-write-failed") from exc
229
+ except Exception:
230
+ self._session.rollback()
231
+ raise
232
+
233
+ @staticmethod
234
+ def _document(row: Document) -> DocumentIndexRecord:
235
+ return DocumentIndexRecord(
236
+ path=row.path,
237
+ content_hash=row.content_hash,
238
+ name=row.name,
239
+ document_type=row.document_type,
240
+ domain_id=row.domain_id,
241
+ project_id=row.project_id,
242
+ document_status=row.document_status,
243
+ lifecycle=row.lifecycle,
244
+ task_status=row.task_status,
245
+ priority=row.priority,
246
+ assignee=json.loads(row.assignee_json or "[]"),
247
+ due=row.due,
248
+ source_updated=row.source_updated,
249
+ source_size=row.source_size or 0,
250
+ source_mtime_ns=row.source_mtime_ns or 0,
251
+ queryable=row.queryable,
252
+ indexed_at=row.indexed_at,
253
+ )
254
+
255
+ @staticmethod
256
+ def _edge(row: DocumentEdge) -> DocumentEdgeRecord:
257
+ return DocumentEdgeRecord(
258
+ source_path=row.source_path,
259
+ target_path=row.target_path,
260
+ raw_target=row.raw_target,
261
+ relation_type=row.relation_type,
262
+ resolution=row.resolution,
263
+ candidates=json.loads(row.candidates_json or "[]"),
264
+ line=row.line,
265
+ )
@@ -13,6 +13,7 @@ class DocumentApplyRequest(BaseModel):
13
13
  document_type: str | None = None
14
14
  values: dict[str, str] = Field(default_factory=dict)
15
15
  expected_hash: str | None = None
16
+ expected_plan: str | None = None
16
17
  confirm: bool = False
17
18
 
18
19
 
@@ -26,6 +27,7 @@ class DocumentApplyResult(BaseModel):
26
27
  normalization: list[dict[str, Any]] = Field(default_factory=list)
27
28
  profile: str
28
29
  expected_hash: str
30
+ expected_plan: str | None = None
29
31
  write_performed: bool = False
30
32
  updated_references: list[str] = Field(default_factory=list)
31
33
  issues: list[dict[str, Any]] = Field(default_factory=list)
@@ -42,6 +44,7 @@ class DocumentMoveResult(BaseModel):
42
44
  source_domain: str | None = None
43
45
  target_domain: str | None = None
44
46
  profile: str | None = None
47
+ expected_plan: str | None = None
45
48
  write_performed: bool = False
46
49
  updated_references: list[str] = Field(default_factory=list)
47
50
  frontmatter_changes: dict[str, Any] = Field(default_factory=dict)
@@ -74,14 +77,8 @@ class DocumentEdgeRecord(BaseModel):
74
77
  source_path: str
75
78
  target_path: str | None = None
76
79
  raw_target: str
77
- relation_type: Literal[
78
- "declared-related",
79
- "declared-superseded-by",
80
- "wikilink",
81
- "markdown-link",
82
- "embed",
83
- ]
84
- resolution: Literal["resolved", "missing", "ambiguous"]
80
+ relation_type: Literal["related_docs"] = "related_docs"
81
+ resolution: Literal["resolved", "missing", "invalid", "self"]
85
82
  candidates: list[str] = Field(default_factory=list)
86
83
  line: int | None = None
87
84