docspan 0.3.0__tar.gz → 0.4.0__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 (198) hide show
  1. docspan-0.4.0/.release-please-manifest.json +3 -0
  2. {docspan-0.3.0 → docspan-0.4.0}/CHANGELOG.md +27 -0
  3. {docspan-0.3.0 → docspan-0.4.0}/PKG-INFO +6 -3
  4. {docspan-0.3.0 → docspan-0.4.0}/README.md +4 -2
  5. {docspan-0.3.0 → docspan-0.4.0}/docs/backends/confluence.md +1 -0
  6. docspan-0.4.0/docs/backends/google-docs.md +67 -0
  7. {docspan-0.3.0 → docspan-0.4.0}/docs/index.md +3 -1
  8. docspan-0.4.0/project_plans/wedding-planning-workflow/decisions/ADR-003-no-comment-anchor-migration.md +128 -0
  9. {docspan-0.3.0 → docspan-0.4.0}/pyproject.toml +1 -0
  10. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/base.py +17 -2
  11. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/backend.py +21 -1
  12. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/models.py +5 -1
  13. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/backend.py +342 -53
  14. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/client.py +72 -1
  15. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/converter.py +6 -3
  16. docspan-0.4.0/src/docspan/backends/google_docs/cross_doc_links.py +286 -0
  17. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/docs_request_builder.py +822 -264
  18. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/docs_structure_parser.py +95 -3
  19. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/heading_anchors.py +26 -11
  20. docspan-0.4.0/src/docspan/backends/google_docs/image_source.py +260 -0
  21. docspan-0.4.0/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +603 -0
  22. docspan-0.4.0/src/docspan/backends/google_docs/mermaid_renderer.py +100 -0
  23. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/nodes_to_markdown.py +236 -65
  24. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/projection.py +2 -1
  25. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/push_preview.py +138 -28
  26. docspan-0.4.0/src/docspan/backends/google_docs/registry.py +67 -0
  27. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/tabs.py +46 -1
  28. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/cli/main.py +165 -2
  29. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/config.py +95 -1
  30. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/core/orchestrator.py +23 -1
  31. docspan-0.4.0/tests/__init__.py +0 -0
  32. {docspan-0.3.0 → docspan-0.4.0}/tests/test_cli.py +216 -2
  33. {docspan-0.3.0 → docspan-0.4.0}/tests/test_code_block_granularity.py +255 -22
  34. {docspan-0.3.0 → docspan-0.4.0}/tests/test_config.py +79 -2
  35. docspan-0.4.0/tests/test_confluence_backend.py +47 -0
  36. docspan-0.4.0/tests/test_confluence_mermaid_push_pipeline.py +101 -0
  37. docspan-0.4.0/tests/test_content_key_pooling_performance.py +115 -0
  38. {docspan-0.3.0 → docspan-0.4.0}/tests/test_converter.py +18 -0
  39. docspan-0.4.0/tests/test_cross_doc_link_issues.py +135 -0
  40. docspan-0.4.0/tests/test_cross_doc_links.py +310 -0
  41. docspan-0.4.0/tests/test_cross_doc_links_backend.py +481 -0
  42. {docspan-0.3.0 → docspan-0.4.0}/tests/test_docs_request_builder.py +556 -5
  43. docspan-0.4.0/tests/test_gdocs_images.py +427 -0
  44. docspan-0.4.0/tests/test_gdocs_mermaid.py +119 -0
  45. {docspan-0.3.0 → docspan-0.4.0}/tests/test_gdocs_push_pipeline.py +17 -0
  46. {docspan-0.3.0 → docspan-0.4.0}/tests/test_gdocs_tables_and_styles.py +70 -0
  47. {docspan-0.3.0 → docspan-0.4.0}/tests/test_google_docs_backend.py +862 -0
  48. {docspan-0.3.0 → docspan-0.4.0}/tests/test_google_onboarding.py +8 -2
  49. {docspan-0.3.0 → docspan-0.4.0}/tests/test_heading_anchors.py +166 -0
  50. docspan-0.4.0/tests/test_heading_identity.py +982 -0
  51. {docspan-0.3.0 → docspan-0.4.0}/tests/test_markdown_to_paragraph_parser.py +8 -0
  52. {docspan-0.3.0 → docspan-0.4.0}/tests/test_nodes_to_markdown.py +112 -0
  53. {docspan-0.3.0 → docspan-0.4.0}/tests/test_orchestrator.py +4 -1
  54. docspan-0.4.0/tests/test_push_preview.py +689 -0
  55. docspan-0.4.0/tests/test_registry.py +93 -0
  56. docspan-0.4.0/tests/test_restyle_destruction_rate.py +149 -0
  57. {docspan-0.3.0 → docspan-0.4.0}/tests/test_table_cell_spans.py +153 -22
  58. {docspan-0.3.0 → docspan-0.4.0}/tests/test_tabs.py +130 -0
  59. {docspan-0.3.0 → docspan-0.4.0}/uv.lock +85 -74
  60. docspan-0.3.0/.release-please-manifest.json +0 -3
  61. docspan-0.3.0/docs/backends/google-docs.md +0 -67
  62. docspan-0.3.0/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +0 -357
  63. docspan-0.3.0/tests/test_heading_identity.py +0 -456
  64. docspan-0.3.0/tests/test_push_preview.py +0 -338
  65. {docspan-0.3.0 → docspan-0.4.0}/.github/workflows/ci.yml +0 -0
  66. {docspan-0.3.0 → docspan-0.4.0}/.github/workflows/publish.yml +0 -0
  67. {docspan-0.3.0 → docspan-0.4.0}/.github/workflows/release-please.yml +0 -0
  68. {docspan-0.3.0 → docspan-0.4.0}/.gitignore +0 -0
  69. {docspan-0.3.0 → docspan-0.4.0}/.mypy-error-baseline +0 -0
  70. {docspan-0.3.0 → docspan-0.4.0}/CONTRIBUTING.md +0 -0
  71. {docspan-0.3.0 → docspan-0.4.0}/Procfile +0 -0
  72. {docspan-0.3.0 → docspan-0.4.0}/RAILWAY_SETUP.md +0 -0
  73. /docspan-0.3.0/src/docspan/__main__.py → /docspan-0.4.0/doc.md +0 -0
  74. {docspan-0.3.0 → docspan-0.4.0}/docs/commands.md +0 -0
  75. {docspan-0.3.0 → docspan-0.4.0}/docs/configuration.md +0 -0
  76. {docspan-0.3.0 → docspan-0.4.0}/docs/contributing.md +0 -0
  77. {docspan-0.3.0 → docspan-0.4.0}/docs/install.md +0 -0
  78. {docspan-0.3.0 → docspan-0.4.0}/docspan.yaml.example +0 -0
  79. {docspan-0.3.0 → docspan-0.4.0}/markgate.yaml.example +0 -0
  80. {docspan-0.3.0 → docspan-0.4.0}/mkdocs.yml +0 -0
  81. {docspan-0.3.0 → docspan-0.4.0}/modules/__init__.py +0 -0
  82. {docspan-0.3.0 → docspan-0.4.0}/modules/auth.py +0 -0
  83. {docspan-0.3.0 → docspan-0.4.0}/modules/conflict_handler.py +0 -0
  84. {docspan-0.3.0 → docspan-0.4.0}/modules/converter.py +0 -0
  85. {docspan-0.3.0 → docspan-0.4.0}/modules/gdrive_client.py +0 -0
  86. {docspan-0.3.0 → docspan-0.4.0}/modules/sync_engine.py +0 -0
  87. {docspan-0.3.0 → docspan-0.4.0}/project_plans/bidirectional-comments/plan.md +0 -0
  88. {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/adversarial-review.md +0 -0
  89. {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/plan.md +0 -0
  90. {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/release-checklist.md +0 -0
  91. {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/validation.md +0 -0
  92. {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/requirements.md +0 -0
  93. {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/research/architecture.md +0 -0
  94. {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/research/features.md +0 -0
  95. {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/research/google-docs-push.md +0 -0
  96. {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/research/pitfalls.md +0 -0
  97. {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/research/stack.md +0 -0
  98. {docspan-0.3.0 → docspan-0.4.0}/project_plans/gdocs-tables-inline-styles/plan.md +0 -0
  99. {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/decisions/ADR-001-merge3-dependency.md +0 -0
  100. {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/decisions/ADR-002-base-content-sidecar-store.md +0 -0
  101. {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/implementation/adversarial-review.md +0 -0
  102. {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/implementation/plan.md +0 -0
  103. {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/implementation/validation.md +0 -0
  104. {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/requirements.md +0 -0
  105. {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/research/architecture.md +0 -0
  106. {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/research/features.md +0 -0
  107. {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/research/pitfalls.md +0 -0
  108. {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/research/stack.md +0 -0
  109. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/decisions/ADR-001-checklist-state-as-literal-text.md +0 -0
  110. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/decisions/ADR-002-comment-risk-flagging-not-anchor-preservation.md +0 -0
  111. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/feature-gap-report.md +0 -0
  112. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/adversarial-review.md +0 -0
  113. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/architecture-review.md +0 -0
  114. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/plan.md +0 -0
  115. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/pre-mortem.md +0 -0
  116. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/validation.md +0 -0
  117. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/requirements.md +0 -0
  118. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/architecture.md +0 -0
  119. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/build-vs-buy.md +0 -0
  120. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/features.md +0 -0
  121. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/pitfalls.md +0 -0
  122. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/stack.md +0 -0
  123. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/ux.md +0 -0
  124. {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/workflow-runbook.md +0 -0
  125. {docspan-0.3.0 → docspan-0.4.0}/release-please-config.json +0 -0
  126. {docspan-0.3.0 → docspan-0.4.0}/requirements.txt +0 -0
  127. {docspan-0.3.0 → docspan-0.4.0}/runtime.txt +0 -0
  128. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/__init__.py +0 -0
  129. /docspan-0.3.0/src/docspan/backends/confluence/__init__.py → /docspan-0.4.0/src/docspan/__main__.py +0 -0
  130. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/__init__.py +0 -0
  131. {docspan-0.3.0/src/docspan/backends/confluence/services → docspan-0.4.0/src/docspan/backends/confluence}/__init__.py +0 -0
  132. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/__init__.py +0 -0
  133. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/comparator.py +0 -0
  134. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/converter.py +0 -0
  135. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/converters.py +0 -0
  136. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/interfaces.py +0 -0
  137. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/nodes.py +0 -0
  138. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/parser.py +0 -0
  139. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/validators.py +0 -0
  140. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/visitors.py +0 -0
  141. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/client.py +0 -0
  142. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/__init__.py +0 -0
  143. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/loader.py +0 -0
  144. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/validation.py +0 -0
  145. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/__init__.py +0 -0
  146. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/ast.py +0 -0
  147. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/__init__.py +0 -0
  148. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/frontmatter.py +0 -0
  149. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/mermaid.py +0 -0
  150. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/wikilinks.py +0 -0
  151. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/inline_parser.py +0 -0
  152. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/parser.py +0 -0
  153. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/__init__.py +0 -0
  154. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/markdown_file.py +0 -0
  155. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/page.py +0 -0
  156. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/path_utils.py +0 -0
  157. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/results.py +0 -0
  158. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/sync_status.py +0 -0
  159. {docspan-0.3.0/src/docspan/backends/google_docs → docspan-0.4.0/src/docspan/backends/confluence/services}/__init__.py +0 -0
  160. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/__init__.py +0 -0
  161. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/attachment_client.py +0 -0
  162. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/base_client.py +0 -0
  163. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/client.py +0 -0
  164. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/comment_client.py +0 -0
  165. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/crawler.py +0 -0
  166. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/label_client.py +0 -0
  167. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/page_client.py +0 -0
  168. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/space_client.py +0 -0
  169. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/url_parser.py +0 -0
  170. {docspan-0.3.0/src/docspan/cli → docspan-0.4.0/src/docspan/backends/google_docs}/__init__.py +0 -0
  171. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/auth.py +0 -0
  172. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/checkbox_state.py +0 -0
  173. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/comments.py +0 -0
  174. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/onboarding.py +0 -0
  175. {docspan-0.3.0/tests → docspan-0.4.0/src/docspan/cli}/__init__.py +0 -0
  176. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/core/__init__.py +0 -0
  177. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/core/merge.py +0 -0
  178. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/core/paths.py +0 -0
  179. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/core/state.py +0 -0
  180. {docspan-0.3.0 → docspan-0.4.0}/src/docspan/core/xdg.py +0 -0
  181. {docspan-0.3.0 → docspan-0.4.0}/sync.py +0 -0
  182. {docspan-0.3.0 → docspan-0.4.0}/terraform/gcp/README.md +0 -0
  183. {docspan-0.3.0 → docspan-0.4.0}/terraform/gcp/main.tf +0 -0
  184. {docspan-0.3.0 → docspan-0.4.0}/terraform/gcp/outputs.tf +0 -0
  185. {docspan-0.3.0 → docspan-0.4.0}/terraform/gcp/variables.tf +0 -0
  186. {docspan-0.3.0 → docspan-0.4.0}/terraform/main.tf +0 -0
  187. {docspan-0.3.0 → docspan-0.4.0}/terraform/variables.tf +0 -0
  188. {docspan-0.3.0 → docspan-0.4.0}/tests/conftest.py +0 -0
  189. {docspan-0.3.0 → docspan-0.4.0}/tests/fixtures/github_slugger_vectors.json +0 -0
  190. {docspan-0.3.0 → docspan-0.4.0}/tests/test_checkbox_state.py +0 -0
  191. {docspan-0.3.0 → docspan-0.4.0}/tests/test_conflict_resolution.py +0 -0
  192. {docspan-0.3.0 → docspan-0.4.0}/tests/test_docs_structure_parser.py +0 -0
  193. {docspan-0.3.0 → docspan-0.4.0}/tests/test_google_comments.py +0 -0
  194. {docspan-0.3.0 → docspan-0.4.0}/tests/test_google_oauth.py +0 -0
  195. {docspan-0.3.0 → docspan-0.4.0}/tests/test_merge.py +0 -0
  196. {docspan-0.3.0 → docspan-0.4.0}/tests/test_span_trailing_newline.py +0 -0
  197. {docspan-0.3.0 → docspan-0.4.0}/tests/test_state.py +0 -0
  198. {docspan-0.3.0 → docspan-0.4.0}/tests/test_xdg_central_config.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.0"
3
+ }
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0](https://github.com/tstapler/docspan/compare/docspan-v0.3.0...docspan-v0.4.0) (2026-08-13)
9
+
10
+
11
+ ### Features
12
+
13
+ * **docspan:** add docspan map command and push auto-create for unmapped files ([#80](https://github.com/tstapler/docspan/issues/80)) ([aa4258b](https://github.com/tstapler/docspan/commit/aa4258b3ff381c3e6afc389ad55c426abb5850ba))
14
+ * **google-docs:** add inline image push/pull support ([#101](https://github.com/tstapler/docspan/issues/101)) ([e5256af](https://github.com/tstapler/docspan/commit/e5256afa18076051b891f44511786e4f2750a489))
15
+ * **google-docs:** render mermaid diagrams as inline PNGs on push ([9298d1b](https://github.com/tstapler/docspan/commit/9298d1b0c645231056571a2c5afaa296e14d751b))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **config:** round-trip YAML comments on save_config writes ([780ddcc](https://github.com/tstapler/docspan/commit/780ddcc18b760f184627eb9c9a2b870249c6ed51))
21
+ * **confluence:** confirm mermaid render pipeline is a stub, not a rasterizer ([#94](https://github.com/tstapler/docspan/issues/94)) ([fb5ca80](https://github.com/tstapler/docspan/commit/fb5ca8002e91e050447766d7e498ff30eadae9dc))
22
+ * **google-docs:** bound difflib's cubic-ish blowup on duplicate-heavy documents ([#84](https://github.com/tstapler/docspan/issues/84)) ([834df71](https://github.com/tstapler/docspan/commit/834df71d2737b09ee68b14dd1f509bc3928249da))
23
+ * **google-docs:** distinguish delete-and-reinsert churn from real removal in push preview ([#86](https://github.com/tstapler/docspan/issues/86)) ([052b64d](https://github.com/tstapler/docspan/commit/052b64d591bc88ab6962e5173c2494b2fedab1c3))
24
+ * **google-docs:** escape backticks in monospace spans on both pull paths ([#103](https://github.com/tstapler/docspan/issues/103)) ([74d007d](https://github.com/tstapler/docspan/commit/74d007d162d511641650d2afb8b9f8b482c3e48e))
25
+ * **google-docs:** land PR [#70](https://github.com/tstapler/docspan/issues/70) restyle repair, verify AC0-8 (issue [#52](https://github.com/tstapler/docspan/issues/52)) ([#99](https://github.com/tstapler/docspan/issues/99)) ([8203c93](https://github.com/tstapler/docspan/commit/8203c93567a3d962bb3bb5754338298260f610aa))
26
+ * **google-docs:** order same-anchor insert groups after restyle/delete groups ([#83](https://github.com/tstapler/docspan/issues/83)) ([b8eace0](https://github.com/tstapler/docspan/commit/b8eace0505f7196eeb5bfe659f2d284a4b8c07ad))
27
+ * **google-docs:** render multi-paragraph table cells as HTML tables ([#79](https://github.com/tstapler/docspan/issues/79)) ([45e072d](https://github.com/tstapler/docspan/commit/45e072de2ae845443d6b5862a6c70234a55fb501))
28
+ * **google-docs:** resolve cross-tab heading anchors on push ([#102](https://github.com/tstapler/docspan/issues/102)) ([c1be541](https://github.com/tstapler/docspan/commit/c1be541d7e8d227369367ac8d99700405ac31195))
29
+ * **google-docs:** resolve relative cross-document markdown links to target Google Doc URLs ([#98](https://github.com/tstapler/docspan/issues/98)) ([27e7f15](https://github.com/tstapler/docspan/commit/27e7f1551549d7bff9d64b5e43abfd09f4f19e36))
30
+ * **google-docs:** richer at-risk-comment warning, no anchor migration ([#92](https://github.com/tstapler/docspan/issues/92)) ([#95](https://github.com/tstapler/docspan/issues/95)) ([be854db](https://github.com/tstapler/docspan/commit/be854db8cc4dd5a8252685847455e51d444decee))
31
+ * **google-docs:** split/preserve fenced code blocks in list items and blockquotes ([#87](https://github.com/tstapler/docspan/issues/87)) ([0a01f9f](https://github.com/tstapler/docspan/commit/0a01f9f6c927489c9234049aed56b4fe8c895b58))
32
+ * **google-docs:** stop force-push from corrupting tab-scoped checkbox docs ([#97](https://github.com/tstapler/docspan/issues/97)) ([63ab43b](https://github.com/tstapler/docspan/commit/63ab43bc755dad67ce22d516b6ea4f1eaeba9c3e))
33
+ * **google-docs:** stop replace branch from duplicating the doc-end-clamped newline ([#85](https://github.com/tstapler/docspan/issues/85)) ([68f0de9](https://github.com/tstapler/docspan/commit/68f0de985be5b13df0d4e1b3c4b0177a2e0eced8))
34
+
8
35
  ## [0.3.0](https://github.com/tstapler/docspan/compare/docspan-v0.2.0...docspan-v0.3.0) (2026-08-11)
9
36
 
10
37
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: docspan
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Push and pull markdown to Google Docs and Confluence from a single CLI
5
5
  Project-URL: Homepage, https://github.com/tstapler/docspan
6
6
  Project-URL: Repository, https://github.com/tstapler/docspan
@@ -36,6 +36,7 @@ Requires-Dist: python-dateutil>=2.8.2
36
36
  Requires-Dist: pyyaml>=6.0
37
37
  Requires-Dist: requests>=2.25.0
38
38
  Requires-Dist: rich>=13.0.0
39
+ Requires-Dist: ruamel-yaml>=0.18.0
39
40
  Requires-Dist: typer>=0.9.0
40
41
  Provides-Extra: dev
41
42
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
@@ -306,8 +307,10 @@ docspan generates these files in your project directory after first sync:
306
307
  > [!NOTE]
307
308
  > **Known limitations in v0.1.0**
308
309
  >
309
- > - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved). `docspan push --dry-run` and a default fail-closed `--force`-gated block now warn before this happens — it is still not prevented.
310
- > - Push: no image support — local images cannot be pushed to Google Docs or Confluence
310
+ > - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved). `docspan push --dry-run` and a default fail-closed `--force`-gated block now warn before this happens, naming every at-risk comment (id, author, quoted snippet) per flagged paragraph — it is still not prevented. Re-anchoring or recreating the comment was investigated and rejected: neither `comments().update` nor `comments().create` can produce a comment Google Docs' own UI renders as anchored to arbitrary text (see [ADR-003](project_plans/wedding-planning-workflow/decisions/ADR-003-no-comment-anchor-migration.md)), so no migration ships.
311
+ > - Google Docs: images push and pull (`![alt](./local.png)` uploads to Drive; `https://` URLs are referenced directly). SVG, missing, and oversized (>50MB) images are reported as warnings rather than blocking the push. An image mixed into a paragraph alongside running text is not supported — only a standalone `![alt](src)` on its own line
312
+ > - Google Docs: a pulled image's markdown link is Google's `contentUri` for that embedded object, which Google's API docs say may change over time even when the image itself hasn't. docspan's push diff keys image identity on `alt`/size rather than this URI, so a rotated URI alone will not cause a paragraph to be needlessly deleted and reinserted (and its comments lost) on the next push — but the URI written into your markdown file can itself go stale between pulls, and a stale-but-unchanged URI line can still show up as a one-sided edit in `docspan conflicts resolve`'s three-way diff even though nothing meaningful changed
313
+ > - Push: no image support for Confluence — local images cannot be pushed
311
314
  > - Push: no table support — markdown tables are not rendered in Google Docs
312
315
  > - Confluence: requires an Atlassian API token; no OAuth flow
313
316
  > - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
@@ -255,8 +255,10 @@ docspan generates these files in your project directory after first sync:
255
255
  > [!NOTE]
256
256
  > **Known limitations in v0.1.0**
257
257
  >
258
- > - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved). `docspan push --dry-run` and a default fail-closed `--force`-gated block now warn before this happens — it is still not prevented.
259
- > - Push: no image support — local images cannot be pushed to Google Docs or Confluence
258
+ > - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved). `docspan push --dry-run` and a default fail-closed `--force`-gated block now warn before this happens, naming every at-risk comment (id, author, quoted snippet) per flagged paragraph — it is still not prevented. Re-anchoring or recreating the comment was investigated and rejected: neither `comments().update` nor `comments().create` can produce a comment Google Docs' own UI renders as anchored to arbitrary text (see [ADR-003](project_plans/wedding-planning-workflow/decisions/ADR-003-no-comment-anchor-migration.md)), so no migration ships.
259
+ > - Google Docs: images push and pull (`![alt](./local.png)` uploads to Drive; `https://` URLs are referenced directly). SVG, missing, and oversized (>50MB) images are reported as warnings rather than blocking the push. An image mixed into a paragraph alongside running text is not supported — only a standalone `![alt](src)` on its own line
260
+ > - Google Docs: a pulled image's markdown link is Google's `contentUri` for that embedded object, which Google's API docs say may change over time even when the image itself hasn't. docspan's push diff keys image identity on `alt`/size rather than this URI, so a rotated URI alone will not cause a paragraph to be needlessly deleted and reinserted (and its comments lost) on the next push — but the URI written into your markdown file can itself go stale between pulls, and a stale-but-unchanged URI line can still show up as a one-sided edit in `docspan conflicts resolve`'s three-way diff even though nothing meaningful changed
261
+ > - Push: no image support for Confluence — local images cannot be pushed
260
262
  > - Push: no table support — markdown tables are not rendered in Google Docs
261
263
  > - Confluence: requires an Atlassian API token; no OAuth flow
262
264
  > - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
@@ -65,3 +65,4 @@ export CONFLUENCE_API_TOKEN=your-token
65
65
  - **Comment sidecar is informational only**: Comments pulled from Confluence are written to `{file}.comments.md` but cannot be pushed back via docspan.
66
66
  - **Push replaces page content**: The full page body is replaced on every push. Inline comment positions in Confluence may shift after a push.
67
67
  - **Complex macros not preserved faithfully**: Confluence macros (status, panels, expand, etc.) are converted to approximate markdown equivalents on pull and may not round-trip cleanly on push.
68
+ - **Mermaid diagrams are not rendered**: a ` ```mermaid ` fence is pushed as a plain ADF code block (language `mermaid`), i.e. the raw diagram source as visible text — not an image, and not Confluence's native mermaid macro. `render_mermaid_diagrams` in `PublishConfig` currently has no effect on this: the code path that would rasterize a diagram (`MermaidParser.render_diagram()` in `markdown/extensions/mermaid.py`) is a stub that never runs — it's dead code, unreachable from the real parse pipeline (`markdown/parser.py` builds `MermaidNode` directly). Fixing this requires a real renderer (e.g. `mermaid-cli` or a hosted rendering service) wired to upload the result as a Confluence attachment before ADF conversion; tracked as follow-up work, not yet implemented.
@@ -0,0 +1,67 @@
1
+ # Google Docs Backend
2
+
3
+ ## How it works
4
+
5
+ The Google Docs backend authenticates either via a Google service account JSON key or via per-user OAuth (an `InstalledAppFlow` that acts as you, similar to `gws`) — whichever `markgate.yaml` configures (`credentials_path` for the service account, `oauth_client_secret_path` for OAuth). Push uses a paragraph-level structural diff that computes the minimal set of `batchUpdate` requests needed to transform the current document into the target content. This approach preserves comments attached to paragraphs that have not changed. Pull exports the Google Doc as HTML and converts it to markdown.
6
+
7
+ ## Auth Setup
8
+
9
+ Run `docspan auth setup google_docs` to see setup instructions.
10
+
11
+ ```
12
+ Google Docs Auth Setup
13
+ ========================================
14
+ Run this in an interactive terminal for a guided setup, or configure manually:
15
+
16
+ Per-user OAuth (recommended — acts as you, like gws):
17
+ 1. Create an OAuth client (Desktop app); download client_secret.json
18
+ 2. docspan auth setup google_docs --oauth --client-secret /path/to/client_secret.json
19
+ (or set backends.google_docs.oauth_client_secret_path in markgate.yaml)
20
+
21
+ Service account (automation):
22
+ 1. Create a service account + JSON key; enable the Docs & Drive APIs
23
+ 2. Share your docs with the service-account email
24
+ 3. Set credentials_path in markgate.yaml (or ACCOUNT_A_CREDENTIALS_PATH env)
25
+ ```
26
+
27
+ Service account credentials can also be provided inline via `ACCOUNT_A_CREDENTIALS` (the JSON itself, not a path) instead of `ACCOUNT_A_CREDENTIALS_PATH`.
28
+
29
+ ## Required Scopes
30
+
31
+ Every credential path (`GoogleAuthenticator`, `OAuthAuthenticator`) requests the same read/write scopes (`PUSH_SCOPES`, aliased as `SCOPES`/`DEFAULT_SCOPES`), whether the operation is push or pull:
32
+
33
+ - `https://www.googleapis.com/auth/documents` — read and write Google Docs
34
+ - `https://www.googleapis.com/auth/drive` — read/write Drive (comment reads/writes, file metadata; not just export)
35
+ - `https://www.googleapis.com/auth/spreadsheets.readonly` — read Sheets embedded/linked in a Doc
36
+
37
+ `auth.py` also defines a narrower read-only `PULL_SCOPES`, but nothing in the codebase wires it up today — pull requests the same full grant as push, not a readonly subset. Comment reads/writes reuse this same grant too; no separate scope is added for them.
38
+
39
+ ## `markgate.yaml` Example
40
+
41
+ ```yaml
42
+ backends:
43
+ google_docs:
44
+ credentials_path: /path/to/service-account.json
45
+ # token_path: .markgate/google_token.json # default, rarely changed
46
+
47
+ mappings:
48
+ - local: docs/design-doc.md
49
+ backend: google_docs
50
+ remote_id: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74O
51
+ direction: both
52
+ ```
53
+
54
+ ## Limitations
55
+
56
+ !!! warning
57
+ - **Comments destroyed on push for edited paragraphs**: The structural diff preserves comments on unchanged paragraphs, but any paragraph that is deleted and reinserted loses its comments. This is a known v0.1.0 limitation.
58
+ - **Images push and pull**: `![alt](./local.png)` uploads the local file to Drive and references it by URI; an `https://` URL is referenced directly, bypassing upload. Only a standalone image on its own line is supported — one mixed into a paragraph alongside running text is left as plain text. Missing files, files over 50MB, and unsupported formats (SVG) are reported as push warnings rather than blocking the write or crashing.
59
+ - **Mermaid diagrams push as rendered PNGs**: a fenced ` ```mermaid ` block is rendered to a raster PNG (via `mermaid-cli`/`mmdc`, shelled out to — install with `npm install -g @mermaid-js/mermaid-cli`, or it's fetched on demand through `npx`) and pushed as an inline image, since `insertInlineImage` has no native mermaid or SVG support. A render failure (missing Node.js/mermaid-cli, invalid diagram syntax, timeout) is reported as a push warning, not a crash. There is no pull-side reconstruction — a mermaid diagram round-trips back to markdown as a plain image reference, not a ` ```mermaid ` fence.
60
+ - **Pulled image URIs can go stale**: a pulled `![alt](src)` link is Google's `contentUri` for that embedded object, which Google's API docs say may change over time even when the image itself is unchanged. The push structural diff keys image identity on `alt`/width/height, not this URI, so a rotated `contentUri` alone will not cause the paragraph to be deleted and reinserted (which would destroy any comment anchored to it) — but the stale URI persisted in your markdown file can still surface as a one-sided edit in `docspan conflicts resolve`'s three-way diff.
61
+ - **Table cells hold one paragraph**: a markdown table cell is pushed as a single
62
+ paragraph, and inline formatting inside it (bold, monospace, links, internal
63
+ `#anchor` references) is applied on the second pass. Two limits follow: a cell
64
+ whose content spans more than one paragraph in the Doc cannot be styled, and a
65
+ table created by the current push gets its cell styling on the *next* push —
66
+ docspan reports both rather than failing silently.
67
+ - **Rate limiting**: The Google Docs API allows 300 requests per minute per project. Large documents with many changed paragraphs may trigger rate limit errors.
@@ -57,7 +57,9 @@ See the [Install](install.md) page for full auth setup instructions and the [Com
57
57
 
58
58
  !!! warning "Known limitations in v0.1.0"
59
59
  - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved)
60
- - Push: no image support — local images cannot be pushed to Google Docs or Confluence
60
+ - Google Docs: images push and pull (local files upload to Drive; `https://` URLs are referenced directly)
61
+ - Google Docs: a pulled image's markdown link is Google's `contentUri`, which can change over time even when the image hasn't — push doesn't misdetect this as a real change, but the stale URI in your local file can still show up as a one-sided edit during conflict resolution
62
+ - Push: no image support for Confluence — local images cannot be pushed
61
63
  - Push: no table support — markdown tables are not rendered in Google Docs
62
64
  - Confluence: requires an Atlassian API token; no OAuth flow
63
65
  - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
@@ -0,0 +1,128 @@
1
+ # ADR-003: Do not implement comment re-anchoring/migration on push — richer warning only
2
+
3
+ **Date**: 2026-08-12
4
+ **Status**: Accepted
5
+
6
+ ## Context
7
+
8
+ `docspan` issue #92 asks whether a comment anchored to a paragraph that push
9
+ resolves to delete+insert (per `_repair`'s docstring in
10
+ `docs_request_builder.py`: any diff opcode that doesn't collapse to `"equal"`
11
+ becomes a literal `deleteContentRange` + `insertText`) can be preserved
12
+ across that push, either by mutating the
13
+ Drive comment's existing `anchor`/`quotedFileContent` via `comments().update`,
14
+ or by creating a replacement comment via `comments().create` with a custom
15
+ `anchor`/`quotedFileContent` pointing at the reinserted paragraph.
16
+
17
+ A live-API spike against a real Drive document/comment was the intended way
18
+ to answer this, but no OAuth credentials or test Drive account are available
19
+ in this sandboxed environment, so no live `comments().update`/`comments().create`
20
+ call was made. This is stated plainly, not glossed over: the finding below
21
+ rests on documented API contract and this repo's own prior investigation,
22
+ not a fresh live call. **This is a hard environmental blocker, not a gap in
23
+ effort** — closing it for real requires a human to supply live OAuth
24
+ credentials plus a scratch Drive document/comment outside this sandbox and
25
+ run the two calls below; no amount of further reasoning in this environment
26
+ can substitute for that. If/when that access exists, the spike is exactly
27
+ two calls:
28
+ ```
29
+ comments().update(fileId=doc_id, commentId=c.id, fields="anchor", body={"anchor": target_anchor})
30
+ comments().create(fileId=doc_id, body={"content": c.content, "anchor": target_anchor, "quotedFileContent": {"value": snippet}})
31
+ ```
32
+ followed by reloading the comment in the Docs UI to see whether it renders anchored.
33
+
34
+ ### Confidence labeling
35
+
36
+ - **UNVERIFIED (this session)**: whether a live `comments().update`/`comments().create`
37
+ call against a real document actually behaves as documented — no such call was made here.
38
+ - **VERIFIED (secondary source, source 1 below)**: Google's own API reference and guide
39
+ text, read directly from developers.google.com, states anchors are immutable and that
40
+ `update`'s only documented-writable field is `content`.
41
+ - **VERIFIED (this repo's own prior work, source 2 below)**: `project_plans/bidirectional-comments/plan.md`
42
+ §2 already reached and recorded the same conclusion, citing a third-party reproduction
43
+ (`googleworkspace/cli#169`) as its own evidence, independent of this ADR.
44
+
45
+ Two sources answer the question decisively without a live call:
46
+
47
+ 1. **Google's own guide**, [Manage comments and replies](https://developers.google.com/workspace/drive/api/guides/manage-comments):
48
+ anchors are documented as immutable ("Anchors are immutable, and their
49
+ position relative to the content of a document cannot be guaranteed
50
+ between revisions"), and — separately — Google Workspace editor apps
51
+ (Docs included) render an API-set `anchor` as an **unanchored** comment in
52
+ the UI, regardless of whether the anchor value itself is well-formed.
53
+ 2. **This repo's own prior research**, `project_plans/bidirectional-comments/plan.md`
54
+ §2 (dated before this ADR, marked "High confidence — documented +
55
+ reproduced"): "`anchor` is ignored on Google Docs — a new anchored
56
+ comment renders as 'Original content deleted' / no highlight," citing
57
+ Google's guide plus a reproduced case at
58
+ [googleworkspace/cli#169](https://github.com/googleworkspace/cli/issues/169).
59
+ That plan already reached the same conclusion for *new* top-level
60
+ comments (ship unanchored-only, v1); this ADR extends the same finding
61
+ to the *migrate-an-existing-comment* case #92 asks about.
62
+
63
+ Both sources agree, from two different angles (official docs; independent
64
+ reproduction), that neither `comments().update` nor `comments().create` can
65
+ produce a comment that Google Docs' own UI will render as anchored to
66
+ arbitrary text. `comments().update`'s only documented writable field (per
67
+ its REST reference) is `content` — no update path for `anchor` is
68
+ documented at all.
69
+
70
+ Options considered:
71
+
72
+ 1. **Re-anchor via `comments().update`.** Blocked: `anchor` isn't a
73
+ documented-writable field on `update`, and even if it were, "anchors are
74
+ immutable" per Google's own guide.
75
+ 2. **Recreate via `comments().create` + resolve/delete the original.**
76
+ Technically possible to call, but the new comment renders **unanchored**
77
+ in the Docs UI (source 1 and 2 above) — a materially different, arguably
78
+ worse UX than today's silent loss: instead of nothing, the user gets a
79
+ stray document-level comment, under a new `id`/`createdTime`, that fires
80
+ a fresh "new comment" notification to every watcher, with no visual
81
+ anchor to the paragraph it's about.
82
+ 3. **Warn-and-proceed only (status quo mechanism, richer message).** No new
83
+ Drive write capability, no new failure mode, no notification-spam
84
+ regression. Ship the AC2 enrichment (list every at-risk comment per
85
+ paragraph, not just the first) on top of this.
86
+
87
+ ## Decision
88
+
89
+ Use **Option 3**. No comment migration ships. `find_high_risk_paragraphs`/
90
+ `render_high_risk` (`push_preview.py`) are extended to enumerate every
91
+ at-risk comment per flagged paragraph (id, author, snippet) instead of
92
+ stopping at the first match, but the underlying warn-before-`--force`
93
+ mechanism from ADR-002 is unchanged.
94
+
95
+ ## Rationale
96
+
97
+ - Option 1 is foreclosed by the API contract itself, not by appetite or
98
+ risk tolerance — there's no live spike result that could reverse this;
99
+ the field is undocumented-as-writable and documented-as-immutable.
100
+ - Option 2 is technically shippable but trades a *quiet* known limitation
101
+ for a *loud* new one (unanchored stray comment + notification spam) —
102
+ worse for the exact "silent vs. loud" trade ADR-002 already reasoned
103
+ through, but on the wrong side of it: ADR-002 chose loud-and-blocking
104
+ over silent-and-undetected specifically because a blocked push costs the
105
+ user two minutes, which is a good trade; recreate-based migration would
106
+ cost watchers a false "new comment" notification and the user a
107
+ duplicate, unanchored comment, which is not a favorable trade for a
108
+ problem that's still only partially solved (the anchor is still gone).
109
+ - Option 3 keeps the fix surface exactly where ADR-002 already put it —
110
+ read-only risk detection — and directly satisfies #92's fallback
111
+ instruction: "If re-anchoring isn't feasible, improve the existing
112
+ warn-before-force message."
113
+
114
+ ## Consequences
115
+
116
+ - AC5 ("if migration ships, a comment is preserved across push") is
117
+ satisfied by this ADR's explicit decision not to ship migration, per
118
+ #92's own conditional phrasing ("If migration ships...").
119
+ - No new Drive API write surface is added; `PUSH_SCOPES` is unchanged
120
+ (already covers today's `create_reply` calls).
121
+ - `push_preview.py`'s `HighRiskParagraph`/`find_high_risk_paragraphs`/
122
+ `render_high_risk` gain a richer, multi-comment rendering (AC2) without
123
+ changing which paragraphs get classified as high-risk (AC3) or how the
124
+ post-push open-comment-count backstop works (AC4).
125
+ - If Google ever documents a writable `anchor` field on `comments().update`,
126
+ or changes how Workspace editors render API-set anchors, this decision
127
+ should be revisited — it is contingent on the current, cited API
128
+ behavior, not a permanent architectural constraint.
@@ -33,6 +33,7 @@ dependencies = [
33
33
  "typer>=0.9.0",
34
34
  "rich>=13.0.0",
35
35
  "PyYAML>=6.0",
36
+ "ruamel.yaml>=0.18.0",
36
37
  "pydantic>=2.0.0",
37
38
  # Google Docs backend
38
39
  "google-auth>=2.23.0",
@@ -2,9 +2,9 @@
2
2
 
3
3
  import inspect
4
4
  from abc import ABC, abstractmethod
5
- from dataclasses import dataclass
5
+ from dataclasses import dataclass, field
6
6
  from enum import Enum
7
- from typing import Literal, Optional
7
+ from typing import List, Literal, Optional
8
8
 
9
9
 
10
10
  class SyncDirection(str, Enum):
@@ -29,6 +29,10 @@ class PushResult:
29
29
  doc_id: str
30
30
  message: Optional[str] = None
31
31
  url: Optional[str] = None
32
+ # Drive file ids for images uploaded as part of this push but not yet
33
+ # deleted -- populated only when push failed after upload, so a retry
34
+ # can clean them up or reuse them instead of leaking orphaned files.
35
+ retryable_temp_drive_file_ids: List[str] = field(default_factory=list)
32
36
 
33
37
 
34
38
  @dataclass
@@ -39,6 +43,13 @@ class PullResult:
39
43
  message: Optional[str] = None
40
44
 
41
45
 
46
+ @dataclass
47
+ class CreateResult:
48
+ doc_id: str
49
+ title: str
50
+ url: Optional[str] = None
51
+
52
+
42
53
  class Backend(ABC):
43
54
  """
44
55
  Base class for all docspan platform backends.
@@ -67,6 +78,10 @@ class Backend(ABC):
67
78
  def pull(self, doc_id: str, local_path: str, **kwargs) -> PullResult:
68
79
  """Fetch the remote document and write it as local markdown."""
69
80
 
81
+ @abstractmethod
82
+ def create(self, title: str, **kwargs: object) -> CreateResult:
83
+ """Create a new remote document/page and return its id/title/url."""
84
+
70
85
  @abstractmethod
71
86
  def auth_setup(self, config_path: Optional[str] = None) -> None:
72
87
  """Interactive / instructional setup wizard for this backend.
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Optional
10
10
 
11
11
  import markdownify as md_lib
12
12
 
13
- from docspan.backends.base import Backend, PullResult, PushResult
13
+ from docspan.backends.base import Backend, CreateResult, PullResult, PushResult
14
14
  from docspan.backends.confluence.adf.converter import AdfConverter
15
15
  from docspan.backends.confluence.config.models import ConfluenceConfig as InternalConfluenceConfig
16
16
  from docspan.backends.confluence.markdown.parser import MarkdownParser
@@ -193,6 +193,26 @@ class ConfluenceBackend(Backend):
193
193
  page = self._client.get_page(doc_id)
194
194
  return str(page["version"]["number"])
195
195
 
196
+ def create(self, title: str, **kwargs: object) -> CreateResult:
197
+ """Create a new, empty Confluence page and return its id/title/url."""
198
+ self._ensure_client()
199
+ assert self._client is not None
200
+ space_key = kwargs.get("space") or self.config.space_key
201
+ if not space_key:
202
+ raise ValueError(
203
+ "Confluence page creation requires a space key (--space or backends.confluence.space_key)."
204
+ )
205
+ empty_adf = AdfConverter().convert(MarkdownParser().parse(""))
206
+ page = ConfluencePage(title=title, content=empty_adf, parent_id="", space_key=str(space_key))
207
+ result = self._client.create_page(page)
208
+ page_id = result["id"]
209
+ base_url = self.config.base_url or ""
210
+ return CreateResult(
211
+ doc_id=page_id,
212
+ title=result.get("title", title),
213
+ url=f"{base_url}/pages/{page_id}",
214
+ )
215
+
196
216
  def auth_setup(self, config_path: "Optional[str]" = None) -> None:
197
217
  """Interactive Confluence auth setup — prompts for credentials and prints YAML snippet."""
198
218
  print("\nConfluence auth setup")
@@ -75,7 +75,11 @@ class PublishConfig:
75
75
  auto_fix_hierarchy: Whether to automatically fix page hierarchy based on directory structure
76
76
  auto_migrate_legacy: Whether to automatically migrate legacy editor pages to new editor before publishing
77
77
  duplicate_similarity_threshold: Threshold for considering pages as duplicates (0.0-1.0, default 0.8)
78
- render_mermaid_diagrams: Whether to render mermaid diagrams as images
78
+ render_mermaid_diagrams: Whether to render mermaid diagrams as images. NOTE: currently a
79
+ no-op — no code path reads this flag. Mermaid fences are always emitted as a plain
80
+ ADF code block (see docs/backends/confluence.md#limitations). Kept as a reserved
81
+ config key for when rendering is implemented, rather than removed, so existing
82
+ markgate.yaml files that set it don't start failing validation.
79
83
  process_assets: Whether to process assets (images, diagrams) for embedding
80
84
  ignore_patterns: List of file patterns to ignore (supports glob patterns like **/TODO.md)
81
85
  archive_ignored: Whether to archive/delete ignored files from Confluence if they have page IDs