docspan 0.2.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 (184) hide show
  1. docspan-0.3.0/.github/workflows/ci.yml +161 -0
  2. {docspan-0.2.0 → docspan-0.3.0}/.gitignore +5 -0
  3. docspan-0.3.0/.mypy-error-baseline +1 -0
  4. docspan-0.3.0/.release-please-manifest.json +3 -0
  5. docspan-0.3.0/CHANGELOG.md +143 -0
  6. {docspan-0.2.0 → docspan-0.3.0}/PKG-INFO +5 -1
  7. {docspan-0.2.0 → docspan-0.3.0}/README.md +2 -0
  8. {docspan-0.2.0 → docspan-0.3.0}/docs/backends/google-docs.md +6 -1
  9. {docspan-0.2.0 → docspan-0.3.0}/project_plans/gdocs-tables-inline-styles/plan.md +4 -0
  10. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/decisions/ADR-001-checklist-state-as-literal-text.md +7 -7
  11. {docspan-0.2.0 → docspan-0.3.0}/pyproject.toml +17 -0
  12. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/base.py +1 -1
  13. docspan-0.3.0/src/docspan/backends/google_docs/backend.py +939 -0
  14. docspan-0.3.0/src/docspan/backends/google_docs/checkbox_state.py +100 -0
  15. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/client.py +36 -3
  16. docspan-0.3.0/src/docspan/backends/google_docs/docs_request_builder.py +2210 -0
  17. docspan-0.3.0/src/docspan/backends/google_docs/docs_structure_parser.py +638 -0
  18. docspan-0.3.0/src/docspan/backends/google_docs/heading_anchors.py +467 -0
  19. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +106 -12
  20. docspan-0.3.0/src/docspan/backends/google_docs/nodes_to_markdown.py +367 -0
  21. docspan-0.3.0/src/docspan/backends/google_docs/projection.py +301 -0
  22. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/push_preview.py +70 -1
  23. docspan-0.3.0/src/docspan/backends/google_docs/tabs.py +130 -0
  24. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/cli/main.py +66 -4
  25. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/config.py +4 -0
  26. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/core/orchestrator.py +9 -8
  27. docspan-0.3.0/tests/fixtures/github_slugger_vectors.json +349 -0
  28. docspan-0.3.0/tests/test_checkbox_state.py +74 -0
  29. {docspan-0.2.0 → docspan-0.3.0}/tests/test_cli.py +164 -2
  30. docspan-0.3.0/tests/test_code_block_granularity.py +765 -0
  31. {docspan-0.2.0 → docspan-0.3.0}/tests/test_docs_request_builder.py +266 -7
  32. {docspan-0.2.0 → docspan-0.3.0}/tests/test_docs_structure_parser.py +263 -0
  33. docspan-0.3.0/tests/test_gdocs_push_pipeline.py +1363 -0
  34. {docspan-0.2.0 → docspan-0.3.0}/tests/test_gdocs_tables_and_styles.py +157 -10
  35. docspan-0.3.0/tests/test_google_docs_backend.py +1147 -0
  36. docspan-0.3.0/tests/test_heading_anchors.py +1154 -0
  37. docspan-0.3.0/tests/test_heading_identity.py +456 -0
  38. {docspan-0.2.0 → docspan-0.3.0}/tests/test_markdown_to_paragraph_parser.py +33 -0
  39. docspan-0.3.0/tests/test_nodes_to_markdown.py +296 -0
  40. {docspan-0.2.0 → docspan-0.3.0}/tests/test_orchestrator.py +81 -1
  41. docspan-0.3.0/tests/test_span_trailing_newline.py +111 -0
  42. docspan-0.3.0/tests/test_table_cell_spans.py +542 -0
  43. docspan-0.3.0/tests/test_tabs.py +120 -0
  44. docspan-0.2.0/.github/workflows/ci.yml +0 -34
  45. docspan-0.2.0/.release-please-manifest.json +0 -3
  46. docspan-0.2.0/CHANGELOG.md +0 -67
  47. docspan-0.2.0/src/docspan/backends/google_docs/backend.py +0 -473
  48. docspan-0.2.0/src/docspan/backends/google_docs/docs_request_builder.py +0 -529
  49. docspan-0.2.0/src/docspan/backends/google_docs/docs_structure_parser.py +0 -212
  50. docspan-0.2.0/tests/test_google_docs_backend.py +0 -340
  51. {docspan-0.2.0 → docspan-0.3.0}/.github/workflows/publish.yml +0 -0
  52. {docspan-0.2.0 → docspan-0.3.0}/.github/workflows/release-please.yml +0 -0
  53. {docspan-0.2.0 → docspan-0.3.0}/CONTRIBUTING.md +0 -0
  54. {docspan-0.2.0 → docspan-0.3.0}/Procfile +0 -0
  55. {docspan-0.2.0 → docspan-0.3.0}/RAILWAY_SETUP.md +0 -0
  56. {docspan-0.2.0 → docspan-0.3.0}/docs/backends/confluence.md +0 -0
  57. {docspan-0.2.0 → docspan-0.3.0}/docs/commands.md +0 -0
  58. {docspan-0.2.0 → docspan-0.3.0}/docs/configuration.md +0 -0
  59. {docspan-0.2.0 → docspan-0.3.0}/docs/contributing.md +0 -0
  60. {docspan-0.2.0 → docspan-0.3.0}/docs/index.md +0 -0
  61. {docspan-0.2.0 → docspan-0.3.0}/docs/install.md +0 -0
  62. {docspan-0.2.0 → docspan-0.3.0}/docspan.yaml.example +0 -0
  63. {docspan-0.2.0 → docspan-0.3.0}/markgate.yaml.example +0 -0
  64. {docspan-0.2.0 → docspan-0.3.0}/mkdocs.yml +0 -0
  65. {docspan-0.2.0 → docspan-0.3.0}/modules/__init__.py +0 -0
  66. {docspan-0.2.0 → docspan-0.3.0}/modules/auth.py +0 -0
  67. {docspan-0.2.0 → docspan-0.3.0}/modules/conflict_handler.py +0 -0
  68. {docspan-0.2.0 → docspan-0.3.0}/modules/converter.py +0 -0
  69. {docspan-0.2.0 → docspan-0.3.0}/modules/gdrive_client.py +0 -0
  70. {docspan-0.2.0 → docspan-0.3.0}/modules/sync_engine.py +0 -0
  71. {docspan-0.2.0 → docspan-0.3.0}/project_plans/bidirectional-comments/plan.md +0 -0
  72. {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/implementation/adversarial-review.md +0 -0
  73. {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/implementation/plan.md +0 -0
  74. {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/implementation/release-checklist.md +0 -0
  75. {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/implementation/validation.md +0 -0
  76. {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/requirements.md +0 -0
  77. {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/research/architecture.md +0 -0
  78. {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/research/features.md +0 -0
  79. {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/research/google-docs-push.md +0 -0
  80. {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/research/pitfalls.md +0 -0
  81. {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/research/stack.md +0 -0
  82. {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/decisions/ADR-001-merge3-dependency.md +0 -0
  83. {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/decisions/ADR-002-base-content-sidecar-store.md +0 -0
  84. {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/implementation/adversarial-review.md +0 -0
  85. {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/implementation/plan.md +0 -0
  86. {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/implementation/validation.md +0 -0
  87. {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/requirements.md +0 -0
  88. {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/research/architecture.md +0 -0
  89. {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/research/features.md +0 -0
  90. {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/research/pitfalls.md +0 -0
  91. {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/research/stack.md +0 -0
  92. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/decisions/ADR-002-comment-risk-flagging-not-anchor-preservation.md +0 -0
  93. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/feature-gap-report.md +0 -0
  94. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/implementation/adversarial-review.md +0 -0
  95. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/implementation/architecture-review.md +0 -0
  96. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/implementation/plan.md +0 -0
  97. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/implementation/pre-mortem.md +0 -0
  98. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/implementation/validation.md +0 -0
  99. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/requirements.md +0 -0
  100. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/research/architecture.md +0 -0
  101. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/research/build-vs-buy.md +0 -0
  102. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/research/features.md +0 -0
  103. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/research/pitfalls.md +0 -0
  104. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/research/stack.md +0 -0
  105. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/research/ux.md +0 -0
  106. {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/workflow-runbook.md +0 -0
  107. {docspan-0.2.0 → docspan-0.3.0}/release-please-config.json +0 -0
  108. {docspan-0.2.0 → docspan-0.3.0}/requirements.txt +0 -0
  109. {docspan-0.2.0 → docspan-0.3.0}/runtime.txt +0 -0
  110. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/__init__.py +0 -0
  111. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/__main__.py +0 -0
  112. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/__init__.py +0 -0
  113. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/__init__.py +0 -0
  114. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/__init__.py +0 -0
  115. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/comparator.py +0 -0
  116. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/converter.py +0 -0
  117. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/converters.py +0 -0
  118. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/interfaces.py +0 -0
  119. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/nodes.py +0 -0
  120. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/parser.py +0 -0
  121. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/validators.py +0 -0
  122. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/visitors.py +0 -0
  123. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/backend.py +0 -0
  124. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/client.py +0 -0
  125. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/config/__init__.py +0 -0
  126. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/config/loader.py +0 -0
  127. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/config/models.py +0 -0
  128. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/config/validation.py +0 -0
  129. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/__init__.py +0 -0
  130. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/ast.py +0 -0
  131. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/extensions/__init__.py +0 -0
  132. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/extensions/frontmatter.py +0 -0
  133. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/extensions/mermaid.py +0 -0
  134. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/extensions/wikilinks.py +0 -0
  135. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/inline_parser.py +0 -0
  136. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/parser.py +0 -0
  137. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/__init__.py +0 -0
  138. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/markdown_file.py +0 -0
  139. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/page.py +0 -0
  140. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/path_utils.py +0 -0
  141. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/results.py +0 -0
  142. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/sync_status.py +0 -0
  143. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/__init__.py +0 -0
  144. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/__init__.py +0 -0
  145. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/attachment_client.py +0 -0
  146. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/base_client.py +0 -0
  147. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/client.py +0 -0
  148. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/comment_client.py +0 -0
  149. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/crawler.py +0 -0
  150. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/label_client.py +0 -0
  151. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/page_client.py +0 -0
  152. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/space_client.py +0 -0
  153. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/url_parser.py +0 -0
  154. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/__init__.py +0 -0
  155. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/auth.py +0 -0
  156. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/comments.py +0 -0
  157. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/converter.py +0 -0
  158. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/onboarding.py +0 -0
  159. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/cli/__init__.py +0 -0
  160. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/core/__init__.py +0 -0
  161. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/core/merge.py +0 -0
  162. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/core/paths.py +0 -0
  163. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/core/state.py +0 -0
  164. {docspan-0.2.0 → docspan-0.3.0}/src/docspan/core/xdg.py +0 -0
  165. {docspan-0.2.0 → docspan-0.3.0}/sync.py +0 -0
  166. {docspan-0.2.0 → docspan-0.3.0}/terraform/gcp/README.md +0 -0
  167. {docspan-0.2.0 → docspan-0.3.0}/terraform/gcp/main.tf +0 -0
  168. {docspan-0.2.0 → docspan-0.3.0}/terraform/gcp/outputs.tf +0 -0
  169. {docspan-0.2.0 → docspan-0.3.0}/terraform/gcp/variables.tf +0 -0
  170. {docspan-0.2.0 → docspan-0.3.0}/terraform/main.tf +0 -0
  171. {docspan-0.2.0 → docspan-0.3.0}/terraform/variables.tf +0 -0
  172. {docspan-0.2.0 → docspan-0.3.0}/tests/__init__.py +0 -0
  173. {docspan-0.2.0 → docspan-0.3.0}/tests/conftest.py +0 -0
  174. {docspan-0.2.0 → docspan-0.3.0}/tests/test_config.py +0 -0
  175. {docspan-0.2.0 → docspan-0.3.0}/tests/test_conflict_resolution.py +0 -0
  176. {docspan-0.2.0 → docspan-0.3.0}/tests/test_converter.py +0 -0
  177. {docspan-0.2.0 → docspan-0.3.0}/tests/test_google_comments.py +0 -0
  178. {docspan-0.2.0 → docspan-0.3.0}/tests/test_google_oauth.py +0 -0
  179. {docspan-0.2.0 → docspan-0.3.0}/tests/test_google_onboarding.py +0 -0
  180. {docspan-0.2.0 → docspan-0.3.0}/tests/test_merge.py +0 -0
  181. {docspan-0.2.0 → docspan-0.3.0}/tests/test_push_preview.py +0 -0
  182. {docspan-0.2.0 → docspan-0.3.0}/tests/test_state.py +0 -0
  183. {docspan-0.2.0 → docspan-0.3.0}/tests/test_xdg_central_config.py +0 -0
  184. {docspan-0.2.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"
@@ -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
1
  Metadata-Version: 2.5
2
2
  Name: docspan
3
- Version: 0.2.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
@@ -282,6 +284,7 @@ Auth resolution order: `credentials_path` → `ACCOUNT_A_CREDENTIALS[_PATH]` env
282
284
  | `backend` | string | — | yes | `"google_docs"` or `"confluence"` |
283
285
  | `remote_id` | string | — | yes | Google Doc ID or Confluence page ID |
284
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. |
285
288
 
286
289
  ---
287
290
 
@@ -311,6 +314,7 @@ docspan generates these files in your project directory after first sync:
311
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)
312
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
313
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)
314
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)
315
319
 
316
320
  ---
@@ -233,6 +233,7 @@ Auth resolution order: `credentials_path` → `ACCOUNT_A_CREDENTIALS[_PATH]` env
233
233
  | `backend` | string | — | yes | `"google_docs"` or `"confluence"` |
234
234
  | `remote_id` | string | — | yes | Google Doc ID or Confluence page ID |
235
235
  | `direction` | enum | `"both"` | no | `"push"`, `"pull"`, or `"both"` |
236
+ | `tab_id` | string | `null` | no | Google Docs tab id (e.g. `"t.moqlkhpwn82e"`) to target on a multi-tab doc. Ignored by the Confluence backend. If unset and the doc has more than one tab, push/pull still succeed but report a `"warning"` naming the tabs found, instead of silently syncing whichever tab is first. |
236
237
 
237
238
  ---
238
239
 
@@ -262,6 +263,7 @@ docspan generates these files in your project directory after first sync:
262
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)
263
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
264
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)
265
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)
266
268
 
267
269
  ---
@@ -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.
@@ -37,6 +37,10 @@ The Google Docs push path drops two things, both blocking real-world design-doc
37
37
 
38
38
  ### Tables (Part 2)
39
39
  - New `DocsTableNode(rows: List[List[str]], start_index, end_index)` in the structure module.
40
+ - **Superseded.** `rows` is now `List[List[TableCell]]`, where `TableCell` carries
41
+ `text` *and* `spans`. Plain-text cells meant every mark inside one was dropped —
42
+ including internal `#anchor` cross-references, which rendered as dead text in the
43
+ Doc while the identical reference in a paragraph resolved. See #49.
40
44
  - Parser emits `DocsTableNode` for `table` tokens (header row + body rows, cell = plain text).
41
45
  - `DocsStructureParser` parses live `table` elements into `DocsTableNode` (rows from
42
46
  `table.tableRows[].tableCells[].content` paragraphs), with real start/end indices.
@@ -12,7 +12,7 @@
12
12
  Research (`research/stack.md` §2, `research/build-vs-buy.md` §1c, cross-checked against multiple independent sources — Google's own REST reference, a tanaikech Apps Script gist, a Latenode community thread) confirms:
13
13
 
14
14
  - The Docs API's `createParagraphBullets` request does support a distinct `bulletPreset: "BULLET_CHECKBOX"` value — checkboxes *can* be created programmatically.
15
- - **Checked/unchecked state cannot be read back.** `documents.get()` returns `glyphType: GLYPH_TYPE_UNSPECIFIED` for a checkbox bullet regardless of whether it is checked or unchecked. The JSON is reported identical across a check/uncheck action — the only observed difference is the document's `revisionId`, which carries no state information. This is confirmed symmetric between the REST API and the Apps Script `DocumentApp` service, meaning it is a limitation of the underlying document model, not an SDK gap.
15
+ - **`documents.get()` cannot expose checked/unchecked state.** It returns `glyphType: GLYPH_TYPE_UNSPECIFIED` for a checkbox bullet regardless of whether it is checked or unchecked. The JSON is reported identical across a check/uncheck action — the only observed difference is the document's `revisionId`, which carries no state information. This is confirmed symmetric between the REST API and the Apps Script `DocumentApp` service, meaning it is a limitation of that document model surface, not an SDK gap. (Later finding, see docspan issue "pull silently reports every checked box as unchecked": Drive's `files.export(mimeType='text/markdown')` is a *different* renderer that does expose it, as GFM `- [x]`/`- [ ]` — this is now used to recover state on docspan's default, non-tab-scoped pull path. It cannot target a single tab, so the tab-scoped pull path remains genuinely unrecoverable and continues to render every native checkbox unchecked.)
16
16
  - The only indirect, *unofficial* signal is `textStyle.strikethrough` (checking a box in the UI applies strikethrough as a side effect) — but this is a UI convention, not a documented API contract, and Google Docs now offers a "checklist without strikethrough" UI option that breaks even this heuristic.
17
17
 
18
18
  Options considered:
@@ -27,9 +27,9 @@ Use **Option 2 — literal text** as the default, document-wide representation.
27
27
 
28
28
  **If the full-document survey (Task 0.1.2a) finds a mix of literal-text and native-glyph checklist paragraphs (finding (c) — the realistic case for a multi-author doc), this decision does not apply uniformly.** The literal-text scheme remains the default assumed representation, but any specific paragraph the survey identified as already being a native `BULLET_CHECKBOX` glyph is treated as **high-risk / requires manual handling**, not silently assumed to behave like the rest of the doc:
29
29
 
30
- - Those paragraphs are listed by text prefix in `feature-gap-report.md` as known native-checkbox lines docspan does not track.
31
- - Pull emits a `MixedChecklistWarning` (a `WARN`-level log line, plus a trailing marker comment in the pulled markdown, e.g. `<!-- docspan: native checkbox glyph, state not readable -->`) whenever it encounters a bullet paragraph whose resolved glyph is checkbox-shaped, so the blind spot is visible on every pull, not just documented once here and then forgotten.
32
- - Tyler continues toggling those specific lines by hand in the Docs UI; docspan's pull renders them as plain, unmarked bulleted text (no marker corruption, just invisible state) until a future cycle designs real conversion support.
30
+ - Those paragraphs are listed by text prefix in `feature-gap-report.md` as known native-checkbox lines docspan does not track as literal text.
31
+ - Pull emits a `MixedChecklistWarning` (a `WARN`-level log line, plus a trailing marker comment in the pulled markdown, e.g. `<!-- docspan: native checkbox glyph, state not readable -->`) whenever it encounters a bullet paragraph whose resolved glyph is checkbox-shaped, so the blind spot is visible on every pull, not just documented once here and then forgotten. **This warning path is superseded on the default pull path** by the markdown-export recovery described above: when the recovered count matches and every line is matched, pull instead renders the real `- [x]`/`- [ ]` state with no warning at all. The warning (now phrased as a checkbox-count/match warning rather than an absolute "not readable" claim) still fires whenever that recovery can't proceed safely — a checkbox-count mismatch, an unmatched line, or a markdown-export transport failure — and always fires on the tab-scoped path, where recovery is never attempted.
32
+ - On the tab-scoped path specifically, Tyler continues toggling those specific lines by hand in the Docs UI; docspan's pull renders them as plain, unmarked bulleted text (no marker corruption, just invisible state) until a future cycle designs real conversion support for that path too.
33
33
  - **As of the pre-mortem repair pass, `push()` itself also refuses (fail-closed, `--force`-gated) to write through any of these specific native-glyph paragraphs.** A live `GlyphShapeCheck`, folded into `find_high_risk_paragraphs()` alongside the existing comment-risk check, re-resolves each changed paragraph's glyph shape from `push()`'s own single fetch — not from this ADR's static survey table — and flags it the same way an open-comment paragraph is flagged (same `HighRiskParagraph`/`--force` mechanism, see plan.md Story 1.2.2/1.2.3). This closes pre-mortem.md #1: without it, checking off a paragraph the survey found to be a native glyph would layer a literal `[x]`/`[ ]` marker onto that glyph with no warning and no `--force` requirement.
34
34
 
35
35
  ## Rationale
@@ -57,7 +57,7 @@ Use **Option 2 — literal text** as the default, document-wide representation.
57
57
 
58
58
  ## Consequences
59
59
 
60
- - No new fields are added to `DocsParagraphNode` (no `checked: Optional[bool]`), keeping the diff key (`style, text, is_list_item`) and all downstream request-building logic untouched.
60
+ - No new fields are added to `DocsParagraphNode` (no `checked: Optional[bool]`), keeping the diff key (`style, text, is_list_item`) and all downstream request-building logic untouched. Native-checkbox checked-state recovery on pull (see above) is implemented as a separate, purely functional post-processing step (`checkbox_state.py`) over the already-rendered markdown string, not as a new node field — this decision's diff-key claim still holds.
61
61
  - A checklist toggle always produces a `deleteContentRange` + `insertText` (delete+reinsert) for that paragraph, exactly like any other text edit to that paragraph — this is the same mechanism that risks dropping an anchored comment on that paragraph (see ADR-002 for the mitigation).
62
- - If a future cycle wants native checkbox glyphs purely for visual polish (not as source of truth), that can be added later as a strictly additive, cosmetic push-time flag without touching this decision but must never become the mechanism used to detect checked/unchecked state on pull.
63
- - **If the survey finds a mixed document (finding (c)):** the literal-text scheme is not a safe blanket assumption — specific paragraphs are exceptions, tracked by name, and flagged loudly (not silently) on every pull via `MixedChecklistWarning`. Tyler must treat any doc-wide claim like "checklist state round-trips correctly" (Success Metric 3) as true only for the literal-text paragraphs, not the whole document, until a future cycle closes this gap. Since the pre-mortem repair pass, `push()` also refuses to write through one of these specific paragraphs without `--force` (`GlyphShapeCheck`, plan.md Story 1.2.2/1.2.3) — the mixed-doc case is now guarded at write time, not only disclosed at read time via `MixedChecklistWarning`.
62
+ - If a future cycle wants native checkbox glyphs purely for visual polish (not as source of truth), that can be added later as a strictly additive, cosmetic push-time flag without touching this decision. Push behavior for native checkboxes remains unchanged by the pull-side recovery above: an edit to one of these paragraphs is still routed through the existing `GlyphShapeCheck` high-risk gate, never silently applied.
63
+ - **If the survey finds a mixed document (finding (c)):** the literal-text scheme is not a safe blanket assumption — specific paragraphs are exceptions, tracked by name, and flagged loudly (not silently) on every pull via `MixedChecklistWarning` whenever native-checkbox state can't be safely recovered (mismatch, transport failure, or the tab-scoped path). Tyler must treat any doc-wide claim like "checklist state round-trips correctly" (Success Metric 3) as true for the literal-text paragraphs, and — on the default pull path, when recovery succeeds for native-checkbox paragraphs too; only the tab-scoped path remains an unconditional gap. Since the pre-mortem repair pass, `push()` also refuses to write through one of these specific paragraphs without `--force` (`GlyphShapeCheck`, plan.md Story 1.2.2/1.2.3) — the mixed-doc case is guarded at write time and, for the default pull path, largely resolved at read time.
@@ -23,6 +23,8 @@ classifiers = [
23
23
  "Programming Language :: Python :: 3.10",
24
24
  "Programming Language :: Python :: 3.11",
25
25
  "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
26
28
  "Topic :: Software Development :: Documentation",
27
29
  "Topic :: Text Processing :: Markup :: Markdown",
28
30
  ]
@@ -94,3 +96,18 @@ disallow_untyped_defs = true
94
96
  [tool.pytest.ini_options]
95
97
  testpaths = ["tests"]
96
98
  python_files = "test_*.py"
99
+
100
+ # Resolve against the public index explicitly, overriding whatever index the
101
+ # machine is configured with. uv's precedence is CLI > env > this file > the
102
+ # user's ~/.config/uv/uv.toml > system config, so a contributor whose machine
103
+ # points uv at a private or corporate mirror still gets a lockfile with
104
+ # pypi.org URLs.
105
+ #
106
+ # Without this, any `uv run`/`uv lock` on such a machine silently rewrites every
107
+ # URL in uv.lock to that mirror. Committing the result leaks the internal
108
+ # hostname into this public repo and breaks CI, which cannot reach it — and the
109
+ # diff is thousands of near-identical lines nobody reads. In CI there is no user
110
+ # config, so this is a no-op there. `ci.yml` greps uv.lock as the backstop.
111
+ [[tool.uv.index]]
112
+ url = "https://pypi.org/simple"
113
+ default = true
@@ -33,7 +33,7 @@ class PushResult:
33
33
 
34
34
  @dataclass
35
35
  class PullResult:
36
- status: Literal["ok", "conflict", "error", "skipped"]
36
+ status: Literal["ok", "conflict", "error", "skipped", "warning"]
37
37
  doc_id: str
38
38
  local_path: str
39
39
  message: Optional[str] = None