docspan 0.1.0__tar.gz → 0.3.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 (188) hide show
  1. docspan-0.3.0/.github/workflows/ci.yml +161 -0
  2. {docspan-0.1.0 → docspan-0.3.0}/.github/workflows/publish.yml +7 -3
  3. {docspan-0.1.0 → docspan-0.3.0}/.gitignore +5 -0
  4. docspan-0.3.0/.mypy-error-baseline +1 -0
  5. docspan-0.3.0/.release-please-manifest.json +3 -0
  6. docspan-0.3.0/CHANGELOG.md +143 -0
  7. {docspan-0.1.0 → docspan-0.3.0}/PKG-INFO +63 -9
  8. {docspan-0.1.0 → docspan-0.3.0}/README.md +59 -7
  9. {docspan-0.1.0 → docspan-0.3.0}/docs/backends/google-docs.md +6 -1
  10. {docspan-0.1.0 → docspan-0.3.0}/markgate.yaml.example +5 -0
  11. docspan-0.3.0/project_plans/bidirectional-comments/plan.md +212 -0
  12. docspan-0.3.0/project_plans/gdocs-tables-inline-styles/plan.md +63 -0
  13. docspan-0.3.0/project_plans/wedding-planning-workflow/decisions/ADR-001-checklist-state-as-literal-text.md +63 -0
  14. docspan-0.3.0/project_plans/wedding-planning-workflow/decisions/ADR-002-comment-risk-flagging-not-anchor-preservation.md +46 -0
  15. docspan-0.3.0/project_plans/wedding-planning-workflow/feature-gap-report.md +84 -0
  16. docspan-0.3.0/project_plans/wedding-planning-workflow/implementation/adversarial-review.md +39 -0
  17. docspan-0.3.0/project_plans/wedding-planning-workflow/implementation/architecture-review.md +31 -0
  18. docspan-0.3.0/project_plans/wedding-planning-workflow/implementation/plan.md +646 -0
  19. docspan-0.3.0/project_plans/wedding-planning-workflow/implementation/pre-mortem.md +22 -0
  20. docspan-0.3.0/project_plans/wedding-planning-workflow/implementation/validation.md +78 -0
  21. docspan-0.3.0/project_plans/wedding-planning-workflow/requirements.md +89 -0
  22. docspan-0.3.0/project_plans/wedding-planning-workflow/research/architecture.md +272 -0
  23. docspan-0.3.0/project_plans/wedding-planning-workflow/research/build-vs-buy.md +245 -0
  24. docspan-0.3.0/project_plans/wedding-planning-workflow/research/features.md +247 -0
  25. docspan-0.3.0/project_plans/wedding-planning-workflow/research/pitfalls.md +94 -0
  26. docspan-0.3.0/project_plans/wedding-planning-workflow/research/stack.md +75 -0
  27. docspan-0.3.0/project_plans/wedding-planning-workflow/research/ux.md +106 -0
  28. docspan-0.3.0/project_plans/wedding-planning-workflow/workflow-runbook.md +290 -0
  29. {docspan-0.1.0 → docspan-0.3.0}/pyproject.toml +17 -0
  30. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/base.py +7 -4
  31. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/backend.py +1 -1
  32. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/google_docs/auth.py +95 -0
  33. docspan-0.3.0/src/docspan/backends/google_docs/backend.py +939 -0
  34. docspan-0.3.0/src/docspan/backends/google_docs/checkbox_state.py +100 -0
  35. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/google_docs/client.py +138 -5
  36. docspan-0.3.0/src/docspan/backends/google_docs/comments.py +124 -0
  37. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/google_docs/converter.py +59 -4
  38. docspan-0.3.0/src/docspan/backends/google_docs/docs_request_builder.py +2210 -0
  39. docspan-0.3.0/src/docspan/backends/google_docs/docs_structure_parser.py +638 -0
  40. docspan-0.3.0/src/docspan/backends/google_docs/heading_anchors.py +467 -0
  41. docspan-0.3.0/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +357 -0
  42. docspan-0.3.0/src/docspan/backends/google_docs/nodes_to_markdown.py +367 -0
  43. docspan-0.3.0/src/docspan/backends/google_docs/onboarding.py +94 -0
  44. docspan-0.3.0/src/docspan/backends/google_docs/projection.py +301 -0
  45. docspan-0.3.0/src/docspan/backends/google_docs/push_preview.py +270 -0
  46. docspan-0.3.0/src/docspan/backends/google_docs/tabs.py +130 -0
  47. docspan-0.3.0/src/docspan/cli/main.py +726 -0
  48. docspan-0.3.0/src/docspan/config.py +133 -0
  49. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/core/orchestrator.py +18 -12
  50. docspan-0.3.0/src/docspan/core/xdg.py +59 -0
  51. docspan-0.3.0/terraform/gcp/README.md +23 -0
  52. docspan-0.3.0/terraform/gcp/main.tf +39 -0
  53. docspan-0.3.0/terraform/gcp/outputs.tf +23 -0
  54. docspan-0.3.0/terraform/gcp/variables.tf +17 -0
  55. docspan-0.3.0/tests/conftest.py +55 -0
  56. docspan-0.3.0/tests/fixtures/github_slugger_vectors.json +349 -0
  57. docspan-0.3.0/tests/test_checkbox_state.py +74 -0
  58. {docspan-0.1.0 → docspan-0.3.0}/tests/test_cli.py +383 -6
  59. docspan-0.3.0/tests/test_code_block_granularity.py +765 -0
  60. docspan-0.3.0/tests/test_converter.py +51 -0
  61. docspan-0.3.0/tests/test_docs_request_builder.py +637 -0
  62. docspan-0.3.0/tests/test_docs_structure_parser.py +586 -0
  63. docspan-0.3.0/tests/test_gdocs_push_pipeline.py +1363 -0
  64. docspan-0.3.0/tests/test_gdocs_tables_and_styles.py +384 -0
  65. docspan-0.3.0/tests/test_google_comments.py +253 -0
  66. docspan-0.3.0/tests/test_google_docs_backend.py +1147 -0
  67. docspan-0.3.0/tests/test_google_oauth.py +122 -0
  68. docspan-0.3.0/tests/test_google_onboarding.py +121 -0
  69. docspan-0.3.0/tests/test_heading_anchors.py +1154 -0
  70. docspan-0.3.0/tests/test_heading_identity.py +456 -0
  71. {docspan-0.1.0 → docspan-0.3.0}/tests/test_markdown_to_paragraph_parser.py +89 -0
  72. docspan-0.3.0/tests/test_nodes_to_markdown.py +296 -0
  73. {docspan-0.1.0 → docspan-0.3.0}/tests/test_orchestrator.py +81 -1
  74. docspan-0.3.0/tests/test_push_preview.py +338 -0
  75. docspan-0.3.0/tests/test_span_trailing_newline.py +111 -0
  76. docspan-0.3.0/tests/test_table_cell_spans.py +542 -0
  77. docspan-0.3.0/tests/test_tabs.py +120 -0
  78. docspan-0.3.0/tests/test_xdg_central_config.py +116 -0
  79. docspan-0.1.0/.github/workflows/ci.yml +0 -34
  80. docspan-0.1.0/.release-please-manifest.json +0 -3
  81. docspan-0.1.0/CHANGELOG.md +0 -35
  82. docspan-0.1.0/src/docspan/backends/google_docs/backend.py +0 -140
  83. docspan-0.1.0/src/docspan/backends/google_docs/docs_request_builder.py +0 -232
  84. docspan-0.1.0/src/docspan/backends/google_docs/docs_structure_parser.py +0 -120
  85. docspan-0.1.0/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +0 -145
  86. docspan-0.1.0/src/docspan/cli/main.py +0 -408
  87. docspan-0.1.0/src/docspan/config.py +0 -62
  88. docspan-0.1.0/tests/test_docs_request_builder.py +0 -130
  89. docspan-0.1.0/tests/test_docs_structure_parser.py +0 -187
  90. {docspan-0.1.0 → docspan-0.3.0}/.github/workflows/release-please.yml +0 -0
  91. {docspan-0.1.0 → docspan-0.3.0}/CONTRIBUTING.md +0 -0
  92. {docspan-0.1.0 → docspan-0.3.0}/Procfile +0 -0
  93. {docspan-0.1.0 → docspan-0.3.0}/RAILWAY_SETUP.md +0 -0
  94. {docspan-0.1.0 → docspan-0.3.0}/docs/backends/confluence.md +0 -0
  95. {docspan-0.1.0 → docspan-0.3.0}/docs/commands.md +0 -0
  96. {docspan-0.1.0 → docspan-0.3.0}/docs/configuration.md +0 -0
  97. {docspan-0.1.0 → docspan-0.3.0}/docs/contributing.md +0 -0
  98. {docspan-0.1.0 → docspan-0.3.0}/docs/index.md +0 -0
  99. {docspan-0.1.0 → docspan-0.3.0}/docs/install.md +0 -0
  100. {docspan-0.1.0 → docspan-0.3.0}/docspan.yaml.example +0 -0
  101. {docspan-0.1.0 → docspan-0.3.0}/mkdocs.yml +0 -0
  102. {docspan-0.1.0 → docspan-0.3.0}/modules/__init__.py +0 -0
  103. {docspan-0.1.0 → docspan-0.3.0}/modules/auth.py +0 -0
  104. {docspan-0.1.0 → docspan-0.3.0}/modules/conflict_handler.py +0 -0
  105. {docspan-0.1.0 → docspan-0.3.0}/modules/converter.py +0 -0
  106. {docspan-0.1.0 → docspan-0.3.0}/modules/gdrive_client.py +0 -0
  107. {docspan-0.1.0 → docspan-0.3.0}/modules/sync_engine.py +0 -0
  108. {docspan-0.1.0 → docspan-0.3.0}/project_plans/docspan-release/implementation/adversarial-review.md +0 -0
  109. {docspan-0.1.0 → docspan-0.3.0}/project_plans/docspan-release/implementation/plan.md +0 -0
  110. {docspan-0.1.0 → docspan-0.3.0}/project_plans/docspan-release/implementation/release-checklist.md +0 -0
  111. {docspan-0.1.0 → docspan-0.3.0}/project_plans/docspan-release/implementation/validation.md +0 -0
  112. {docspan-0.1.0 → docspan-0.3.0}/project_plans/docspan-release/requirements.md +0 -0
  113. {docspan-0.1.0 → docspan-0.3.0}/project_plans/docspan-release/research/architecture.md +0 -0
  114. {docspan-0.1.0 → docspan-0.3.0}/project_plans/docspan-release/research/features.md +0 -0
  115. {docspan-0.1.0 → docspan-0.3.0}/project_plans/docspan-release/research/google-docs-push.md +0 -0
  116. {docspan-0.1.0 → docspan-0.3.0}/project_plans/docspan-release/research/pitfalls.md +0 -0
  117. {docspan-0.1.0 → docspan-0.3.0}/project_plans/docspan-release/research/stack.md +0 -0
  118. {docspan-0.1.0 → docspan-0.3.0}/project_plans/markgate-sync/decisions/ADR-001-merge3-dependency.md +0 -0
  119. {docspan-0.1.0 → docspan-0.3.0}/project_plans/markgate-sync/decisions/ADR-002-base-content-sidecar-store.md +0 -0
  120. {docspan-0.1.0 → docspan-0.3.0}/project_plans/markgate-sync/implementation/adversarial-review.md +0 -0
  121. {docspan-0.1.0 → docspan-0.3.0}/project_plans/markgate-sync/implementation/plan.md +0 -0
  122. {docspan-0.1.0 → docspan-0.3.0}/project_plans/markgate-sync/implementation/validation.md +0 -0
  123. {docspan-0.1.0 → docspan-0.3.0}/project_plans/markgate-sync/requirements.md +0 -0
  124. {docspan-0.1.0 → docspan-0.3.0}/project_plans/markgate-sync/research/architecture.md +0 -0
  125. {docspan-0.1.0 → docspan-0.3.0}/project_plans/markgate-sync/research/features.md +0 -0
  126. {docspan-0.1.0 → docspan-0.3.0}/project_plans/markgate-sync/research/pitfalls.md +0 -0
  127. {docspan-0.1.0 → docspan-0.3.0}/project_plans/markgate-sync/research/stack.md +0 -0
  128. {docspan-0.1.0 → docspan-0.3.0}/release-please-config.json +0 -0
  129. {docspan-0.1.0 → docspan-0.3.0}/requirements.txt +0 -0
  130. {docspan-0.1.0 → docspan-0.3.0}/runtime.txt +0 -0
  131. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/__init__.py +0 -0
  132. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/__main__.py +0 -0
  133. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/__init__.py +0 -0
  134. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/__init__.py +0 -0
  135. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/__init__.py +0 -0
  136. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/comparator.py +0 -0
  137. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/converter.py +0 -0
  138. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/converters.py +0 -0
  139. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/interfaces.py +0 -0
  140. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/nodes.py +0 -0
  141. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/parser.py +0 -0
  142. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/validators.py +0 -0
  143. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/visitors.py +0 -0
  144. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/client.py +0 -0
  145. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/config/__init__.py +0 -0
  146. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/config/loader.py +0 -0
  147. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/config/models.py +0 -0
  148. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/config/validation.py +0 -0
  149. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/__init__.py +0 -0
  150. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/ast.py +0 -0
  151. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/extensions/__init__.py +0 -0
  152. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/extensions/frontmatter.py +0 -0
  153. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/extensions/mermaid.py +0 -0
  154. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/extensions/wikilinks.py +0 -0
  155. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/inline_parser.py +0 -0
  156. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/parser.py +0 -0
  157. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/__init__.py +0 -0
  158. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/markdown_file.py +0 -0
  159. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/page.py +0 -0
  160. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/path_utils.py +0 -0
  161. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/results.py +0 -0
  162. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/sync_status.py +0 -0
  163. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/__init__.py +0 -0
  164. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/__init__.py +0 -0
  165. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/attachment_client.py +0 -0
  166. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/base_client.py +0 -0
  167. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/client.py +0 -0
  168. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/comment_client.py +0 -0
  169. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/crawler.py +0 -0
  170. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/label_client.py +0 -0
  171. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/page_client.py +0 -0
  172. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/space_client.py +0 -0
  173. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/url_parser.py +0 -0
  174. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/backends/google_docs/__init__.py +0 -0
  175. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/cli/__init__.py +0 -0
  176. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/core/__init__.py +0 -0
  177. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/core/merge.py +0 -0
  178. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/core/paths.py +0 -0
  179. {docspan-0.1.0 → docspan-0.3.0}/src/docspan/core/state.py +0 -0
  180. {docspan-0.1.0 → docspan-0.3.0}/sync.py +0 -0
  181. {docspan-0.1.0 → docspan-0.3.0}/terraform/main.tf +0 -0
  182. {docspan-0.1.0 → docspan-0.3.0}/terraform/variables.tf +0 -0
  183. {docspan-0.1.0 → docspan-0.3.0}/tests/__init__.py +0 -0
  184. {docspan-0.1.0 → docspan-0.3.0}/tests/test_config.py +0 -0
  185. {docspan-0.1.0 → docspan-0.3.0}/tests/test_conflict_resolution.py +0 -0
  186. {docspan-0.1.0 → docspan-0.3.0}/tests/test_merge.py +0 -0
  187. {docspan-0.1.0 → docspan-0.3.0}/tests/test_state.py +0 -0
  188. {docspan-0.1.0 → docspan-0.3.0}/uv.lock +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"
@@ -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/
@@ -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.3.0"
3
+ }
@@ -0,0 +1,143 @@
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.3.0](https://github.com/tstapler/docspan/compare/docspan-v0.2.0...docspan-v0.3.0) (2026-08-11)
9
+
10
+
11
+ ### Features
12
+
13
+ * **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)
14
+ * **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))
15
+ * **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))
16
+ * **google-docs:** restyle a paragraph in place instead of retyping it ([edf0b13](https://github.com/tstapler/docspan/commit/edf0b13520aa1a60b1fb02ee51dc1bd4a967c412))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **cli:** accept --config and --prefix before the subcommand as well as after ([00f6524](https://github.com/tstapler/docspan/commit/00f65241e41c5bb2deec5642eb36e8ea64a58744))
22
+ * **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))
23
+ * **google-docs:** align pass 2 by content so trimmed deletes can't mis-style ([ba56e08](https://github.com/tstapler/docspan/commit/ba56e08b7cc482c6bd569e9e5560e1c71a768ebf))
24
+ * **google-docs:** append past the last node without merging it into the last paragraph ([1aef861](https://github.com/tstapler/docspan/commit/1aef861981ee95463e7c2c9caa67bf32d90c9d2a))
25
+ * **google-docs:** apply inline styling when it is the only change ([c205552](https://github.com/tstapler/docspan/commit/c205552aed5c0f483ba5213bd39f2aff16587efe))
26
+ * **google-docs:** clear the inherited bullet on inserted non-list paragraphs ([dda8a50](https://github.com/tstapler/docspan/commit/dda8a505e8f822c077478656158b87cf6d74132b))
27
+ * **google-docs:** insert before a Table/ToC/SectionBreak into the body, not into it ([701e2d0](https://github.com/tstapler/docspan/commit/701e2d01433e9efc2c42a159ffb4f92d90972c56))
28
+ * **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))
29
+ * **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))
30
+ * **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))
31
+ * **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))
32
+ * **google-docs:** order pass-1 requests by their anchor, not their own index ([58e2b6a](https://github.com/tstapler/docspan/commit/58e2b6af2271dc200c225c39c44d1a567ae5722a))
33
+ * **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))
34
+ * **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))
35
+ * **google-docs:** project empty paragraphs out of the diff instead of deleting them ([d8b1b5f](https://github.com/tstapler/docspan/commit/d8b1b5feb56439da34ae7be64c092b80927c30a7))
36
+ * **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))
37
+ * **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))
38
+ * **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))
39
+ * **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))
40
+ * **google-docs:** render TITLE/SUBTITLE as headings instead of silently demoting them ([cc6cd0b](https://github.com/tstapler/docspan/commit/cc6cd0b6e8fdb8337cfa871f77c4466445041922))
41
+ * **google-docs:** report a dropped over-long span, and share the delete-trim arithmetic ([daa77a6](https://github.com/tstapler/docspan/commit/daa77a6398f9ed967deef1376b39d73e66050aea))
42
+ * **google-docs:** stop deleting the newline that anchors a table/ToC/section break ([9eba496](https://github.com/tstapler/docspan/commit/9eba496ee1d4db3fb788e910c359d9487f217baa))
43
+ * **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))
44
+
45
+ ## [0.2.0](https://github.com/tstapler/docspan/compare/docspan-v0.1.0...docspan-v0.2.0) (2026-07-22)
46
+
47
+
48
+ ### Features
49
+
50
+ * Add Railway Volume support for persistent state storage ([3a4b76e](https://github.com/tstapler/docspan/commit/3a4b76eb0cbc14afa02f3aa3de2c4607808fad9f))
51
+ * Add retry mechanism and improved error handling for Google Drive API ([e8a7b5f](https://github.com/tstapler/docspan/commit/e8a7b5f177ad2c3b8a356852eceb827326f8ce76))
52
+ * Auto-reload Google Sheet mappings on each sync cycle ([a2647c6](https://github.com/tstapler/docspan/commit/a2647c6c1435eb5624a389a4e673a57ead012123))
53
+ * **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))
54
+ * **confluence:** port adf/markdown/services from markdown-confluence ([e9d1a85](https://github.com/tstapler/docspan/commit/e9d1a85a9747ac75a6d95d6351d18483297726a4))
55
+ * **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))
56
+ * **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))
57
+ * **google-docs:** add per-user OAuth auth option ([#4](https://github.com/tstapler/docspan/issues/4)) ([830369c](https://github.com/tstapler/docspan/commit/830369cba1817224ae0d02f0b14b6a84de84a4eb))
58
+ * **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))
59
+ * **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))
60
+ * scaffold markgate package from google-docs-obsidian-sync fork ([44dd3c5](https://github.com/tstapler/docspan/commit/44dd3c586a670b4689154b2db9bc6cb8673d9702))
61
+ * **sync:** Google Docs structural-diff push, Confluence comments, three-way merge ([9a20e34](https://github.com/tstapler/docspan/commit/9a20e3452f2d92a240128d7c8e2f9c4b63a547f9))
62
+
63
+
64
+ ### Bug Fixes
65
+
66
+ * **ci:** add __future__ annotations for Python 3.9 compat in test ([9ceca65](https://github.com/tstapler/docspan/commit/9ceca65ca5ad2040c1c2ec215fc097b02ba1a0c4))
67
+ * **ci:** apply ruff autofix across all src and test files ([bee2784](https://github.com/tstapler/docspan/commit/bee2784b940d872045ce33faf0ce53f65150d80d))
68
+ * **ci:** resolve ruff lint failures and enable Actions PR creation ([8727e7b](https://github.com/tstapler/docspan/commit/8727e7bcabf1ddec4b4116b06e605ff24d0eaffe))
69
+ * **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))
70
+ * **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))
71
+ * **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))
72
+ * **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))
73
+ * Improve nested list indentation in Google Docs to Markdown conversion ([d6a7539](https://github.com/tstapler/docspan/commit/d6a7539d4beade3426e5d0db838f1d9974f7294b))
74
+ * Remove CONFIG_YAML dependency, prefer individual env vars ([d5d5d4a](https://github.com/tstapler/docspan/commit/d5d5d4ae25a7c1d7a213071f968eba56a9289da3))
75
+ * Resolve service account storage quota error by storing sync state locally ([00e9cb6](https://github.com/tstapler/docspan/commit/00e9cb65033dfb6cca8e0aae2258cde458cfb342))
76
+
77
+ ## [Unreleased]
78
+
79
+ ### Added
80
+ - **google-docs:** internal markdown anchors (`[A1](#a1-current-state)`) now resolve to
81
+ Google Docs heading links instead of being written as a `#fragment` URL the Doc cannot
82
+ follow. Slugs follow `github-slugger`, checked against vectors generated from the real
83
+ implementation. `TITLE`/`SUBTITLE` paragraphs count as anchor targets, and both the
84
+ modern `Link.heading` and the legacy `Link.headingId` union members are read, so an
85
+ anchor survives a pull whether or not the fetch used `includeTabsContent`.
86
+ - **google-docs:** an anchor that names no heading is written as plain text with no link
87
+ and reported — `docspan push --dry-run` lists it, `docspan push` exits non-zero with a
88
+ warning naming the anchor and the heading anchors that *are* available. It is never
89
+ written as a link a reader can click and land nowhere, and never reported as a clean ✓.
90
+ - **google-docs:** both pull paths now emit the heading's slug. A default (no `tab_id`) pull
91
+ goes through Drive's HTML export, which carries the Doc's opaque `#h.abc123` through
92
+ verbatim; it is upgraded to the slug, so the pulled markdown works as markdown.
93
+
94
+ ### Changed
95
+ - **google-docs:** pass 2 parses and aligns the document once per push instead of three
96
+ times. The discarded work sat inside the window between pass 2's read and its write, where
97
+ a concurrent edit costs a conflict on a document pass 1 has already changed.
98
+
99
+ ### Known limitations
100
+ Each of these is tracked as a follow-up rather than half-addressed here.
101
+ - An anchor into a heading in a *different tab* of the same document cannot be resolved and
102
+ is reported unresolved. The flat `headingId` member resolves against the tab named in the
103
+ request, so expressing one needs the tabs-aware `Link.heading` member.
104
+ - A pull cannot express a `bookmark`/`bookmarkId` link, a link to a tab, or any link inside
105
+ a table cell, so those are dropped from the pulled file without a report.
106
+ - Confluence writes an internal anchor as a literal `#fragment` href, which it does not
107
+ resolve.
108
+ - An anchor that resolves to nothing is written as plain text, so a later pull replaces the
109
+ author's `[text](#anchor)` with `text`. The push reports it; nothing does afterwards.
110
+ - Such a push exits non-zero on every run, with no flag to suppress it.
111
+ - A heading containing an HTML entity reference (`## Team &amp; process`) or inline HTML
112
+ (`## <code>push()</code> …`) is slugged from the markdown *source* rather than the rendered
113
+ text, so its slug differs from GitHub's. Because duplicate numbering depends on the
114
+ headings before it, that can land an anchor on a neighbouring heading. Pre-existing; a fix
115
+ attempt was reverted on this branch because it needs the slug text and the
116
+ document text separated, which is its own change.
117
+
118
+ ## [0.1.0] - 2026-06-07
119
+
120
+ ### Added
121
+ - `docspan push` — push local markdown files to Google Docs or Confluence
122
+ - `docspan pull` — pull remote documents into local markdown files with three-way merge
123
+ - `docspan status` — show current mapping status in a table
124
+ - `docspan auth setup` — interactive authentication setup for `google_docs` and `confluence` backends
125
+ - `docspan conflicts list` — list files with unresolved merge conflicts
126
+ - `docspan conflicts resolve` — resolve merge conflicts with `remote`, `local`, or `merged` strategy
127
+ - Google Docs backend: push and pull via Google Docs API (service account auth)
128
+ - Confluence backend: push and pull via Atlassian REST API (API token auth)
129
+ - Three-way merge for bidirectional sync conflict detection
130
+ - Confluence comment sidecar: pull writes inline and footer comments to `{file}.comments.md`
131
+ - `markgate.yaml` config file format with per-mapping direction control (`push`/`pull`/`both`)
132
+ - Sync state tracking via `.markgate-state.json` and content-addressed base store in `.markgate-base/`
133
+
134
+ ### Known Limitations
135
+ - Google Docs: comments on edited paragraphs are destroyed on push (paragraph-level diff; comments on unchanged paragraphs are preserved)
136
+ - Push: no image support — local image files cannot be pushed to Google Docs or Confluence
137
+ - Push: no table support — markdown tables are not rendered in Google Docs
138
+ - Confluence: requires an Atlassian API token; no OAuth flow
139
+ - Confluence: comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
140
+ - 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.
141
+
142
+ [Unreleased]: https://github.com/tstapler/docspan/compare/v0.1.0...HEAD
143
+ [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.3.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
@@ -142,6 +144,38 @@ mappings:
142
144
 
143
145
  ---
144
146
 
147
+ ## Central config & XDG storage
148
+
149
+ 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.
150
+
151
+ ```
152
+ $XDG_CONFIG_HOME/docspan/config.yaml # central config (project registry)
153
+ $XDG_CONFIG_HOME/docspan/<prefix>/… # cached OAuth token
154
+ $XDG_STATE_HOME/docspan/<prefix>/… # sync state + base store, per project
155
+ ```
156
+
157
+ Central config (`~/.config/docspan/config.yaml`):
158
+
159
+ ```yaml
160
+ default_prefix: design-docs
161
+ projects:
162
+ design-docs:
163
+ markgate: ~/Documents/design-docs/markgate.yaml
164
+ ```
165
+
166
+ Register and use projects:
167
+
168
+ ```bash
169
+ docspan config add design-docs ~/Documents/design-docs/markgate.yaml # register (prefix → markgate.yaml)
170
+ docspan config show # list projects + active resolution
171
+ docspan push --prefix design-docs # or DOCSPAN_PREFIX, or default_prefix, or cwd match
172
+ docspan migrate-xdg --prefix design-docs # move legacy in-repo state to XDG + register
173
+ ```
174
+
175
+ **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).
176
+
177
+ ---
178
+
145
179
  ## Command Reference
146
180
 
147
181
  ### `docspan push`
@@ -158,7 +192,7 @@ Push local markdown files to remote docs. Skips mappings with `direction = "pull
158
192
  docspan pull [FILES]... [--dry-run] [--config PATH]
159
193
  ```
160
194
 
161
- Pull remote documents into local markdown files with three-way merge. Writes conflict markers to the file if automatic merge fails.
195
+ 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
196
 
163
197
  ### `docspan status`
164
198
 
@@ -176,7 +210,17 @@ docspan auth setup BACKEND [--config PATH]
176
210
 
177
211
  Interactive authentication setup. `BACKEND` is one of `google_docs` or `confluence`.
178
212
 
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`.
213
+ For **Google Docs**, run it with no flags for a guided flow:
214
+
215
+ ```
216
+ docspan auth setup google_docs
217
+ ```
218
+
219
+ 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.
220
+
221
+ 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.
222
+
223
+ For **Confluence**, prompts for base URL, username, and API token, then prints a YAML snippet to add to `markgate.yaml`.
180
224
 
181
225
  ### `docspan conflicts list`
182
226
 
@@ -208,10 +252,14 @@ Resolve a merge conflict in a tracked file.
208
252
 
209
253
  | Field | Type | Default | Description |
210
254
  |---|---|---|---|
211
- | `credentials_path` | string | null | Path to Google service account JSON key |
212
- | `token_path` | string | `.markgate/google_token.json` | OAuth token storage path |
255
+ | `credentials_path` | string | null | Path to a Google **service account** JSON key |
256
+ | `oauth_client_secret_path` | string | null | Path to an **OAuth client secret** JSON (Desktop app) for per-user auth |
257
+ | `token_path` | string | `$XDG_CONFIG_HOME/docspan/google_token.json` | Where the cached OAuth user token is stored/refreshed (out of the repo) |
258
+ | `pull_comments` | bool | `true` | On pull, write a `{file}.comments.md` sidecar of the doc's comments |
213
259
 
214
- **Environment variable alternatives:**
260
+ Auth resolution order: `credentials_path` → `ACCOUNT_A_CREDENTIALS[_PATH]` env → per-user OAuth (`oauth_client_secret_path`, or an already-cached `token_path`).
261
+
262
+ **Environment variable alternatives (service account):**
215
263
  - `ACCOUNT_A_CREDENTIALS_PATH` — path to service account JSON
216
264
  - `ACCOUNT_A_CREDENTIALS` — inline service account JSON string
217
265
 
@@ -236,6 +284,7 @@ Resolve a merge conflict in a tracked file.
236
284
  | `backend` | string | — | yes | `"google_docs"` or `"confluence"` |
237
285
  | `remote_id` | string | — | yes | Google Doc ID or Confluence page ID |
238
286
  | `direction` | enum | `"both"` | no | `"push"`, `"pull"`, or `"both"` |
287
+ | `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. |
239
288
 
240
289
  ---
241
290
 
@@ -248,7 +297,7 @@ docspan generates these files in your project directory after first sync:
248
297
  | `.markgate-state.json` | Sync state tracking (content hashes, remote versions) |
249
298
  | `.markgate-base/` | Content-addressed store of merge bases |
250
299
  | `{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 |
300
+ | `{file}.comments.md` | Comment sidecar (Google Docs + Confluence); written during pull if comments exist |
252
301
 
253
302
  ---
254
303
 
@@ -257,11 +306,16 @@ docspan generates these files in your project directory after first sync:
257
306
  > [!NOTE]
258
307
  > **Known limitations in v0.1.0**
259
308
  >
260
- > - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved)
309
+ > - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved). `docspan push --dry-run` and a default fail-closed `--force`-gated block now warn before this happens — it is still not prevented.
261
310
  > - Push: no image support — local images cannot be pushed to Google Docs or Confluence
262
311
  > - Push: no table support — markdown tables are not rendered in Google Docs
263
312
  > - Confluence: requires an Atlassian API token; no OAuth flow
264
313
  > - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
314
+ > - 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)
315
+ > - `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
316
+ > - 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
317
+ > - 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)
318
+ > - 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
319
 
266
320
  ---
267
321
 
@@ -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
 
@@ -187,6 +233,7 @@ Resolve a merge conflict in a tracked file.
187
233
  | `backend` | string | — | yes | `"google_docs"` or `"confluence"` |
188
234
  | `remote_id` | string | — | yes | Google Doc ID or Confluence page ID |
189
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. |
190
237
 
191
238
  ---
192
239
 
@@ -199,7 +246,7 @@ docspan generates these files in your project directory after first sync:
199
246
  | `.markgate-state.json` | Sync state tracking (content hashes, remote versions) |
200
247
  | `.markgate-base/` | Content-addressed store of merge bases |
201
248
  | `{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 |
249
+ | `{file}.comments.md` | Comment sidecar (Google Docs + Confluence); written during pull if comments exist |
203
250
 
204
251
  ---
205
252
 
@@ -208,11 +255,16 @@ docspan generates these files in your project directory after first sync:
208
255
  > [!NOTE]
209
256
  > **Known limitations in v0.1.0**
210
257
  >
211
- > - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved)
258
+ > - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved). `docspan push --dry-run` and a default fail-closed `--force`-gated block now warn before this happens — it is still not prevented.
212
259
  > - Push: no image support — local images cannot be pushed to Google Docs or Confluence
213
260
  > - Push: no table support — markdown tables are not rendered in Google Docs
214
261
  > - Confluence: requires an Atlassian API token; no OAuth flow
215
262
  > - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
263
+ > - 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)
264
+ > - `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
265
+ > - 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
266
+ > - 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)
267
+ > - 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
268
 
217
269
  ---
218
270
 
@@ -58,5 +58,10 @@ mappings:
58
58
  !!! warning
59
59
  - **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.
60
60
  - **No image push support**: Local image files cannot be pushed. Images require publicly accessible URLs and additional Drive upload scope.
61
- - **No table push support**: Markdown tables are not converted when pushing to Google Docs.
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.
62
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.