docspan 0.1.0__tar.gz → 0.2.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 (169) hide show
  1. {docspan-0.1.0 → docspan-0.2.0}/.github/workflows/publish.yml +7 -3
  2. docspan-0.2.0/.release-please-manifest.json +3 -0
  3. docspan-0.2.0/CHANGELOG.md +67 -0
  4. {docspan-0.1.0 → docspan-0.2.0}/PKG-INFO +59 -9
  5. {docspan-0.1.0 → docspan-0.2.0}/README.md +57 -7
  6. {docspan-0.1.0 → docspan-0.2.0}/markgate.yaml.example +5 -0
  7. docspan-0.2.0/project_plans/bidirectional-comments/plan.md +212 -0
  8. docspan-0.2.0/project_plans/gdocs-tables-inline-styles/plan.md +59 -0
  9. docspan-0.2.0/project_plans/wedding-planning-workflow/decisions/ADR-001-checklist-state-as-literal-text.md +63 -0
  10. docspan-0.2.0/project_plans/wedding-planning-workflow/decisions/ADR-002-comment-risk-flagging-not-anchor-preservation.md +46 -0
  11. docspan-0.2.0/project_plans/wedding-planning-workflow/feature-gap-report.md +84 -0
  12. docspan-0.2.0/project_plans/wedding-planning-workflow/implementation/adversarial-review.md +39 -0
  13. docspan-0.2.0/project_plans/wedding-planning-workflow/implementation/architecture-review.md +31 -0
  14. docspan-0.2.0/project_plans/wedding-planning-workflow/implementation/plan.md +646 -0
  15. docspan-0.2.0/project_plans/wedding-planning-workflow/implementation/pre-mortem.md +22 -0
  16. docspan-0.2.0/project_plans/wedding-planning-workflow/implementation/validation.md +78 -0
  17. docspan-0.2.0/project_plans/wedding-planning-workflow/requirements.md +89 -0
  18. docspan-0.2.0/project_plans/wedding-planning-workflow/research/architecture.md +272 -0
  19. docspan-0.2.0/project_plans/wedding-planning-workflow/research/build-vs-buy.md +245 -0
  20. docspan-0.2.0/project_plans/wedding-planning-workflow/research/features.md +247 -0
  21. docspan-0.2.0/project_plans/wedding-planning-workflow/research/pitfalls.md +94 -0
  22. docspan-0.2.0/project_plans/wedding-planning-workflow/research/stack.md +75 -0
  23. docspan-0.2.0/project_plans/wedding-planning-workflow/research/ux.md +106 -0
  24. docspan-0.2.0/project_plans/wedding-planning-workflow/workflow-runbook.md +290 -0
  25. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/base.py +6 -3
  26. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/backend.py +1 -1
  27. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/google_docs/auth.py +95 -0
  28. docspan-0.2.0/src/docspan/backends/google_docs/backend.py +473 -0
  29. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/google_docs/client.py +102 -2
  30. docspan-0.2.0/src/docspan/backends/google_docs/comments.py +124 -0
  31. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/google_docs/converter.py +59 -4
  32. docspan-0.2.0/src/docspan/backends/google_docs/docs_request_builder.py +529 -0
  33. docspan-0.2.0/src/docspan/backends/google_docs/docs_structure_parser.py +212 -0
  34. docspan-0.2.0/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +263 -0
  35. docspan-0.2.0/src/docspan/backends/google_docs/onboarding.py +94 -0
  36. docspan-0.2.0/src/docspan/backends/google_docs/push_preview.py +201 -0
  37. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/cli/main.py +281 -25
  38. docspan-0.2.0/src/docspan/config.py +129 -0
  39. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/core/orchestrator.py +10 -5
  40. docspan-0.2.0/src/docspan/core/xdg.py +59 -0
  41. docspan-0.2.0/terraform/gcp/README.md +23 -0
  42. docspan-0.2.0/terraform/gcp/main.tf +39 -0
  43. docspan-0.2.0/terraform/gcp/outputs.tf +23 -0
  44. docspan-0.2.0/terraform/gcp/variables.tf +17 -0
  45. docspan-0.2.0/tests/conftest.py +55 -0
  46. {docspan-0.1.0 → docspan-0.2.0}/tests/test_cli.py +220 -5
  47. docspan-0.2.0/tests/test_converter.py +51 -0
  48. docspan-0.2.0/tests/test_docs_request_builder.py +378 -0
  49. {docspan-0.1.0 → docspan-0.2.0}/tests/test_docs_structure_parser.py +136 -0
  50. docspan-0.2.0/tests/test_gdocs_tables_and_styles.py +237 -0
  51. docspan-0.2.0/tests/test_google_comments.py +253 -0
  52. docspan-0.2.0/tests/test_google_docs_backend.py +340 -0
  53. docspan-0.2.0/tests/test_google_oauth.py +122 -0
  54. docspan-0.2.0/tests/test_google_onboarding.py +121 -0
  55. {docspan-0.1.0 → docspan-0.2.0}/tests/test_markdown_to_paragraph_parser.py +56 -0
  56. docspan-0.2.0/tests/test_push_preview.py +338 -0
  57. docspan-0.2.0/tests/test_xdg_central_config.py +116 -0
  58. docspan-0.1.0/.release-please-manifest.json +0 -3
  59. docspan-0.1.0/CHANGELOG.md +0 -35
  60. docspan-0.1.0/src/docspan/backends/google_docs/backend.py +0 -140
  61. docspan-0.1.0/src/docspan/backends/google_docs/docs_request_builder.py +0 -232
  62. docspan-0.1.0/src/docspan/backends/google_docs/docs_structure_parser.py +0 -120
  63. docspan-0.1.0/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +0 -145
  64. docspan-0.1.0/src/docspan/config.py +0 -62
  65. docspan-0.1.0/tests/test_docs_request_builder.py +0 -130
  66. {docspan-0.1.0 → docspan-0.2.0}/.github/workflows/ci.yml +0 -0
  67. {docspan-0.1.0 → docspan-0.2.0}/.github/workflows/release-please.yml +0 -0
  68. {docspan-0.1.0 → docspan-0.2.0}/.gitignore +0 -0
  69. {docspan-0.1.0 → docspan-0.2.0}/CONTRIBUTING.md +0 -0
  70. {docspan-0.1.0 → docspan-0.2.0}/Procfile +0 -0
  71. {docspan-0.1.0 → docspan-0.2.0}/RAILWAY_SETUP.md +0 -0
  72. {docspan-0.1.0 → docspan-0.2.0}/docs/backends/confluence.md +0 -0
  73. {docspan-0.1.0 → docspan-0.2.0}/docs/backends/google-docs.md +0 -0
  74. {docspan-0.1.0 → docspan-0.2.0}/docs/commands.md +0 -0
  75. {docspan-0.1.0 → docspan-0.2.0}/docs/configuration.md +0 -0
  76. {docspan-0.1.0 → docspan-0.2.0}/docs/contributing.md +0 -0
  77. {docspan-0.1.0 → docspan-0.2.0}/docs/index.md +0 -0
  78. {docspan-0.1.0 → docspan-0.2.0}/docs/install.md +0 -0
  79. {docspan-0.1.0 → docspan-0.2.0}/docspan.yaml.example +0 -0
  80. {docspan-0.1.0 → docspan-0.2.0}/mkdocs.yml +0 -0
  81. {docspan-0.1.0 → docspan-0.2.0}/modules/__init__.py +0 -0
  82. {docspan-0.1.0 → docspan-0.2.0}/modules/auth.py +0 -0
  83. {docspan-0.1.0 → docspan-0.2.0}/modules/conflict_handler.py +0 -0
  84. {docspan-0.1.0 → docspan-0.2.0}/modules/converter.py +0 -0
  85. {docspan-0.1.0 → docspan-0.2.0}/modules/gdrive_client.py +0 -0
  86. {docspan-0.1.0 → docspan-0.2.0}/modules/sync_engine.py +0 -0
  87. {docspan-0.1.0 → docspan-0.2.0}/project_plans/docspan-release/implementation/adversarial-review.md +0 -0
  88. {docspan-0.1.0 → docspan-0.2.0}/project_plans/docspan-release/implementation/plan.md +0 -0
  89. {docspan-0.1.0 → docspan-0.2.0}/project_plans/docspan-release/implementation/release-checklist.md +0 -0
  90. {docspan-0.1.0 → docspan-0.2.0}/project_plans/docspan-release/implementation/validation.md +0 -0
  91. {docspan-0.1.0 → docspan-0.2.0}/project_plans/docspan-release/requirements.md +0 -0
  92. {docspan-0.1.0 → docspan-0.2.0}/project_plans/docspan-release/research/architecture.md +0 -0
  93. {docspan-0.1.0 → docspan-0.2.0}/project_plans/docspan-release/research/features.md +0 -0
  94. {docspan-0.1.0 → docspan-0.2.0}/project_plans/docspan-release/research/google-docs-push.md +0 -0
  95. {docspan-0.1.0 → docspan-0.2.0}/project_plans/docspan-release/research/pitfalls.md +0 -0
  96. {docspan-0.1.0 → docspan-0.2.0}/project_plans/docspan-release/research/stack.md +0 -0
  97. {docspan-0.1.0 → docspan-0.2.0}/project_plans/markgate-sync/decisions/ADR-001-merge3-dependency.md +0 -0
  98. {docspan-0.1.0 → docspan-0.2.0}/project_plans/markgate-sync/decisions/ADR-002-base-content-sidecar-store.md +0 -0
  99. {docspan-0.1.0 → docspan-0.2.0}/project_plans/markgate-sync/implementation/adversarial-review.md +0 -0
  100. {docspan-0.1.0 → docspan-0.2.0}/project_plans/markgate-sync/implementation/plan.md +0 -0
  101. {docspan-0.1.0 → docspan-0.2.0}/project_plans/markgate-sync/implementation/validation.md +0 -0
  102. {docspan-0.1.0 → docspan-0.2.0}/project_plans/markgate-sync/requirements.md +0 -0
  103. {docspan-0.1.0 → docspan-0.2.0}/project_plans/markgate-sync/research/architecture.md +0 -0
  104. {docspan-0.1.0 → docspan-0.2.0}/project_plans/markgate-sync/research/features.md +0 -0
  105. {docspan-0.1.0 → docspan-0.2.0}/project_plans/markgate-sync/research/pitfalls.md +0 -0
  106. {docspan-0.1.0 → docspan-0.2.0}/project_plans/markgate-sync/research/stack.md +0 -0
  107. {docspan-0.1.0 → docspan-0.2.0}/pyproject.toml +0 -0
  108. {docspan-0.1.0 → docspan-0.2.0}/release-please-config.json +0 -0
  109. {docspan-0.1.0 → docspan-0.2.0}/requirements.txt +0 -0
  110. {docspan-0.1.0 → docspan-0.2.0}/runtime.txt +0 -0
  111. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/__init__.py +0 -0
  112. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/__main__.py +0 -0
  113. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/__init__.py +0 -0
  114. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/__init__.py +0 -0
  115. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/adf/__init__.py +0 -0
  116. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/adf/comparator.py +0 -0
  117. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/adf/converter.py +0 -0
  118. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/adf/converters.py +0 -0
  119. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/adf/interfaces.py +0 -0
  120. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/adf/nodes.py +0 -0
  121. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/adf/parser.py +0 -0
  122. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/adf/validators.py +0 -0
  123. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/adf/visitors.py +0 -0
  124. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/client.py +0 -0
  125. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/config/__init__.py +0 -0
  126. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/config/loader.py +0 -0
  127. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/config/models.py +0 -0
  128. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/config/validation.py +0 -0
  129. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/markdown/__init__.py +0 -0
  130. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/markdown/ast.py +0 -0
  131. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/markdown/extensions/__init__.py +0 -0
  132. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/markdown/extensions/frontmatter.py +0 -0
  133. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/markdown/extensions/mermaid.py +0 -0
  134. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/markdown/extensions/wikilinks.py +0 -0
  135. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/markdown/inline_parser.py +0 -0
  136. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/markdown/parser.py +0 -0
  137. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/models/__init__.py +0 -0
  138. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/models/markdown_file.py +0 -0
  139. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/models/page.py +0 -0
  140. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/models/path_utils.py +0 -0
  141. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/models/results.py +0 -0
  142. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/models/sync_status.py +0 -0
  143. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/services/__init__.py +0 -0
  144. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/services/confluence/__init__.py +0 -0
  145. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/services/confluence/attachment_client.py +0 -0
  146. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/services/confluence/base_client.py +0 -0
  147. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/services/confluence/client.py +0 -0
  148. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/services/confluence/comment_client.py +0 -0
  149. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/services/confluence/crawler.py +0 -0
  150. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/services/confluence/label_client.py +0 -0
  151. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/services/confluence/page_client.py +0 -0
  152. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/services/confluence/space_client.py +0 -0
  153. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/confluence/services/confluence/url_parser.py +0 -0
  154. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/backends/google_docs/__init__.py +0 -0
  155. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/cli/__init__.py +0 -0
  156. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/core/__init__.py +0 -0
  157. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/core/merge.py +0 -0
  158. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/core/paths.py +0 -0
  159. {docspan-0.1.0 → docspan-0.2.0}/src/docspan/core/state.py +0 -0
  160. {docspan-0.1.0 → docspan-0.2.0}/sync.py +0 -0
  161. {docspan-0.1.0 → docspan-0.2.0}/terraform/main.tf +0 -0
  162. {docspan-0.1.0 → docspan-0.2.0}/terraform/variables.tf +0 -0
  163. {docspan-0.1.0 → docspan-0.2.0}/tests/__init__.py +0 -0
  164. {docspan-0.1.0 → docspan-0.2.0}/tests/test_config.py +0 -0
  165. {docspan-0.1.0 → docspan-0.2.0}/tests/test_conflict_resolution.py +0 -0
  166. {docspan-0.1.0 → docspan-0.2.0}/tests/test_merge.py +0 -0
  167. {docspan-0.1.0 → docspan-0.2.0}/tests/test_orchestrator.py +0 -0
  168. {docspan-0.1.0 → docspan-0.2.0}/tests/test_state.py +0 -0
  169. {docspan-0.1.0 → docspan-0.2.0}/uv.lock +0 -0
@@ -18,7 +18,7 @@ jobs:
18
18
  - name: Build package
19
19
  run: uv build
20
20
 
21
- - uses: actions/upload-artifact@v4
21
+ - uses: actions/upload-artifact@v4.6.2
22
22
  with:
23
23
  name: dist
24
24
  path: dist/
@@ -31,8 +31,10 @@ jobs:
31
31
  url: https://test.pypi.org/p/docspan
32
32
  permissions:
33
33
  id-token: write
34
+ env:
35
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
34
36
  steps:
35
- - uses: actions/download-artifact@v4
37
+ - uses: actions/download-artifact@v4.3.0
36
38
  with:
37
39
  name: dist
38
40
  path: dist/
@@ -49,8 +51,10 @@ jobs:
49
51
  url: https://pypi.org/p/docspan
50
52
  permissions:
51
53
  id-token: write
54
+ env:
55
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
52
56
  steps:
53
- - uses: actions/download-artifact@v4
57
+ - uses: actions/download-artifact@v4.3.0
54
58
  with:
55
59
  name: dist
56
60
  path: dist/
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.2.0"
3
+ }
@@ -0,0 +1,67 @@
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.2.0](https://github.com/tstapler/docspan/compare/docspan-v0.1.0...docspan-v0.2.0) (2026-07-22)
9
+
10
+
11
+ ### Features
12
+
13
+ * Add Railway Volume support for persistent state storage ([3a4b76e](https://github.com/tstapler/docspan/commit/3a4b76eb0cbc14afa02f3aa3de2c4607808fad9f))
14
+ * Add retry mechanism and improved error handling for Google Drive API ([e8a7b5f](https://github.com/tstapler/docspan/commit/e8a7b5f177ad2c3b8a356852eceb827326f8ce76))
15
+ * Auto-reload Google Sheet mappings on each sync cycle ([a2647c6](https://github.com/tstapler/docspan/commit/a2647c6c1435eb5624a389a4e673a57ead012123))
16
+ * **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))
17
+ * **confluence:** port adf/markdown/services from markdown-confluence ([e9d1a85](https://github.com/tstapler/docspan/commit/e9d1a85a9747ac75a6d95d6351d18483297726a4))
18
+ * **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))
19
+ * **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))
20
+ * **google-docs:** add per-user OAuth auth option ([#4](https://github.com/tstapler/docspan/issues/4)) ([830369c](https://github.com/tstapler/docspan/commit/830369cba1817224ae0d02f0b14b6a84de84a4eb))
21
+ * **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))
22
+ * **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))
23
+ * scaffold markgate package from google-docs-obsidian-sync fork ([44dd3c5](https://github.com/tstapler/docspan/commit/44dd3c586a670b4689154b2db9bc6cb8673d9702))
24
+ * **sync:** Google Docs structural-diff push, Confluence comments, three-way merge ([9a20e34](https://github.com/tstapler/docspan/commit/9a20e3452f2d92a240128d7c8e2f9c4b63a547f9))
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * **ci:** add __future__ annotations for Python 3.9 compat in test ([9ceca65](https://github.com/tstapler/docspan/commit/9ceca65ca5ad2040c1c2ec215fc097b02ba1a0c4))
30
+ * **ci:** apply ruff autofix across all src and test files ([bee2784](https://github.com/tstapler/docspan/commit/bee2784b940d872045ce33faf0ce53f65150d80d))
31
+ * **ci:** resolve ruff lint failures and enable Actions PR creation ([8727e7b](https://github.com/tstapler/docspan/commit/8727e7bcabf1ddec4b4116b06e605ff24d0eaffe))
32
+ * **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))
33
+ * **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))
34
+ * **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))
35
+ * **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))
36
+ * Improve nested list indentation in Google Docs to Markdown conversion ([d6a7539](https://github.com/tstapler/docspan/commit/d6a7539d4beade3426e5d0db838f1d9974f7294b))
37
+ * Remove CONFIG_YAML dependency, prefer individual env vars ([d5d5d4a](https://github.com/tstapler/docspan/commit/d5d5d4ae25a7c1d7a213071f968eba56a9289da3))
38
+ * Resolve service account storage quota error by storing sync state locally ([00e9cb6](https://github.com/tstapler/docspan/commit/00e9cb65033dfb6cca8e0aae2258cde458cfb342))
39
+
40
+ ## [Unreleased]
41
+
42
+ ## [0.1.0] - 2026-06-07
43
+
44
+ ### Added
45
+ - `docspan push` — push local markdown files to Google Docs or Confluence
46
+ - `docspan pull` — pull remote documents into local markdown files with three-way merge
47
+ - `docspan status` — show current mapping status in a table
48
+ - `docspan auth setup` — interactive authentication setup for `google_docs` and `confluence` backends
49
+ - `docspan conflicts list` — list files with unresolved merge conflicts
50
+ - `docspan conflicts resolve` — resolve merge conflicts with `remote`, `local`, or `merged` strategy
51
+ - Google Docs backend: push and pull via Google Docs API (service account auth)
52
+ - Confluence backend: push and pull via Atlassian REST API (API token auth)
53
+ - Three-way merge for bidirectional sync conflict detection
54
+ - Confluence comment sidecar: pull writes inline and footer comments to `{file}.comments.md`
55
+ - `markgate.yaml` config file format with per-mapping direction control (`push`/`pull`/`both`)
56
+ - Sync state tracking via `.markgate-state.json` and content-addressed base store in `.markgate-base/`
57
+
58
+ ### Known Limitations
59
+ - Google Docs: comments on edited paragraphs are destroyed on push (paragraph-level diff; comments on unchanged paragraphs are preserved)
60
+ - Push: no image support — local image files cannot be pushed to Google Docs or Confluence
61
+ - Push: no table support — markdown tables are not rendered in Google Docs
62
+ - Confluence: requires an Atlassian API token; no OAuth flow
63
+ - Confluence: comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
64
+ - 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.
65
+
66
+ [Unreleased]: https://github.com/tstapler/docspan/compare/v0.1.0...HEAD
67
+ [0.1.0]: https://github.com/tstapler/docspan/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: docspan
3
- Version: 0.1.0
3
+ Version: 0.2.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
@@ -142,6 +142,38 @@ mappings:
142
142
 
143
143
  ---
144
144
 
145
+ ## Central config & XDG storage
146
+
147
+ By default docspan stores its config, sync state, and credentials under the [XDG base directories](https://specifications.freedesktop.org/basedir-spec/latest/), and a **central config** lets you register multiple projects by *prefix* and run docspan from anywhere.
148
+
149
+ ```
150
+ $XDG_CONFIG_HOME/docspan/config.yaml # central config (project registry)
151
+ $XDG_CONFIG_HOME/docspan/<prefix>/… # cached OAuth token
152
+ $XDG_STATE_HOME/docspan/<prefix>/… # sync state + base store, per project
153
+ ```
154
+
155
+ Central config (`~/.config/docspan/config.yaml`):
156
+
157
+ ```yaml
158
+ default_prefix: design-docs
159
+ projects:
160
+ design-docs:
161
+ markgate: ~/Documents/design-docs/markgate.yaml
162
+ ```
163
+
164
+ Register and use projects:
165
+
166
+ ```bash
167
+ docspan config add design-docs ~/Documents/design-docs/markgate.yaml # register (prefix → markgate.yaml)
168
+ docspan config show # list projects + active resolution
169
+ docspan push --prefix design-docs # or DOCSPAN_PREFIX, or default_prefix, or cwd match
170
+ docspan migrate-xdg --prefix design-docs # move legacy in-repo state to XDG + register
171
+ ```
172
+
173
+ **Prefix resolution order:** `--config PATH` (legacy — storage stays beside the file) → `--prefix` → `DOCSPAN_PREFIX` → cwd inside a registered project → `default_prefix`. If nothing matches, docspan falls back to a local `./markgate.yaml` with beside-the-file storage (fully backward-compatible).
174
+
175
+ ---
176
+
145
177
  ## Command Reference
146
178
 
147
179
  ### `docspan push`
@@ -158,7 +190,7 @@ Push local markdown files to remote docs. Skips mappings with `direction = "pull
158
190
  docspan pull [FILES]... [--dry-run] [--config PATH]
159
191
  ```
160
192
 
161
- Pull remote documents into local markdown files with three-way merge. Writes conflict markers to the file if automatic merge fails.
193
+ Pull remote documents into local markdown files with three-way merge. Writes conflict markers to the file if automatic merge fails. For Google Docs, also writes a `{file}.comments.md` sidecar of the doc's comments (open + resolved, with quoted selections and reply threads) unless `pull_comments: false`.
162
194
 
163
195
  ### `docspan status`
164
196
 
@@ -176,7 +208,17 @@ docspan auth setup BACKEND [--config PATH]
176
208
 
177
209
  Interactive authentication setup. `BACKEND` is one of `google_docs` or `confluence`.
178
210
 
179
- For Google Docs, prints step-by-step service account setup instructions. For Confluence, prompts for base URL, username, and API token, then prints a YAML snippet to add to `markgate.yaml`.
211
+ For **Google Docs**, run it with no flags for a guided flow:
212
+
213
+ ```
214
+ docspan auth setup google_docs
215
+ ```
216
+
217
+ It detects your current state, lets you pick **Personal (OAuth)** [recommended] or **Service account**, auto-detects a `client_secret.json` (scanning `.`, `.markgate/`, `~/Downloads`) or prompts for the path with validation, runs the browser sign-in, verifies the connection, and offers to persist the choice into `markgate.yaml` so you never repeat it. In a non-TTY/CI environment it prints manual instructions instead of prompting.
218
+
219
+ Everything is scriptable — any answer can be supplied as a flag: `--oauth` / `--service-account`, `--client-secret PATH`, `--credentials PATH`. If a `docspan push`/`pull` runs without credentials in an interactive terminal, it offers to run setup inline and then continues.
220
+
221
+ For **Confluence**, prompts for base URL, username, and API token, then prints a YAML snippet to add to `markgate.yaml`.
180
222
 
181
223
  ### `docspan conflicts list`
182
224
 
@@ -208,10 +250,14 @@ Resolve a merge conflict in a tracked file.
208
250
 
209
251
  | Field | Type | Default | Description |
210
252
  |---|---|---|---|
211
- | `credentials_path` | string | null | Path to Google service account JSON key |
212
- | `token_path` | string | `.markgate/google_token.json` | OAuth token storage path |
253
+ | `credentials_path` | string | null | Path to a Google **service account** JSON key |
254
+ | `oauth_client_secret_path` | string | null | Path to an **OAuth client secret** JSON (Desktop app) for per-user auth |
255
+ | `token_path` | string | `$XDG_CONFIG_HOME/docspan/google_token.json` | Where the cached OAuth user token is stored/refreshed (out of the repo) |
256
+ | `pull_comments` | bool | `true` | On pull, write a `{file}.comments.md` sidecar of the doc's comments |
213
257
 
214
- **Environment variable alternatives:**
258
+ Auth resolution order: `credentials_path` → `ACCOUNT_A_CREDENTIALS[_PATH]` env → per-user OAuth (`oauth_client_secret_path`, or an already-cached `token_path`).
259
+
260
+ **Environment variable alternatives (service account):**
215
261
  - `ACCOUNT_A_CREDENTIALS_PATH` — path to service account JSON
216
262
  - `ACCOUNT_A_CREDENTIALS` — inline service account JSON string
217
263
 
@@ -248,7 +294,7 @@ docspan generates these files in your project directory after first sync:
248
294
  | `.markgate-state.json` | Sync state tracking (content hashes, remote versions) |
249
295
  | `.markgate-base/` | Content-addressed store of merge bases |
250
296
  | `{file}.orig` | Backup of local file before merge; deleted after conflict resolution |
251
- | `{file}.comments.md` | Confluence comment sidecar; written during pull if comments exist |
297
+ | `{file}.comments.md` | Comment sidecar (Google Docs + Confluence); written during pull if comments exist |
252
298
 
253
299
  ---
254
300
 
@@ -257,11 +303,15 @@ docspan generates these files in your project directory after first sync:
257
303
  > [!NOTE]
258
304
  > **Known limitations in v0.1.0**
259
305
  >
260
- > - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved)
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.
261
307
  > - Push: no image support — local images cannot be pushed to Google Docs or Confluence
262
308
  > - Push: no table support — markdown tables are not rendered in Google Docs
263
309
  > - Confluence: requires an Atlassian API token; no OAuth flow
264
310
  > - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
311
+ > - 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
+ > - `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
+ > - 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
314
+ > - 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)
265
315
 
266
316
  ---
267
317
 
@@ -93,6 +93,38 @@ mappings:
93
93
 
94
94
  ---
95
95
 
96
+ ## Central config & XDG storage
97
+
98
+ By default docspan stores its config, sync state, and credentials under the [XDG base directories](https://specifications.freedesktop.org/basedir-spec/latest/), and a **central config** lets you register multiple projects by *prefix* and run docspan from anywhere.
99
+
100
+ ```
101
+ $XDG_CONFIG_HOME/docspan/config.yaml # central config (project registry)
102
+ $XDG_CONFIG_HOME/docspan/<prefix>/… # cached OAuth token
103
+ $XDG_STATE_HOME/docspan/<prefix>/… # sync state + base store, per project
104
+ ```
105
+
106
+ Central config (`~/.config/docspan/config.yaml`):
107
+
108
+ ```yaml
109
+ default_prefix: design-docs
110
+ projects:
111
+ design-docs:
112
+ markgate: ~/Documents/design-docs/markgate.yaml
113
+ ```
114
+
115
+ Register and use projects:
116
+
117
+ ```bash
118
+ docspan config add design-docs ~/Documents/design-docs/markgate.yaml # register (prefix → markgate.yaml)
119
+ docspan config show # list projects + active resolution
120
+ docspan push --prefix design-docs # or DOCSPAN_PREFIX, or default_prefix, or cwd match
121
+ docspan migrate-xdg --prefix design-docs # move legacy in-repo state to XDG + register
122
+ ```
123
+
124
+ **Prefix resolution order:** `--config PATH` (legacy — storage stays beside the file) → `--prefix` → `DOCSPAN_PREFIX` → cwd inside a registered project → `default_prefix`. If nothing matches, docspan falls back to a local `./markgate.yaml` with beside-the-file storage (fully backward-compatible).
125
+
126
+ ---
127
+
96
128
  ## Command Reference
97
129
 
98
130
  ### `docspan push`
@@ -109,7 +141,7 @@ Push local markdown files to remote docs. Skips mappings with `direction = "pull
109
141
  docspan pull [FILES]... [--dry-run] [--config PATH]
110
142
  ```
111
143
 
112
- Pull remote documents into local markdown files with three-way merge. Writes conflict markers to the file if automatic merge fails.
144
+ Pull remote documents into local markdown files with three-way merge. Writes conflict markers to the file if automatic merge fails. For Google Docs, also writes a `{file}.comments.md` sidecar of the doc's comments (open + resolved, with quoted selections and reply threads) unless `pull_comments: false`.
113
145
 
114
146
  ### `docspan status`
115
147
 
@@ -127,7 +159,17 @@ docspan auth setup BACKEND [--config PATH]
127
159
 
128
160
  Interactive authentication setup. `BACKEND` is one of `google_docs` or `confluence`.
129
161
 
130
- For Google Docs, prints step-by-step service account setup instructions. For Confluence, prompts for base URL, username, and API token, then prints a YAML snippet to add to `markgate.yaml`.
162
+ For **Google Docs**, run it with no flags for a guided flow:
163
+
164
+ ```
165
+ docspan auth setup google_docs
166
+ ```
167
+
168
+ It detects your current state, lets you pick **Personal (OAuth)** [recommended] or **Service account**, auto-detects a `client_secret.json` (scanning `.`, `.markgate/`, `~/Downloads`) or prompts for the path with validation, runs the browser sign-in, verifies the connection, and offers to persist the choice into `markgate.yaml` so you never repeat it. In a non-TTY/CI environment it prints manual instructions instead of prompting.
169
+
170
+ Everything is scriptable — any answer can be supplied as a flag: `--oauth` / `--service-account`, `--client-secret PATH`, `--credentials PATH`. If a `docspan push`/`pull` runs without credentials in an interactive terminal, it offers to run setup inline and then continues.
171
+
172
+ For **Confluence**, prompts for base URL, username, and API token, then prints a YAML snippet to add to `markgate.yaml`.
131
173
 
132
174
  ### `docspan conflicts list`
133
175
 
@@ -159,10 +201,14 @@ Resolve a merge conflict in a tracked file.
159
201
 
160
202
  | Field | Type | Default | Description |
161
203
  |---|---|---|---|
162
- | `credentials_path` | string | null | Path to Google service account JSON key |
163
- | `token_path` | string | `.markgate/google_token.json` | OAuth token storage path |
204
+ | `credentials_path` | string | null | Path to a Google **service account** JSON key |
205
+ | `oauth_client_secret_path` | string | null | Path to an **OAuth client secret** JSON (Desktop app) for per-user auth |
206
+ | `token_path` | string | `$XDG_CONFIG_HOME/docspan/google_token.json` | Where the cached OAuth user token is stored/refreshed (out of the repo) |
207
+ | `pull_comments` | bool | `true` | On pull, write a `{file}.comments.md` sidecar of the doc's comments |
164
208
 
165
- **Environment variable alternatives:**
209
+ Auth resolution order: `credentials_path` → `ACCOUNT_A_CREDENTIALS[_PATH]` env → per-user OAuth (`oauth_client_secret_path`, or an already-cached `token_path`).
210
+
211
+ **Environment variable alternatives (service account):**
166
212
  - `ACCOUNT_A_CREDENTIALS_PATH` — path to service account JSON
167
213
  - `ACCOUNT_A_CREDENTIALS` — inline service account JSON string
168
214
 
@@ -199,7 +245,7 @@ docspan generates these files in your project directory after first sync:
199
245
  | `.markgate-state.json` | Sync state tracking (content hashes, remote versions) |
200
246
  | `.markgate-base/` | Content-addressed store of merge bases |
201
247
  | `{file}.orig` | Backup of local file before merge; deleted after conflict resolution |
202
- | `{file}.comments.md` | Confluence comment sidecar; written during pull if comments exist |
248
+ | `{file}.comments.md` | Comment sidecar (Google Docs + Confluence); written during pull if comments exist |
203
249
 
204
250
  ---
205
251
 
@@ -208,11 +254,15 @@ docspan generates these files in your project directory after first sync:
208
254
  > [!NOTE]
209
255
  > **Known limitations in v0.1.0**
210
256
  >
211
- > - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved)
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.
212
258
  > - Push: no image support — local images cannot be pushed to Google Docs or Confluence
213
259
  > - Push: no table support — markdown tables are not rendered in Google Docs
214
260
  > - Confluence: requires an Atlassian API token; no OAuth flow
215
261
  > - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
262
+ > - 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
+ > - `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
+ > - 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
265
+ > - 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)
216
266
 
217
267
  ---
218
268
 
@@ -5,8 +5,13 @@
5
5
 
6
6
  backends:
7
7
  google_docs:
8
+ # Service-account auth (app / non-user):
8
9
  credentials_path: /path/to/service-account.json
9
10
  # or set ACCOUNT_A_CREDENTIALS_PATH env var
11
+ #
12
+ # Or per-user OAuth (acts as you, like gws — no service account):
13
+ # oauth_client_secret_path: /path/to/client_secret.json
14
+ # token_path: .markgate/google_token.json # cached user token (default)
10
15
 
11
16
  confluence:
12
17
  base_url: https://yourorg.atlassian.net
@@ -0,0 +1,212 @@
1
+ # SDD Plan: Bidirectional Google Docs Comments
2
+
3
+ **Status:** planning only (no code) · **Backend:** `google_docs` · **Related:** read-only comments reader (PR #5)
4
+
5
+ > Storage locations below (base snapshot, state) are shown at their current repo-relative paths. If the
6
+ > XDG-paths + central-config refactor lands first, these move under the XDG data/state root — the design
7
+ > is unchanged, only the root differs.
8
+
9
+ ---
10
+
11
+ ## 1. Requirements
12
+
13
+ ### Functional
14
+ - **FR1 — Pull others' comments** (exists in PR #5, being refactored): fetch all threads (open + resolved) with replies, quoted selection, author, timestamps; materialize locally.
15
+ - **FR2 — Reply to existing threads:** user writes a reply into a local thread file; `push` creates it via `replies.create` on the parent comment.
16
+ - **FR3 — Add new comments:** `push` creates via `comments.create`. On Google Docs these land **unanchored** (ADR-004).
17
+ - **FR4 — Resolve / reopen:** `push` applies via `replies.create` with `action=resolve|reopen`.
18
+ - **FR5 — Round-trip idempotency:** re-`push` with no local edits = **zero** remote writes; re-`pull` with unchanged remote = **zero** local rewrites.
19
+ - **FR6 — Migration:** existing single `{file}.comments.md` sidecars convert to the `comments/` layout losslessly.
20
+
21
+ ### Non-functional invariants ("bidirectional, no clobber")
22
+ - **NC1 — Append-only to remote:** docspan only ever *creates* comments/replies and *resolves/reopens*. It never updates or deletes remote comments/replies (anyone's, including the user's). Kills the whole "sync rewrote/lost a comment" class.
23
+ - **NC2 — Un-pushed local additions are sacred:** `pull` never discards/overwrites a locally-authored, not-yet-pushed reply/comment.
24
+ - **NC3 — Remote additions never clobber local:** `pull` merges by stable identity, never wholesale file regeneration.
25
+ - **NC4 — Stable thread/reply identity:** every thread/reply addressable by Drive id; not-yet-pushed items carry an explicit "no id yet" marker.
26
+ - **NC5 — Offline-testable:** all request-building/parsing/reconciliation unit-testable with fixtures, no network.
27
+ - **NC6 — Crash-safety:** atomic writes (temp-then-rename, as `SyncState.save` does); a mid-push crash must not silently double-post (best-effort — R1).
28
+
29
+ ### Non-goals (v1)
30
+ - Anchored/text-range placement of new comments (Drive ignores `anchor` on Workspace editor files — §2).
31
+ - Editing/deleting anyone's existing remote comment/reply.
32
+ - Rich formatting (comment `content` is plain text on write).
33
+ - Confluence inline-comment write path (separate later effort).
34
+
35
+ ---
36
+
37
+ ## 2. Research findings — Drive comments/replies API
38
+
39
+ Comments live on **Drive API v3**, not Docs v1. `GoogleDocsClient` already holds `drive_service` — no new wiring.
40
+
41
+ | Capability | Method | Notes |
42
+ |---|---|---|
43
+ | List threads + replies | `comments.list(fileId, fields=…, pageToken)` | Paginated; must request nested `replies` + output-only fields |
44
+ | Get one thread | `comments.get(fileId, commentId, fields=…)` | Cheap resolved/modifiedTime check |
45
+ | Create top-level comment | `comments.create(fileId, body={content, quotedFileContent?, anchor?})` | `content` = plain text (write); `htmlContent` output-only |
46
+ | Create reply | `replies.create(fileId, commentId, body={content})` | Same content semantics |
47
+ | Resolve / reopen | `replies.create(fileId, commentId, body={action:"resolve"|"reopen", content?})` | Resolving = a reply with an action |
48
+ | List replies | `replies.list(fileId, commentId)` | Usually unneeded — `comments.list` embeds replies |
49
+
50
+ **Design-shaping limits:**
51
+ - **`anchor` is ignored on Google Docs** — a new anchored comment renders as "Original content deleted" / no highlight. → new comments must be document-level/unanchored (ADR-004). *(High confidence — documented + reproduced.)*
52
+ - **`content` plain text on write; `htmlContent` read-only** — no rich round-trip.
53
+ - **Output-only fields need explicit `fields=`** (`id`, `createdTime`, `htmlContent`, `resolved`, nested `replies`).
54
+ - **Editing/deleting others' items** → 403; avoided entirely per NC1.
55
+ - **Scopes:** writing needs full `drive` scope. `PUSH_SCOPES` already has it → existing push tokens suffice; `drive.readonly` tokens can't write and must re-consent.
56
+ - **Quoted selection is read-only context** — surfaced, never used for positioning writes.
57
+
58
+ Stable identity = Drive comment/reply `id` (stable server strings). A not-yet-pushed local item has no server id → explicit empty-id + `pushed=false` marker (NC4), filled after `create`.
59
+
60
+ Sources: [Manage comments and replies](https://developers.google.com/workspace/drive/api/guides/manage-comments) · [REST: comments](https://developers.google.com/workspace/drive/api/reference/rest/v3/comments) · [anchor "Original content deleted" issue](https://github.com/googleworkspace/cli/issues/169)
61
+
62
+ ---
63
+
64
+ ## 3. Design
65
+
66
+ ### 3.1 The `comments/` directory layout
67
+
68
+ Replace the single regenerated `{local}.comments.md` blob with a directory beside the local file:
69
+
70
+ ```
71
+ notes/spec.md
72
+ notes/spec.comments/ ← COMMENTS_DIR_SUFFIX = ".comments"
73
+ t-8f3a2c.md ← one file per thread; filename = stable LOCAL id
74
+ local-4c9d.md ← a brand-new thread not yet pushed (no server id)
75
+ ```
76
+
77
+ **Naming (ADR-002): filename = stable local id; server id in front matter.** Filename never changes across syncs (avoids git churn + rename races). New local thread = `local-<short-uuid>.md`; after push it keeps the filename but gains `remote_id`. A `remote_id → filename` index lives in state for O(1) reconcile.
78
+
79
+ **Per-thread file — YAML front matter + id-addressed marker blocks:**
80
+
81
+ ```markdown
82
+ ---
83
+ thread: t-8f3a2c # stable local id (== filename stem), never changes
84
+ remote_id: "AAAABBBBcomment" # Drive comment id; null until pushed
85
+ resolved: false
86
+ author: "Alice <alice@ex.com>"
87
+ created: 2026-07-16T10:00:00Z
88
+ quoted: "the p50 latency figure" # read-only context
89
+ anchored: false # informational; Docs ignores anchor
90
+ ---
91
+
92
+ <!-- comment id=AAAABBBBcomment author="Alice" created=... pushed=true -->
93
+ Where does the p50 number come from?
94
+
95
+ <!-- reply id=CCCCreply author="Bob" created=... pushed=true -->
96
+ From the June dashboard.
97
+
98
+ <!-- reply id= author=me created=... pushed=false -->
99
+ I'll switch this to p99 and cite the source.
100
+ ```
101
+
102
+ - **`pushed=false` + empty `id=`** is the sole "net-new local, push me" signal (NC4).
103
+ - Block body = text between one marker and the next. Adding a reply = appending a `pushed=false` block (`docspan comments reply <thread>` scaffolds it).
104
+ - After `create`, push rewrites **only that block's marker** (fills id, `pushed=true`) + the file's `remote_id`/`resolved` — nothing else touched.
105
+
106
+ ### 3.2 Identity & sync-state
107
+
108
+ Comments are structured, id-bearing records → reconciled **by identity**, not the line-based `three_way_merge` used for doc bodies (ADR-003). Keep the three-way *philosophy* (base/ours/theirs) at thread+reply granularity.
109
+
110
+ Extend the existing state store:
111
+ - `comments` section in `MappingState` (or sibling keyed by local path) in `.markgate-state.json`:
112
+ ```jsonc
113
+ "comments": {
114
+ "index": { "AAAABBBBcomment": "t-8f3a2c" }, // remote_id → filename stem
115
+ "base_snapshot_hash": "<sha256>" // pointer into base store
116
+ }
117
+ ```
118
+ - Comments base snapshot in the content-addressed base store (reuse `save_base_content`/`get_base_content`), under `.markgate-base/comments/`:
119
+ ```jsonc
120
+ { "threads": { "AAAABBBBcomment": { "resolved": false, "replies": ["CCCCreply"], "content_hash": "…" } } }
121
+ ```
122
+ This snapshot is the **base**: `base` = last-synced remote, `theirs` = current `comments.list`, `ours` = local files.
123
+
124
+ ### 3.3 Push semantics
125
+
126
+ `orchestrate_push` gains a comments pass (guarded by `comments_mode != off`; push-direction only):
127
+ 1. Parse every thread file.
128
+ 2. Collect actionable items (stable order — comments before replies, oldest first):
129
+ - `pushed=false` block on a thread with null `remote_id` → `comments.create` (unanchored); store id, update index, mark pushed.
130
+ - `pushed=false` reply under a thread with `remote_id` → `replies.create`; fill id, pushed.
131
+ - Front-matter `resolved` flipped vs base + user asked → `replies.create(action=resolve|reopen)`.
132
+ 3. **NC1:** only create + resolve/reopen calls. No update/delete.
133
+ 4. **Persist after each create** → idempotent re-runs (FR5), bounded crash window (R1).
134
+
135
+ ### 3.4 Pull semantics (anti-clobber core)
136
+
137
+ `orchestrate_pull` gains a reconcile pass:
138
+ 1. `comments.list` (paginated, nested replies/resolved/quoted) → **theirs**.
139
+ 2. Load **base** snapshot + local files (**ours**).
140
+ 3. Reconcile per thread by `remote_id`, then per reply by reply id:
141
+
142
+ | Case (by id) | Action | Invariant |
143
+ |---|---|---|
144
+ | Thread in theirs, not base/local | Create local thread file | — |
145
+ | Reply id in theirs, not local | **Append** reply block (`pushed=true`) | NC3 |
146
+ | Local block `pushed=false` | **Preserve verbatim** | NC2 |
147
+ | `resolved` changed in theirs | Update front-matter | — |
148
+ | Remote body edited (id in base+theirs, hash differs) | Replace **only that block's** body | NC1/NC3 |
149
+ | Thread in base, absent from theirs (deleted remotely) | Mark `deleted: true` (never `rm`) | NC2 |
150
+
151
+ 4. Order remote by `createdTime`; local `pushed=false` blocks sort last.
152
+ 5. Write new base snapshot = current remote; update index.
153
+
154
+ A **structured merge keyed by id** — the anti-clobber replacement for the regenerated blob.
155
+
156
+ ### 3.5 Migration from `{file}.comments.md`
157
+ - New config `comments_mode: dir | sidecar | off` (replaces boolean `pull_comments`; `true` → `sidecar` w/ deprecation note).
158
+ - First `dir`-mode run: if the old sidecar exists, regenerate the `comments/` dir from remote (authoritative; the old blob was read-only so nothing lost), rename old → `.bak`.
159
+ - `docspan comments migrate [path]` (dry-runnable). Add `COMMENTS_DIR_SUFFIX = ".comments"` in `core/paths.py`.
160
+
161
+ ---
162
+
163
+ ## 4. ADRs
164
+
165
+ - **ADR-001 — Directory-per-thread over single regenerated sidecar.** The single blob regenerates wholesale on every pull → can't safely hold user edits. Per-thread files give stable handles, surgical appends, localized conflicts.
166
+ - **ADR-002 — Filename = stable local id; server id in front matter.** Rejected filename=server-id (rename on first push → git churn, identity break, half-push races).
167
+ - **ADR-003 — Structured id-keyed reconciliation, not line-based merge.** `merge3` reused only for a rare edited body within one block. Line merge corrupts identity-bearing records / interleaves replies.
168
+ - **ADR-004 — v1 = replies + resolve + unanchored new comments; no anchored creation.** Drive ignores `anchor` on Docs; anchored create renders "Original content deleted." Ship replies + resolve first; new comments (Phase 3) unanchored, flagged, documented.
169
+
170
+ ---
171
+
172
+ ## 5. Phased implementation plan
173
+
174
+ - **Phase 0 — Directory refactor (read-only).** `comments/` layout, marker-block serializer/parser (round-trip tested), `comments_mode` + migration, `COMMENTS_DIR_SUFFIX`. Pull still regenerates from remote (safe). Smallest useful step.
175
+ - **Phase 1 — Bidirectional replies + reconcile-aware pull (SHIP TOGETHER).** `client.create_reply`/resolve, push comments-pass, base snapshot, id-keyed reconcile pull (§3.4), `docspan comments reply`. *Push and reconcile-pull must ship in one release* — shipping push while pull still regenerates would clobber un-pushed local replies (violates NC2).
176
+ - **Phase 2 — New unanchored top-level comments.** `client.create_comment` + push, behind `comments_new: true`, limitation surfaced. `docspan comments new`.
177
+ - **Phase 3 — Robustness & polish.** Remote-deletion UX, resolve edge cases, crash-window dedupe (R1), `docspan comments status`. Later: Confluence parity (separate plan).
178
+
179
+ ---
180
+
181
+ ## 6. Test strategy (offline-first)
182
+
183
+ Mirror `tests/test_orchestrator.py` (in-memory fake, `tmp_path`, no network):
184
+ - Fixtures: captured `comments.list` JSON (open+resolved, nested replies, quoted, multi-page). Assert `fields` mask requests nested replies + output-only fields.
185
+ - Serializer/parser round-trip byte-stable; empty-id `pushed=false` markers survive; hand-appended reply parses.
186
+ - Reconcile as a **pure table-driven function** `(base, remote, local) → (files, action_plan)` covering every §3.4 row — especially **local-unpushed-preserved (NC2)**.
187
+ - Push planner → ordered create/reply/resolve calls against a `FakeDriveComments` double; payload shapes (plain-text content; resolve `action`); **second run = zero calls (FR5)**.
188
+ - Migration: old sidecar → dir generated, `.bak` created, no loss.
189
+ - Scope guard: read-only token → clear "re-auth for write", not raw 403.
190
+
191
+ ---
192
+
193
+ ## 7. Risks & open questions
194
+
195
+ - **R1 — Crash-window double-post.** No idempotency key on Drive comments; a create that succeeds then crashes before local persist re-posts next run. *Mitigation:* persist after each create; Phase 3 dedupe on next pull. **Open:** acceptable for v1?
196
+ - **R2 — `me` attribution** for un-pushed blocks; confirm no display-name reconcile needed (cosmetic).
197
+ - **R3 — Resolve permissions** may 403 for non-authors depending on sharing; best-effort, per-thread report, never fail whole push.
198
+ - **R4 — Unanchored comments UX** (Phase 2): document-level only. **Operator decision:** acceptable, or omit new-comment creation from v1?
199
+ - **R5 — Large threads / pagination cost.** Reuse `_with_backoff`; consider a `modifiedTime`-gated skip. **Open:** cheap "any comments changed?" probe? (none first-class).
200
+ - **R6 — Ordering churn** when local blocks gain ids; ensure minimal diff.
201
+
202
+ ---
203
+
204
+ ## 8. Adversarial self-review (fixes folded in)
205
+
206
+ - **Clobber window between push and pull** → Phase 1 ships push + reconcile-pull together; Phase 0 stays read-only.
207
+ - **Filename-as-server-id churn/races** → ADR-002 stable local id.
208
+ - **Line-merge on comments corrupts/interleaves** → ADR-003 id-keyed reconcile.
209
+ - **Silent loss on remote deletion** → mark `deleted: true`, never `rm`.
210
+ - **Idempotency false-confidence** → persist after each create; residual window = R1.
211
+ - **Scope trap** (readonly token silently fails writes) → explicit scope check + re-auth message.
212
+ - **Broken anchored feature** → ADR-004 unanchored-only, documented.
@@ -0,0 +1,59 @@
1
+ # Plan: Google Docs table push + inline styles on insert
2
+
3
+ ## Problem
4
+
5
+ The Google Docs push path drops two things, both blocking real-world design-doc use:
6
+
7
+ 1. **Inline styles/links are lost on insert.** `MarkdownToParagraphParser` flattens inline
8
+ tokens to plain text (no spans), and `DocsRequestBuilder._make_insert_requests` never applies
9
+ text styles — the `_make_text_style_requests` helper exists but is unused. So bold, italic,
10
+ inline code, and **links** vanish on push.
11
+ 2. **Tables are dropped entirely.** The markdown parser skips `table` tokens and
12
+ `DocsStructureParser` skips table elements (`# table … silently skipped`), so markdown tables
13
+ never reach the doc, and a doc that already has a table can't be diffed against.
14
+
15
+ ## Goals
16
+
17
+ - Preserve inline **links, bold, italic, monospace** when inserting/replacing paragraphs.
18
+ - Render markdown tables as real Google Docs tables on push.
19
+ - Parse existing Google Docs tables back into the node model so the structural diff is
20
+ **idempotent** (pushing an unchanged table produces no requests; it isn't re-inserted).
21
+
22
+ ## Non-goals (v1)
23
+
24
+ - Rich inline styling *inside* table cells — v1 fills cells with plain text (cell links/bold become
25
+ plain text). Prose links/formatting are fully preserved. Rich cells are a fast-follow.
26
+ - Image push, nested tables, cell merges.
27
+
28
+ ## Design
29
+
30
+ ### Inline styles (Part 1)
31
+ - Parser: add `_spans_from_inline(children, …)` that walks mistune inline tokens
32
+ (`text`/`strong`/`emphasis`/`link`/`codespan`) into ordered `TextSpan`s, propagating
33
+ bold/italic/link/monospace through nesting. `node.text = "".join(span.text)` so the existing
34
+ diff key (keyed on text) is unchanged.
35
+ - Builder: in `_make_insert_requests`, after `insertText`, walk `node.spans`, compute UTF-16
36
+ offsets, and emit `updateTextStyle` (via `_make_text_style_requests`) for any styled span.
37
+
38
+ ### Tables (Part 2)
39
+ - New `DocsTableNode(rows: List[List[str]], start_index, end_index)` in the structure module.
40
+ - Parser emits `DocsTableNode` for `table` tokens (header row + body rows, cell = plain text).
41
+ - `DocsStructureParser` parses live `table` elements into `DocsTableNode` (rows from
42
+ `table.tableRows[].tableCells[].content` paragraphs), with real start/end indices.
43
+ - `DocsRequestBuilder.build` now diffs a mixed `List[Union[DocsParagraphNode, DocsTableNode]]`.
44
+ Table diff key = `("__table__", tuple(tuple(row) for row in rows))`.
45
+ - **insert** → emit `insertTable` (rows×cols) at the insert index (Pass 1).
46
+ - **delete** → `deleteContentRange` over the table span.
47
+ - **equal** → no request (idempotent).
48
+ - **Cell fill is two-pass** (robust vs. fragile predicted indices): Pass 1 inserts empty tables;
49
+ `backend.push` re-fetches the doc, and `build_table_fill_requests(doc, queued_tables)` locates
50
+ empty tables in document order and emits reverse-ordered `insertText` per cell. Cell indices come
51
+ from the *real* re-fetched JSON, so no index guessing.
52
+
53
+ ## Verification
54
+
55
+ - Unit tests (no network) for: span extraction, styled-insert requests, markdown→table node,
56
+ live-table→node parsing, table insert/delete/equal diffing, and cell-fill request generation
57
+ against a sample table JSON.
58
+ - **Live smoke test still required** before trusting cell fill end-to-end — needs docspan Google
59
+ credentials (service account or OAuth token). Tracked as the last step.