docspan 0.2.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 (206) hide show
  1. docspan-0.4.0/.github/workflows/ci.yml +161 -0
  2. {docspan-0.2.0 → docspan-0.4.0}/.gitignore +5 -0
  3. docspan-0.4.0/.mypy-error-baseline +1 -0
  4. docspan-0.4.0/.release-please-manifest.json +3 -0
  5. docspan-0.4.0/CHANGELOG.md +170 -0
  6. {docspan-0.2.0 → docspan-0.4.0}/PKG-INFO +10 -3
  7. {docspan-0.2.0 → docspan-0.4.0}/README.md +6 -2
  8. {docspan-0.2.0 → docspan-0.4.0}/docs/backends/confluence.md +1 -0
  9. docspan-0.4.0/docs/backends/google-docs.md +67 -0
  10. {docspan-0.2.0 → docspan-0.4.0}/docs/index.md +3 -1
  11. {docspan-0.2.0 → docspan-0.4.0}/project_plans/gdocs-tables-inline-styles/plan.md +4 -0
  12. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/decisions/ADR-001-checklist-state-as-literal-text.md +7 -7
  13. docspan-0.4.0/project_plans/wedding-planning-workflow/decisions/ADR-003-no-comment-anchor-migration.md +128 -0
  14. {docspan-0.2.0 → docspan-0.4.0}/pyproject.toml +18 -0
  15. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/base.py +18 -3
  16. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/backend.py +21 -1
  17. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/models.py +5 -1
  18. docspan-0.4.0/src/docspan/backends/google_docs/backend.py +1228 -0
  19. docspan-0.4.0/src/docspan/backends/google_docs/checkbox_state.py +100 -0
  20. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/google_docs/client.py +108 -4
  21. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/google_docs/converter.py +6 -3
  22. docspan-0.4.0/src/docspan/backends/google_docs/cross_doc_links.py +286 -0
  23. docspan-0.4.0/src/docspan/backends/google_docs/docs_request_builder.py +2768 -0
  24. docspan-0.4.0/src/docspan/backends/google_docs/docs_structure_parser.py +730 -0
  25. docspan-0.4.0/src/docspan/backends/google_docs/heading_anchors.py +482 -0
  26. docspan-0.4.0/src/docspan/backends/google_docs/image_source.py +260 -0
  27. docspan-0.4.0/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +603 -0
  28. docspan-0.4.0/src/docspan/backends/google_docs/mermaid_renderer.py +100 -0
  29. docspan-0.4.0/src/docspan/backends/google_docs/nodes_to_markdown.py +538 -0
  30. docspan-0.4.0/src/docspan/backends/google_docs/projection.py +302 -0
  31. docspan-0.4.0/src/docspan/backends/google_docs/push_preview.py +380 -0
  32. docspan-0.4.0/src/docspan/backends/google_docs/registry.py +67 -0
  33. docspan-0.4.0/src/docspan/backends/google_docs/tabs.py +175 -0
  34. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/cli/main.py +231 -6
  35. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/config.py +99 -1
  36. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/core/orchestrator.py +31 -8
  37. docspan-0.4.0/tests/__init__.py +0 -0
  38. docspan-0.4.0/tests/fixtures/github_slugger_vectors.json +349 -0
  39. docspan-0.4.0/tests/test_checkbox_state.py +74 -0
  40. {docspan-0.2.0 → docspan-0.4.0}/tests/test_cli.py +380 -4
  41. docspan-0.4.0/tests/test_code_block_granularity.py +998 -0
  42. {docspan-0.2.0 → docspan-0.4.0}/tests/test_config.py +79 -2
  43. docspan-0.4.0/tests/test_confluence_backend.py +47 -0
  44. docspan-0.4.0/tests/test_confluence_mermaid_push_pipeline.py +101 -0
  45. docspan-0.4.0/tests/test_content_key_pooling_performance.py +115 -0
  46. {docspan-0.2.0 → docspan-0.4.0}/tests/test_converter.py +18 -0
  47. docspan-0.4.0/tests/test_cross_doc_link_issues.py +135 -0
  48. docspan-0.4.0/tests/test_cross_doc_links.py +310 -0
  49. docspan-0.4.0/tests/test_cross_doc_links_backend.py +481 -0
  50. docspan-0.4.0/tests/test_docs_request_builder.py +1188 -0
  51. {docspan-0.2.0 → docspan-0.4.0}/tests/test_docs_structure_parser.py +263 -0
  52. docspan-0.4.0/tests/test_gdocs_images.py +427 -0
  53. docspan-0.4.0/tests/test_gdocs_mermaid.py +119 -0
  54. docspan-0.4.0/tests/test_gdocs_push_pipeline.py +1380 -0
  55. docspan-0.4.0/tests/test_gdocs_tables_and_styles.py +454 -0
  56. docspan-0.4.0/tests/test_google_docs_backend.py +2009 -0
  57. {docspan-0.2.0 → docspan-0.4.0}/tests/test_google_onboarding.py +8 -2
  58. docspan-0.4.0/tests/test_heading_anchors.py +1320 -0
  59. docspan-0.4.0/tests/test_heading_identity.py +982 -0
  60. {docspan-0.2.0 → docspan-0.4.0}/tests/test_markdown_to_paragraph_parser.py +41 -0
  61. docspan-0.4.0/tests/test_nodes_to_markdown.py +408 -0
  62. {docspan-0.2.0 → docspan-0.4.0}/tests/test_orchestrator.py +85 -2
  63. docspan-0.4.0/tests/test_push_preview.py +689 -0
  64. docspan-0.4.0/tests/test_registry.py +93 -0
  65. docspan-0.4.0/tests/test_restyle_destruction_rate.py +149 -0
  66. docspan-0.4.0/tests/test_span_trailing_newline.py +111 -0
  67. docspan-0.4.0/tests/test_table_cell_spans.py +673 -0
  68. docspan-0.4.0/tests/test_tabs.py +250 -0
  69. {docspan-0.2.0 → docspan-0.4.0}/uv.lock +85 -74
  70. docspan-0.2.0/.github/workflows/ci.yml +0 -34
  71. docspan-0.2.0/.release-please-manifest.json +0 -3
  72. docspan-0.2.0/CHANGELOG.md +0 -67
  73. docspan-0.2.0/docs/backends/google-docs.md +0 -62
  74. docspan-0.2.0/src/docspan/backends/google_docs/backend.py +0 -473
  75. docspan-0.2.0/src/docspan/backends/google_docs/docs_request_builder.py +0 -529
  76. docspan-0.2.0/src/docspan/backends/google_docs/docs_structure_parser.py +0 -212
  77. docspan-0.2.0/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +0 -263
  78. docspan-0.2.0/src/docspan/backends/google_docs/push_preview.py +0 -201
  79. docspan-0.2.0/tests/test_docs_request_builder.py +0 -378
  80. docspan-0.2.0/tests/test_gdocs_tables_and_styles.py +0 -237
  81. docspan-0.2.0/tests/test_google_docs_backend.py +0 -340
  82. docspan-0.2.0/tests/test_push_preview.py +0 -338
  83. {docspan-0.2.0 → docspan-0.4.0}/.github/workflows/publish.yml +0 -0
  84. {docspan-0.2.0 → docspan-0.4.0}/.github/workflows/release-please.yml +0 -0
  85. {docspan-0.2.0 → docspan-0.4.0}/CONTRIBUTING.md +0 -0
  86. {docspan-0.2.0 → docspan-0.4.0}/Procfile +0 -0
  87. {docspan-0.2.0 → docspan-0.4.0}/RAILWAY_SETUP.md +0 -0
  88. /docspan-0.2.0/src/docspan/__main__.py → /docspan-0.4.0/doc.md +0 -0
  89. {docspan-0.2.0 → docspan-0.4.0}/docs/commands.md +0 -0
  90. {docspan-0.2.0 → docspan-0.4.0}/docs/configuration.md +0 -0
  91. {docspan-0.2.0 → docspan-0.4.0}/docs/contributing.md +0 -0
  92. {docspan-0.2.0 → docspan-0.4.0}/docs/install.md +0 -0
  93. {docspan-0.2.0 → docspan-0.4.0}/docspan.yaml.example +0 -0
  94. {docspan-0.2.0 → docspan-0.4.0}/markgate.yaml.example +0 -0
  95. {docspan-0.2.0 → docspan-0.4.0}/mkdocs.yml +0 -0
  96. {docspan-0.2.0 → docspan-0.4.0}/modules/__init__.py +0 -0
  97. {docspan-0.2.0 → docspan-0.4.0}/modules/auth.py +0 -0
  98. {docspan-0.2.0 → docspan-0.4.0}/modules/conflict_handler.py +0 -0
  99. {docspan-0.2.0 → docspan-0.4.0}/modules/converter.py +0 -0
  100. {docspan-0.2.0 → docspan-0.4.0}/modules/gdrive_client.py +0 -0
  101. {docspan-0.2.0 → docspan-0.4.0}/modules/sync_engine.py +0 -0
  102. {docspan-0.2.0 → docspan-0.4.0}/project_plans/bidirectional-comments/plan.md +0 -0
  103. {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/adversarial-review.md +0 -0
  104. {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/plan.md +0 -0
  105. {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/release-checklist.md +0 -0
  106. {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/validation.md +0 -0
  107. {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/requirements.md +0 -0
  108. {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/research/architecture.md +0 -0
  109. {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/research/features.md +0 -0
  110. {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/research/google-docs-push.md +0 -0
  111. {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/research/pitfalls.md +0 -0
  112. {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/research/stack.md +0 -0
  113. {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/decisions/ADR-001-merge3-dependency.md +0 -0
  114. {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/decisions/ADR-002-base-content-sidecar-store.md +0 -0
  115. {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/implementation/adversarial-review.md +0 -0
  116. {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/implementation/plan.md +0 -0
  117. {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/implementation/validation.md +0 -0
  118. {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/requirements.md +0 -0
  119. {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/research/architecture.md +0 -0
  120. {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/research/features.md +0 -0
  121. {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/research/pitfalls.md +0 -0
  122. {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/research/stack.md +0 -0
  123. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/decisions/ADR-002-comment-risk-flagging-not-anchor-preservation.md +0 -0
  124. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/feature-gap-report.md +0 -0
  125. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/adversarial-review.md +0 -0
  126. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/architecture-review.md +0 -0
  127. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/plan.md +0 -0
  128. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/pre-mortem.md +0 -0
  129. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/validation.md +0 -0
  130. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/requirements.md +0 -0
  131. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/architecture.md +0 -0
  132. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/build-vs-buy.md +0 -0
  133. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/features.md +0 -0
  134. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/pitfalls.md +0 -0
  135. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/stack.md +0 -0
  136. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/ux.md +0 -0
  137. {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/workflow-runbook.md +0 -0
  138. {docspan-0.2.0 → docspan-0.4.0}/release-please-config.json +0 -0
  139. {docspan-0.2.0 → docspan-0.4.0}/requirements.txt +0 -0
  140. {docspan-0.2.0 → docspan-0.4.0}/runtime.txt +0 -0
  141. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/__init__.py +0 -0
  142. /docspan-0.2.0/src/docspan/backends/confluence/__init__.py → /docspan-0.4.0/src/docspan/__main__.py +0 -0
  143. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/__init__.py +0 -0
  144. {docspan-0.2.0/src/docspan/backends/confluence/services → docspan-0.4.0/src/docspan/backends/confluence}/__init__.py +0 -0
  145. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/__init__.py +0 -0
  146. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/comparator.py +0 -0
  147. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/converter.py +0 -0
  148. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/converters.py +0 -0
  149. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/interfaces.py +0 -0
  150. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/nodes.py +0 -0
  151. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/parser.py +0 -0
  152. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/validators.py +0 -0
  153. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/visitors.py +0 -0
  154. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/client.py +0 -0
  155. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/__init__.py +0 -0
  156. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/loader.py +0 -0
  157. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/validation.py +0 -0
  158. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/__init__.py +0 -0
  159. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/ast.py +0 -0
  160. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/__init__.py +0 -0
  161. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/frontmatter.py +0 -0
  162. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/mermaid.py +0 -0
  163. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/wikilinks.py +0 -0
  164. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/inline_parser.py +0 -0
  165. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/parser.py +0 -0
  166. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/__init__.py +0 -0
  167. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/markdown_file.py +0 -0
  168. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/page.py +0 -0
  169. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/path_utils.py +0 -0
  170. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/results.py +0 -0
  171. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/sync_status.py +0 -0
  172. {docspan-0.2.0/src/docspan/backends/google_docs → docspan-0.4.0/src/docspan/backends/confluence/services}/__init__.py +0 -0
  173. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/__init__.py +0 -0
  174. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/attachment_client.py +0 -0
  175. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/base_client.py +0 -0
  176. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/client.py +0 -0
  177. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/comment_client.py +0 -0
  178. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/crawler.py +0 -0
  179. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/label_client.py +0 -0
  180. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/page_client.py +0 -0
  181. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/space_client.py +0 -0
  182. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/url_parser.py +0 -0
  183. {docspan-0.2.0/src/docspan/cli → docspan-0.4.0/src/docspan/backends/google_docs}/__init__.py +0 -0
  184. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/google_docs/auth.py +0 -0
  185. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/google_docs/comments.py +0 -0
  186. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/google_docs/onboarding.py +0 -0
  187. {docspan-0.2.0/tests → docspan-0.4.0/src/docspan/cli}/__init__.py +0 -0
  188. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/core/__init__.py +0 -0
  189. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/core/merge.py +0 -0
  190. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/core/paths.py +0 -0
  191. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/core/state.py +0 -0
  192. {docspan-0.2.0 → docspan-0.4.0}/src/docspan/core/xdg.py +0 -0
  193. {docspan-0.2.0 → docspan-0.4.0}/sync.py +0 -0
  194. {docspan-0.2.0 → docspan-0.4.0}/terraform/gcp/README.md +0 -0
  195. {docspan-0.2.0 → docspan-0.4.0}/terraform/gcp/main.tf +0 -0
  196. {docspan-0.2.0 → docspan-0.4.0}/terraform/gcp/outputs.tf +0 -0
  197. {docspan-0.2.0 → docspan-0.4.0}/terraform/gcp/variables.tf +0 -0
  198. {docspan-0.2.0 → docspan-0.4.0}/terraform/main.tf +0 -0
  199. {docspan-0.2.0 → docspan-0.4.0}/terraform/variables.tf +0 -0
  200. {docspan-0.2.0 → docspan-0.4.0}/tests/conftest.py +0 -0
  201. {docspan-0.2.0 → docspan-0.4.0}/tests/test_conflict_resolution.py +0 -0
  202. {docspan-0.2.0 → docspan-0.4.0}/tests/test_google_comments.py +0 -0
  203. {docspan-0.2.0 → docspan-0.4.0}/tests/test_google_oauth.py +0 -0
  204. {docspan-0.2.0 → docspan-0.4.0}/tests/test_merge.py +0 -0
  205. {docspan-0.2.0 → docspan-0.4.0}/tests/test_state.py +0 -0
  206. {docspan-0.2.0 → docspan-0.4.0}/tests/test_xdg_central_config.py +0 -0
@@ -0,0 +1,161 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ # No `branches:` filter — every pull request runs CI regardless of its base.
7
+ # With the filter, a stacked PR (one based on another PR's branch rather than
8
+ # on main) got no checks at all, so the only branch in a stack that was ever
9
+ # tested was the bottom one.
10
+ pull_request:
11
+ workflow_dispatch:
12
+
13
+ # A stack of PRs rebases repeatedly, and each push supersedes the run before it.
14
+ # Cancel the outdated run rather than paying for the whole matrix on a commit
15
+ # nobody will look at. `main` is excluded from cancellation so a push there
16
+ # always produces a complete result.
17
+ concurrency:
18
+ group: ci-${{ github.workflow }}-${{ github.ref }}
19
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
20
+
21
+ jobs:
22
+ lint:
23
+ name: Lint
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ with:
28
+ fetch-depth: 0 # needed for hatch-vcs version detection
29
+
30
+ - name: Install uv
31
+ uses: astral-sh/setup-uv@v4
32
+
33
+ - name: Install dependencies
34
+ run: uv sync --extra dev
35
+
36
+ # Runs once rather than once per Python version — ruff's result does not
37
+ # depend on the interpreter it runs under (`target-version` is pinned in
38
+ # pyproject.toml), so the other five runs were duplicate work.
39
+ - name: ruff check
40
+ run: uv run ruff check src tests
41
+
42
+ test:
43
+ name: Test (${{ matrix.python-version }})
44
+ runs-on: ubuntu-latest
45
+ strategy:
46
+ fail-fast: false # one version failing shouldn't hide the others' results
47
+ matrix:
48
+ # pyproject declares requires-python = ">=3.9", so every version it
49
+ # claims to support gets tested. 3.13 and 3.14 were verified green
50
+ # before being added here.
51
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
52
+
53
+ steps:
54
+ - uses: actions/checkout@v4
55
+ with:
56
+ fetch-depth: 0 # needed for hatch-vcs version detection
57
+
58
+ - name: Install uv
59
+ uses: astral-sh/setup-uv@v4
60
+
61
+ - name: Set up Python ${{ matrix.python-version }}
62
+ run: uv python install ${{ matrix.python-version }}
63
+
64
+ - name: Install dependencies
65
+ run: uv sync --extra dev
66
+
67
+ - name: Test
68
+ run: uv run pytest --tb=short -q
69
+
70
+ types:
71
+ name: Types (mypy ratchet)
72
+ runs-on: ubuntu-latest
73
+ steps:
74
+ - uses: actions/checkout@v4
75
+ with:
76
+ fetch-depth: 0
77
+
78
+ - name: Install uv
79
+ uses: astral-sh/setup-uv@v4
80
+
81
+ - name: Install dependencies
82
+ run: uv sync --extra dev
83
+
84
+ # mypy cannot be a plain pass/fail gate here: `mypy src` currently reports
85
+ # 266 errors across 32 files, so adding it as one would leave CI
86
+ # permanently red, and `continue-on-error` would make it decorative.
87
+ #
88
+ # A ratchet is a real gate instead — it fails on any *new* error while
89
+ # tolerating the existing backlog, and the baseline only ever goes down.
90
+ # Delete this job and gate on mypy's exit code once the baseline reaches 0.
91
+ - name: mypy (must not exceed baseline)
92
+ run: |
93
+ set -uo pipefail
94
+ baseline=$(tr -dc '0-9' < .mypy-error-baseline)
95
+ if [ -z "$baseline" ]; then
96
+ echo "::error::.mypy-error-baseline does not contain a number"
97
+ exit 1
98
+ fi
99
+
100
+ output=$(uv run mypy src 2>&1 || true)
101
+ echo "$output" | tail -40
102
+
103
+ if printf '%s\n' "$output" | grep -q '^Success: no issues found'; then
104
+ count=0
105
+ else
106
+ count=$(printf '%s\n' "$output" \
107
+ | sed -n 's/^Found \([0-9]\{1,\}\) error.*/\1/p' | tail -1)
108
+ fi
109
+ if [ -z "$count" ]; then
110
+ echo "::error::could not parse an error count out of mypy's output"
111
+ exit 1
112
+ fi
113
+
114
+ echo "mypy errors: $count (baseline $baseline)"
115
+ if [ "$count" -gt "$baseline" ]; then
116
+ echo "::error::mypy errors rose from $baseline to $count." \
117
+ "Fix the new errors, or justify and raise .mypy-error-baseline."
118
+ exit 1
119
+ fi
120
+ if [ "$count" -lt "$baseline" ]; then
121
+ echo "::notice::mypy errors fell from $baseline to $count." \
122
+ "Please lower .mypy-error-baseline to $count to lock the gain in."
123
+ fi
124
+
125
+ package:
126
+ name: Package
127
+ runs-on: ubuntu-latest
128
+ steps:
129
+ - uses: actions/checkout@v4
130
+ with:
131
+ fetch-depth: 0 # hatch-vcs derives the version from tags
132
+
133
+ - name: Install uv
134
+ uses: astral-sh/setup-uv@v4
135
+
136
+ # publish.yml only builds on `release: published`, so until now a change
137
+ # that broke packaging stayed invisible until release day.
138
+ - name: Build sdist and wheel
139
+ run: uv build
140
+
141
+ # Catches a dependency edited in pyproject.toml without re-locking, which
142
+ # would otherwise surface as `uv sync` resolving something the lockfile
143
+ # never pinned.
144
+ - name: Lockfile is in sync with pyproject
145
+ run: uv lock --check
146
+
147
+ # uv.lock records the index each package came from. Resolving it behind a
148
+ # private or corporate mirror rewrites every URL to that host, and
149
+ # committing the result both leaks the internal hostname into this public
150
+ # repo and breaks CI, which cannot reach it. `git add -A` will happily
151
+ # sweep that in, and the diff is thousands of near-identical lines that
152
+ # nobody reads.
153
+ - name: Lockfile references only the public index
154
+ run: |
155
+ set -uo pipefail
156
+ if grep -n 'registry = "' uv.lock | grep -v 'https://pypi.org/simple'; then
157
+ echo "::error file=uv.lock::uv.lock references a non-public index." \
158
+ "Re-resolve it with UV_INDEX_URL=https://pypi.org/simple uv lock."
159
+ exit 1
160
+ fi
161
+ echo "uv.lock: public index only"
@@ -65,3 +65,8 @@ site/
65
65
  *.tfstate.backup
66
66
  *.tfvars
67
67
  .terraform.lock.hcl
68
+
69
+ # Session scaffolding — regenerated per backlog-session spawn/attach, never a
70
+ # repo deliverable (see commits 8f80a5b, bd693ca on other branches)
71
+ .backlog-context.md
72
+ .claude/commands/backlog/
@@ -0,0 +1 @@
1
+ 266
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.0"
3
+ }
@@ -0,0 +1,170 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
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
+
35
+ ## [0.3.0](https://github.com/tstapler/docspan/compare/docspan-v0.2.0...docspan-v0.3.0) (2026-08-11)
36
+
37
+
38
+ ### Features
39
+
40
+ * **gdocs:** keep inline styling inside table cells ([#51](https://github.com/tstapler/docspan/issues/51)) ([880eeb1](https://github.com/tstapler/docspan/commit/880eeb1feb3a3f700a1641f360abf5bc32cab191)), closes [#49](https://github.com/tstapler/docspan/issues/49)
41
+ * **google-docs:** add Google Doc tab support (tab_id) ([#14](https://github.com/tstapler/docspan/issues/14)) ([cee1db6](https://github.com/tstapler/docspan/commit/cee1db6dde0c957088159339527a6b8dbef70b47))
42
+ * **google-docs:** resolve internal markdown anchors to heading links ([#36](https://github.com/tstapler/docspan/issues/36)) ([5faaa6c](https://github.com/tstapler/docspan/commit/5faaa6c7ded880eba048d20807ff76a4a5f480a8))
43
+ * **google-docs:** restyle a paragraph in place instead of retyping it ([edf0b13](https://github.com/tstapler/docspan/commit/edf0b13520aa1a60b1fb02ee51dc1bd4a967c412))
44
+
45
+
46
+ ### Bug Fixes
47
+
48
+ * **cli:** accept --config and --prefix before the subcommand as well as after ([00f6524](https://github.com/tstapler/docspan/commit/00f65241e41c5bb2deec5642eb36e8ea64a58744))
49
+ * **gdocs:** stop a repeated line stealing a live heading's identity ([#50](https://github.com/tstapler/docspan/issues/50)) ([2992d39](https://github.com/tstapler/docspan/commit/2992d3974dc9f7dea2547b5b8b3881f7c141f810))
50
+ * **google-docs:** align pass 2 by content so trimmed deletes can't mis-style ([ba56e08](https://github.com/tstapler/docspan/commit/ba56e08b7cc482c6bd569e9e5560e1c71a768ebf))
51
+ * **google-docs:** append past the last node without merging it into the last paragraph ([1aef861](https://github.com/tstapler/docspan/commit/1aef861981ee95463e7c2c9caa67bf32d90c9d2a))
52
+ * **google-docs:** apply inline styling when it is the only change ([c205552](https://github.com/tstapler/docspan/commit/c205552aed5c0f483ba5213bd39f2aff16587efe))
53
+ * **google-docs:** clear the inherited bullet on inserted non-list paragraphs ([dda8a50](https://github.com/tstapler/docspan/commit/dda8a505e8f822c077478656158b87cf6d74132b))
54
+ * **google-docs:** insert before a Table/ToC/SectionBreak into the body, not into it ([701e2d0](https://github.com/tstapler/docspan/commit/701e2d01433e9efc2c42a159ffb4f92d90972c56))
55
+ * **google-docs:** keep the paragraph newline out of the spans ([#35](https://github.com/tstapler/docspan/issues/35)) ([f73272a](https://github.com/tstapler/docspan/commit/f73272a0422e5c525a63bcedb6cb68fdb7ff517b))
56
+ * **google-docs:** make push idempotent for documents with fenced code blocks ([#41](https://github.com/tstapler/docspan/issues/41)) ([e98406e](https://github.com/tstapler/docspan/commit/e98406e2858958421c3bdf5e17c94f880056e540))
57
+ * **google-docs:** make render_prefix part of node/content identity ([#67](https://github.com/tstapler/docspan/issues/67)) ([6205850](https://github.com/tstapler/docspan/commit/62058503c38372dec7de64e4a8ea1b6603c7c2fc))
58
+ * **google-docs:** normalize away the render glyph Docs writes before a native code block ([#48](https://github.com/tstapler/docspan/issues/48)) ([31b4edd](https://github.com/tstapler/docspan/commit/31b4edd1bc944f1b48605d3a1ffe0d2283e161fe))
59
+ * **google-docs:** order pass-1 requests by their anchor, not their own index ([58e2b6a](https://github.com/tstapler/docspan/commit/58e2b6af2271dc200c225c39c44d1a567ae5722a))
60
+ * **google-docs:** pair Nth live table with Nth target regardless of emptiness ([#77](https://github.com/tstapler/docspan/issues/77)) ([46f96b6](https://github.com/tstapler/docspan/commit/46f96b6982bfade9656c09ea543bcdf9b692e876))
61
+ * **google-docs:** prefer a code-rendered node over a duplicate-text prose node ([#75](https://github.com/tstapler/docspan/issues/75)) ([5853566](https://github.com/tstapler/docspan/commit/5853566d4aaa3f57755373bccdaedde42709e9c5))
62
+ * **google-docs:** project empty paragraphs out of the diff instead of deleting them ([d8b1b5f](https://github.com/tstapler/docspan/commit/d8b1b5feb56439da34ae7be64c092b80927c30a7))
63
+ * **google-docs:** project the re-fetched live doc before pass-2 style alignment ([#69](https://github.com/tstapler/docspan/issues/69)) ([cf36561](https://github.com/tstapler/docspan/commit/cf36561b953e7d2c2da4c11f60c2a74bca3a2259))
64
+ * **google-docs:** recover native checkbox checked state on pull via markdown export ([#78](https://github.com/tstapler/docspan/issues/78)) ([c7352f7](https://github.com/tstapler/docspan/commit/c7352f79d38c19c092ade81cf1368dc3c72ad853))
65
+ * **google-docs:** render @-mention person chips as name/email text ([#15](https://github.com/tstapler/docspan/issues/15)) ([f5d7427](https://github.com/tstapler/docspan/commit/f5d742758855aeab435c50fba3a1cb51b9a79eef))
66
+ * **google-docs:** render fenced code blocks on tab-scoped pull instead of per-line inline code ([#74](https://github.com/tstapler/docspan/issues/74)) ([3bae98a](https://github.com/tstapler/docspan/commit/3bae98ac01007ca33288c6d87b2bf8288291c216))
67
+ * **google-docs:** render TITLE/SUBTITLE as headings instead of silently demoting them ([cc6cd0b](https://github.com/tstapler/docspan/commit/cc6cd0b6e8fdb8337cfa871f77c4466445041922))
68
+ * **google-docs:** report a dropped over-long span, and share the delete-trim arithmetic ([daa77a6](https://github.com/tstapler/docspan/commit/daa77a6398f9ed967deef1376b39d73e66050aea))
69
+ * **google-docs:** stop deleting the newline that anchors a table/ToC/section break ([9eba496](https://github.com/tstapler/docspan/commit/9eba496ee1d4db3fb788e910c359d9487f217baa))
70
+ * **google-docs:** stop replace inserts from duplicating a clamp-spared newline ([#76](https://github.com/tstapler/docspan/issues/76)) ([f440f68](https://github.com/tstapler/docspan/commit/f440f68b3fcd9fce0333dfb95b60a40e5638c7e3))
71
+
72
+ ## [0.2.0](https://github.com/tstapler/docspan/compare/docspan-v0.1.0...docspan-v0.2.0) (2026-07-22)
73
+
74
+
75
+ ### Features
76
+
77
+ * Add Railway Volume support for persistent state storage ([3a4b76e](https://github.com/tstapler/docspan/commit/3a4b76eb0cbc14afa02f3aa3de2c4607808fad9f))
78
+ * Add retry mechanism and improved error handling for Google Drive API ([e8a7b5f](https://github.com/tstapler/docspan/commit/e8a7b5f177ad2c3b8a356852eceb827326f8ce76))
79
+ * Auto-reload Google Sheet mappings on each sync cycle ([a2647c6](https://github.com/tstapler/docspan/commit/a2647c6c1435eb5624a389a4e673a57ead012123))
80
+ * **config:** XDG storage paths + central config with project prefixes ([#7](https://github.com/tstapler/docspan/issues/7)) ([0aa9165](https://github.com/tstapler/docspan/commit/0aa9165d24df95386b514d283cf846a2cdc809f7))
81
+ * **confluence:** port adf/markdown/services from markdown-confluence ([e9d1a85](https://github.com/tstapler/docspan/commit/e9d1a85a9747ac75a6d95d6351d18483297726a4))
82
+ * **google_docs:** checklist round-trip + comment/glyph-risk push gate ([#8](https://github.com/tstapler/docspan/issues/8)) ([bd2a885](https://github.com/tstapler/docspan/commit/bd2a885d6a2e18b758f12fc4a2aaf588c045d059))
83
+ * **google_docs:** docspan comments respond — reply/resolve round-trip ([#13](https://github.com/tstapler/docspan/issues/13)) ([7a662ed](https://github.com/tstapler/docspan/commit/7a662edb14b6f0078fcca110023cfc7e54724726))
84
+ * **google-docs:** add per-user OAuth auth option ([#4](https://github.com/tstapler/docspan/issues/4)) ([830369c](https://github.com/tstapler/docspan/commit/830369cba1817224ae0d02f0b14b6a84de84a4eb))
85
+ * **google-docs:** push markdown tables and inline links/formatting ([#3](https://github.com/tstapler/docspan/issues/3)) ([5b74246](https://github.com/tstapler/docspan/commit/5b74246eb1070355b39c5e84292e59f443457875))
86
+ * **google-docs:** read comments into a {file}.comments.md sidecar on pull ([#5](https://github.com/tstapler/docspan/issues/5)) ([aca2264](https://github.com/tstapler/docspan/commit/aca226412ef74c80603678d7ae1defaa25e38954))
87
+ * scaffold markgate package from google-docs-obsidian-sync fork ([44dd3c5](https://github.com/tstapler/docspan/commit/44dd3c586a670b4689154b2db9bc6cb8673d9702))
88
+ * **sync:** Google Docs structural-diff push, Confluence comments, three-way merge ([9a20e34](https://github.com/tstapler/docspan/commit/9a20e3452f2d92a240128d7c8e2f9c4b63a547f9))
89
+
90
+
91
+ ### Bug Fixes
92
+
93
+ * **ci:** add __future__ annotations for Python 3.9 compat in test ([9ceca65](https://github.com/tstapler/docspan/commit/9ceca65ca5ad2040c1c2ec215fc097b02ba1a0c4))
94
+ * **ci:** apply ruff autofix across all src and test files ([bee2784](https://github.com/tstapler/docspan/commit/bee2784b940d872045ce33faf0ce53f65150d80d))
95
+ * **ci:** resolve ruff lint failures and enable Actions PR creation ([8727e7b](https://github.com/tstapler/docspan/commit/8727e7bcabf1ddec4b4116b06e605ff24d0eaffe))
96
+ * **google-docs:** don't drop blockquote paragraphs on push ([#9](https://github.com/tstapler/docspan/issues/9)) ([e3b2597](https://github.com/tstapler/docspan/commit/e3b259799acfcdaa7f884edb49633f349860a978))
97
+ * **google-docs:** fix inline-style paragraph misalignment on push ([#10](https://github.com/tstapler/docspan/issues/10)) ([4f79ef8](https://github.com/tstapler/docspan/commit/4f79ef8b4669eee4d6fe361af5bae68bb4486019))
98
+ * **google-docs:** fix mid-document insert off-by-one causing paragraph merges ([#12](https://github.com/tstapler/docspan/issues/12)) ([c74bea2](https://github.com/tstapler/docspan/commit/c74bea2d6464df0c19abce31a39aea0bc18d1e46))
99
+ * **google-docs:** restore inline styling and unwrap redirect links on pull ([#11](https://github.com/tstapler/docspan/issues/11)) ([b90466c](https://github.com/tstapler/docspan/commit/b90466cd9765a290b02634bbe9b3869185e308bc))
100
+ * Improve nested list indentation in Google Docs to Markdown conversion ([d6a7539](https://github.com/tstapler/docspan/commit/d6a7539d4beade3426e5d0db838f1d9974f7294b))
101
+ * Remove CONFIG_YAML dependency, prefer individual env vars ([d5d5d4a](https://github.com/tstapler/docspan/commit/d5d5d4ae25a7c1d7a213071f968eba56a9289da3))
102
+ * Resolve service account storage quota error by storing sync state locally ([00e9cb6](https://github.com/tstapler/docspan/commit/00e9cb65033dfb6cca8e0aae2258cde458cfb342))
103
+
104
+ ## [Unreleased]
105
+
106
+ ### Added
107
+ - **google-docs:** internal markdown anchors (`[A1](#a1-current-state)`) now resolve to
108
+ Google Docs heading links instead of being written as a `#fragment` URL the Doc cannot
109
+ follow. Slugs follow `github-slugger`, checked against vectors generated from the real
110
+ implementation. `TITLE`/`SUBTITLE` paragraphs count as anchor targets, and both the
111
+ modern `Link.heading` and the legacy `Link.headingId` union members are read, so an
112
+ anchor survives a pull whether or not the fetch used `includeTabsContent`.
113
+ - **google-docs:** an anchor that names no heading is written as plain text with no link
114
+ and reported — `docspan push --dry-run` lists it, `docspan push` exits non-zero with a
115
+ warning naming the anchor and the heading anchors that *are* available. It is never
116
+ written as a link a reader can click and land nowhere, and never reported as a clean ✓.
117
+ - **google-docs:** both pull paths now emit the heading's slug. A default (no `tab_id`) pull
118
+ goes through Drive's HTML export, which carries the Doc's opaque `#h.abc123` through
119
+ verbatim; it is upgraded to the slug, so the pulled markdown works as markdown.
120
+
121
+ ### Changed
122
+ - **google-docs:** pass 2 parses and aligns the document once per push instead of three
123
+ times. The discarded work sat inside the window between pass 2's read and its write, where
124
+ a concurrent edit costs a conflict on a document pass 1 has already changed.
125
+
126
+ ### Known limitations
127
+ Each of these is tracked as a follow-up rather than half-addressed here.
128
+ - An anchor into a heading in a *different tab* of the same document cannot be resolved and
129
+ is reported unresolved. The flat `headingId` member resolves against the tab named in the
130
+ request, so expressing one needs the tabs-aware `Link.heading` member.
131
+ - A pull cannot express a `bookmark`/`bookmarkId` link, a link to a tab, or any link inside
132
+ a table cell, so those are dropped from the pulled file without a report.
133
+ - Confluence writes an internal anchor as a literal `#fragment` href, which it does not
134
+ resolve.
135
+ - An anchor that resolves to nothing is written as plain text, so a later pull replaces the
136
+ author's `[text](#anchor)` with `text`. The push reports it; nothing does afterwards.
137
+ - Such a push exits non-zero on every run, with no flag to suppress it.
138
+ - A heading containing an HTML entity reference (`## Team &amp; process`) or inline HTML
139
+ (`## <code>push()</code> …`) is slugged from the markdown *source* rather than the rendered
140
+ text, so its slug differs from GitHub's. Because duplicate numbering depends on the
141
+ headings before it, that can land an anchor on a neighbouring heading. Pre-existing; a fix
142
+ attempt was reverted on this branch because it needs the slug text and the
143
+ document text separated, which is its own change.
144
+
145
+ ## [0.1.0] - 2026-06-07
146
+
147
+ ### Added
148
+ - `docspan push` — push local markdown files to Google Docs or Confluence
149
+ - `docspan pull` — pull remote documents into local markdown files with three-way merge
150
+ - `docspan status` — show current mapping status in a table
151
+ - `docspan auth setup` — interactive authentication setup for `google_docs` and `confluence` backends
152
+ - `docspan conflicts list` — list files with unresolved merge conflicts
153
+ - `docspan conflicts resolve` — resolve merge conflicts with `remote`, `local`, or `merged` strategy
154
+ - Google Docs backend: push and pull via Google Docs API (service account auth)
155
+ - Confluence backend: push and pull via Atlassian REST API (API token auth)
156
+ - Three-way merge for bidirectional sync conflict detection
157
+ - Confluence comment sidecar: pull writes inline and footer comments to `{file}.comments.md`
158
+ - `markgate.yaml` config file format with per-mapping direction control (`push`/`pull`/`both`)
159
+ - Sync state tracking via `.markgate-state.json` and content-addressed base store in `.markgate-base/`
160
+
161
+ ### Known Limitations
162
+ - Google Docs: comments on edited paragraphs are destroyed on push (paragraph-level diff; comments on unchanged paragraphs are preserved)
163
+ - Push: no image support — local image files cannot be pushed to Google Docs or Confluence
164
+ - Push: no table support — markdown tables are not rendered in Google Docs
165
+ - Confluence: requires an Atlassian API token; no OAuth flow
166
+ - Confluence: comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
167
+ - Config file is named `markgate.yaml` (not `docspan.yaml`) and state file is `.markgate-state.json` (not `.docspan-state.json`). These will be renamed in v0.2.0.
168
+
169
+ [Unreleased]: https://github.com/tstapler/docspan/compare/v0.1.0...HEAD
170
+ [0.1.0]: https://github.com/tstapler/docspan/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: docspan
3
- Version: 0.2.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
@@ -18,6 +18,8 @@ Classifier: Programming Language :: Python :: 3.9
18
18
  Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
21
23
  Classifier: Topic :: Software Development :: Documentation
22
24
  Classifier: Topic :: Text Processing :: Markup :: Markdown
23
25
  Requires-Python: >=3.9
@@ -34,6 +36,7 @@ Requires-Dist: python-dateutil>=2.8.2
34
36
  Requires-Dist: pyyaml>=6.0
35
37
  Requires-Dist: requests>=2.25.0
36
38
  Requires-Dist: rich>=13.0.0
39
+ Requires-Dist: ruamel-yaml>=0.18.0
37
40
  Requires-Dist: typer>=0.9.0
38
41
  Provides-Extra: dev
39
42
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
@@ -282,6 +285,7 @@ Auth resolution order: `credentials_path` → `ACCOUNT_A_CREDENTIALS[_PATH]` env
282
285
  | `backend` | string | — | yes | `"google_docs"` or `"confluence"` |
283
286
  | `remote_id` | string | — | yes | Google Doc ID or Confluence page ID |
284
287
  | `direction` | enum | `"both"` | no | `"push"`, `"pull"`, or `"both"` |
288
+ | `tab_id` | string | `null` | no | Google Docs tab id (e.g. `"t.moqlkhpwn82e"`) to target on a multi-tab doc. Ignored by the Confluence backend. If unset and the doc has more than one tab, push/pull still succeed but report a `"warning"` naming the tabs found, instead of silently syncing whichever tab is first. |
285
289
 
286
290
  ---
287
291
 
@@ -303,14 +307,17 @@ docspan generates these files in your project directory after first sync:
303
307
  > [!NOTE]
304
308
  > **Known limitations in v0.1.0**
305
309
  >
306
- > - 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.
307
- > - 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
308
314
  > - Push: no table support — markdown tables are not rendered in Google Docs
309
315
  > - Confluence: requires an Atlassian API token; no OAuth flow
310
316
  > - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
311
317
  > - Checklist state (`- [ ]`/`- [x]`) round-trips as literal text — Google Docs' native checkbox glyph is intentionally not used because its checked/unchecked state cannot be read back via the API (see ADR-001)
312
318
  > - `push --dry-run` now shows a real structural diff and flags paragraphs with open comments at risk; `push` blocks by default on a flagged paragraph unless `--force` is passed
313
319
  > - If a push succeeds but a post-push check finds the open-comment count dropped, docspan reports this as a `⚠` warning — never a plain green success — so it's never mistaken for a clean push
320
+ > - Google Docs: multi-tab docs need `tab_id` set explicitly per mapping — find it in the doc's URL (`...#tab=t.XXXXXXXXXX` after clicking the tab) or from the warning message docspan prints when `tab_id` is left unset. Without it, pull/push default to the doc's first tab (`pull`'s default path additionally can't target a tab at all — it uses Drive's HTML export, which only ever returns the first tab; set `tab_id` to instead pull via the structural API, which can target any tab)
314
321
  > - Google Docs OAuth requires each user to create their own GCP project (`docspan auth setup google_docs` → Personal/OAuth) and stays in Google's "Testing" publishing status — capped at 100 test users, with Google's "app isn't verified" warning shown on first sign-in. This avoids the annual CASA security assessment required to verify apps requesting Drive/Docs' restricted read-write scopes (a real recurring cost), at the price of a few extra manual setup minutes per user instead of a single embedded, zero-config client. Revisit if/when adoption outgrows a per-user-project model — options are paying for verification, or narrowing to the unrestricted `drive.file` scope via Google's Picker API (bigger rework: requires the user to explicitly select their doc through a picker rather than referencing it by ID in config)
315
322
 
316
323
  ---
@@ -233,6 +233,7 @@ Auth resolution order: `credentials_path` → `ACCOUNT_A_CREDENTIALS[_PATH]` env
233
233
  | `backend` | string | — | yes | `"google_docs"` or `"confluence"` |
234
234
  | `remote_id` | string | — | yes | Google Doc ID or Confluence page ID |
235
235
  | `direction` | enum | `"both"` | no | `"push"`, `"pull"`, or `"both"` |
236
+ | `tab_id` | string | `null` | no | Google Docs tab id (e.g. `"t.moqlkhpwn82e"`) to target on a multi-tab doc. Ignored by the Confluence backend. If unset and the doc has more than one tab, push/pull still succeed but report a `"warning"` naming the tabs found, instead of silently syncing whichever tab is first. |
236
237
 
237
238
  ---
238
239
 
@@ -254,14 +255,17 @@ docspan generates these files in your project directory after first sync:
254
255
  > [!NOTE]
255
256
  > **Known limitations in v0.1.0**
256
257
  >
257
- > - 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.
258
- > - 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
259
262
  > - Push: no table support — markdown tables are not rendered in Google Docs
260
263
  > - Confluence: requires an Atlassian API token; no OAuth flow
261
264
  > - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
262
265
  > - Checklist state (`- [ ]`/`- [x]`) round-trips as literal text — Google Docs' native checkbox glyph is intentionally not used because its checked/unchecked state cannot be read back via the API (see ADR-001)
263
266
  > - `push --dry-run` now shows a real structural diff and flags paragraphs with open comments at risk; `push` blocks by default on a flagged paragraph unless `--force` is passed
264
267
  > - If a push succeeds but a post-push check finds the open-comment count dropped, docspan reports this as a `⚠` warning — never a plain green success — so it's never mistaken for a clean push
268
+ > - Google Docs: multi-tab docs need `tab_id` set explicitly per mapping — find it in the doc's URL (`...#tab=t.XXXXXXXXXX` after clicking the tab) or from the warning message docspan prints when `tab_id` is left unset. Without it, pull/push default to the doc's first tab (`pull`'s default path additionally can't target a tab at all — it uses Drive's HTML export, which only ever returns the first tab; set `tab_id` to instead pull via the structural API, which can target any tab)
265
269
  > - Google Docs OAuth requires each user to create their own GCP project (`docspan auth setup google_docs` → Personal/OAuth) and stays in Google's "Testing" publishing status — capped at 100 test users, with Google's "app isn't verified" warning shown on first sign-in. This avoids the annual CASA security assessment required to verify apps requesting Drive/Docs' restricted read-write scopes (a real recurring cost), at the price of a few extra manual setup minutes per user instead of a single embedded, zero-config client. Revisit if/when adoption outgrows a per-user-project model — options are paying for verification, or narrowing to the unrestricted `drive.file` scope via Google's Picker API (bigger rework: requires the user to explicitly select their doc through a picker rather than referencing it by ID in config)
266
270
 
267
271
  ---
@@ -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
@@ -37,6 +37,10 @@ The Google Docs push path drops two things, both blocking real-world design-doc
37
37
 
38
38
  ### Tables (Part 2)
39
39
  - New `DocsTableNode(rows: List[List[str]], start_index, end_index)` in the structure module.
40
+ - **Superseded.** `rows` is now `List[List[TableCell]]`, where `TableCell` carries
41
+ `text` *and* `spans`. Plain-text cells meant every mark inside one was dropped —
42
+ including internal `#anchor` cross-references, which rendered as dead text in the
43
+ Doc while the identical reference in a paragraph resolved. See #49.
40
44
  - Parser emits `DocsTableNode` for `table` tokens (header row + body rows, cell = plain text).
41
45
  - `DocsStructureParser` parses live `table` elements into `DocsTableNode` (rows from
42
46
  `table.tableRows[].tableCells[].content` paragraphs), with real start/end indices.
@@ -12,7 +12,7 @@
12
12
  Research (`research/stack.md` §2, `research/build-vs-buy.md` §1c, cross-checked against multiple independent sources — Google's own REST reference, a tanaikech Apps Script gist, a Latenode community thread) confirms:
13
13
 
14
14
  - The Docs API's `createParagraphBullets` request does support a distinct `bulletPreset: "BULLET_CHECKBOX"` value — checkboxes *can* be created programmatically.
15
- - **Checked/unchecked state cannot be read back.** `documents.get()` returns `glyphType: GLYPH_TYPE_UNSPECIFIED` for a checkbox bullet regardless of whether it is checked or unchecked. The JSON is reported identical across a check/uncheck action — the only observed difference is the document's `revisionId`, which carries no state information. This is confirmed symmetric between the REST API and the Apps Script `DocumentApp` service, meaning it is a limitation of the underlying document model, not an SDK gap.
15
+ - **`documents.get()` cannot expose checked/unchecked state.** It returns `glyphType: GLYPH_TYPE_UNSPECIFIED` for a checkbox bullet regardless of whether it is checked or unchecked. The JSON is reported identical across a check/uncheck action — the only observed difference is the document's `revisionId`, which carries no state information. This is confirmed symmetric between the REST API and the Apps Script `DocumentApp` service, meaning it is a limitation of that document model surface, not an SDK gap. (Later finding, see docspan issue "pull silently reports every checked box as unchecked": Drive's `files.export(mimeType='text/markdown')` is a *different* renderer that does expose it, as GFM `- [x]`/`- [ ]` — this is now used to recover state on docspan's default, non-tab-scoped pull path. It cannot target a single tab, so the tab-scoped pull path remains genuinely unrecoverable and continues to render every native checkbox unchecked.)
16
16
  - The only indirect, *unofficial* signal is `textStyle.strikethrough` (checking a box in the UI applies strikethrough as a side effect) — but this is a UI convention, not a documented API contract, and Google Docs now offers a "checklist without strikethrough" UI option that breaks even this heuristic.
17
17
 
18
18
  Options considered:
@@ -27,9 +27,9 @@ Use **Option 2 — literal text** as the default, document-wide representation.
27
27
 
28
28
  **If the full-document survey (Task 0.1.2a) finds a mix of literal-text and native-glyph checklist paragraphs (finding (c) — the realistic case for a multi-author doc), this decision does not apply uniformly.** The literal-text scheme remains the default assumed representation, but any specific paragraph the survey identified as already being a native `BULLET_CHECKBOX` glyph is treated as **high-risk / requires manual handling**, not silently assumed to behave like the rest of the doc:
29
29
 
30
- - Those paragraphs are listed by text prefix in `feature-gap-report.md` as known native-checkbox lines docspan does not track.
31
- - Pull emits a `MixedChecklistWarning` (a `WARN`-level log line, plus a trailing marker comment in the pulled markdown, e.g. `<!-- docspan: native checkbox glyph, state not readable -->`) whenever it encounters a bullet paragraph whose resolved glyph is checkbox-shaped, so the blind spot is visible on every pull, not just documented once here and then forgotten.
32
- - Tyler continues toggling those specific lines by hand in the Docs UI; docspan's pull renders them as plain, unmarked bulleted text (no marker corruption, just invisible state) until a future cycle designs real conversion support.
30
+ - Those paragraphs are listed by text prefix in `feature-gap-report.md` as known native-checkbox lines docspan does not track as literal text.
31
+ - Pull emits a `MixedChecklistWarning` (a `WARN`-level log line, plus a trailing marker comment in the pulled markdown, e.g. `<!-- docspan: native checkbox glyph, state not readable -->`) whenever it encounters a bullet paragraph whose resolved glyph is checkbox-shaped, so the blind spot is visible on every pull, not just documented once here and then forgotten. **This warning path is superseded on the default pull path** by the markdown-export recovery described above: when the recovered count matches and every line is matched, pull instead renders the real `- [x]`/`- [ ]` state with no warning at all. The warning (now phrased as a checkbox-count/match warning rather than an absolute "not readable" claim) still fires whenever that recovery can't proceed safely — a checkbox-count mismatch, an unmatched line, or a markdown-export transport failure — and always fires on the tab-scoped path, where recovery is never attempted.
32
+ - On the tab-scoped path specifically, Tyler continues toggling those specific lines by hand in the Docs UI; docspan's pull renders them as plain, unmarked bulleted text (no marker corruption, just invisible state) until a future cycle designs real conversion support for that path too.
33
33
  - **As of the pre-mortem repair pass, `push()` itself also refuses (fail-closed, `--force`-gated) to write through any of these specific native-glyph paragraphs.** A live `GlyphShapeCheck`, folded into `find_high_risk_paragraphs()` alongside the existing comment-risk check, re-resolves each changed paragraph's glyph shape from `push()`'s own single fetch — not from this ADR's static survey table — and flags it the same way an open-comment paragraph is flagged (same `HighRiskParagraph`/`--force` mechanism, see plan.md Story 1.2.2/1.2.3). This closes pre-mortem.md #1: without it, checking off a paragraph the survey found to be a native glyph would layer a literal `[x]`/`[ ]` marker onto that glyph with no warning and no `--force` requirement.
34
34
 
35
35
  ## Rationale
@@ -57,7 +57,7 @@ Use **Option 2 — literal text** as the default, document-wide representation.
57
57
 
58
58
  ## Consequences
59
59
 
60
- - No new fields are added to `DocsParagraphNode` (no `checked: Optional[bool]`), keeping the diff key (`style, text, is_list_item`) and all downstream request-building logic untouched.
60
+ - No new fields are added to `DocsParagraphNode` (no `checked: Optional[bool]`), keeping the diff key (`style, text, is_list_item`) and all downstream request-building logic untouched. Native-checkbox checked-state recovery on pull (see above) is implemented as a separate, purely functional post-processing step (`checkbox_state.py`) over the already-rendered markdown string, not as a new node field — this decision's diff-key claim still holds.
61
61
  - A checklist toggle always produces a `deleteContentRange` + `insertText` (delete+reinsert) for that paragraph, exactly like any other text edit to that paragraph — this is the same mechanism that risks dropping an anchored comment on that paragraph (see ADR-002 for the mitigation).
62
- - If a future cycle wants native checkbox glyphs purely for visual polish (not as source of truth), that can be added later as a strictly additive, cosmetic push-time flag without touching this decision but must never become the mechanism used to detect checked/unchecked state on pull.
63
- - **If the survey finds a mixed document (finding (c)):** the literal-text scheme is not a safe blanket assumption — specific paragraphs are exceptions, tracked by name, and flagged loudly (not silently) on every pull via `MixedChecklistWarning`. Tyler must treat any doc-wide claim like "checklist state round-trips correctly" (Success Metric 3) as true only for the literal-text paragraphs, not the whole document, until a future cycle closes this gap. Since the pre-mortem repair pass, `push()` also refuses to write through one of these specific paragraphs without `--force` (`GlyphShapeCheck`, plan.md Story 1.2.2/1.2.3) — the mixed-doc case is now guarded at write time, not only disclosed at read time via `MixedChecklistWarning`.
62
+ - If a future cycle wants native checkbox glyphs purely for visual polish (not as source of truth), that can be added later as a strictly additive, cosmetic push-time flag without touching this decision. Push behavior for native checkboxes remains unchanged by the pull-side recovery above: an edit to one of these paragraphs is still routed through the existing `GlyphShapeCheck` high-risk gate, never silently applied.
63
+ - **If the survey finds a mixed document (finding (c)):** the literal-text scheme is not a safe blanket assumption — specific paragraphs are exceptions, tracked by name, and flagged loudly (not silently) on every pull via `MixedChecklistWarning` whenever native-checkbox state can't be safely recovered (mismatch, transport failure, or the tab-scoped path). Tyler must treat any doc-wide claim like "checklist state round-trips correctly" (Success Metric 3) as true for the literal-text paragraphs, and — on the default pull path, when recovery succeeds for native-checkbox paragraphs too; only the tab-scoped path remains an unconditional gap. Since the pre-mortem repair pass, `push()` also refuses to write through one of these specific paragraphs without `--force` (`GlyphShapeCheck`, plan.md Story 1.2.2/1.2.3) — the mixed-doc case is guarded at write time and, for the default pull path, largely resolved at read time.