docspan 0.2.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docspan-0.4.0/.github/workflows/ci.yml +161 -0
- {docspan-0.2.0 → docspan-0.4.0}/.gitignore +5 -0
- docspan-0.4.0/.mypy-error-baseline +1 -0
- docspan-0.4.0/.release-please-manifest.json +3 -0
- docspan-0.4.0/CHANGELOG.md +170 -0
- {docspan-0.2.0 → docspan-0.4.0}/PKG-INFO +10 -3
- {docspan-0.2.0 → docspan-0.4.0}/README.md +6 -2
- {docspan-0.2.0 → docspan-0.4.0}/docs/backends/confluence.md +1 -0
- docspan-0.4.0/docs/backends/google-docs.md +67 -0
- {docspan-0.2.0 → docspan-0.4.0}/docs/index.md +3 -1
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/gdocs-tables-inline-styles/plan.md +4 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/decisions/ADR-001-checklist-state-as-literal-text.md +7 -7
- docspan-0.4.0/project_plans/wedding-planning-workflow/decisions/ADR-003-no-comment-anchor-migration.md +128 -0
- {docspan-0.2.0 → docspan-0.4.0}/pyproject.toml +18 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/base.py +18 -3
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/backend.py +21 -1
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/models.py +5 -1
- docspan-0.4.0/src/docspan/backends/google_docs/backend.py +1228 -0
- docspan-0.4.0/src/docspan/backends/google_docs/checkbox_state.py +100 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/google_docs/client.py +108 -4
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/google_docs/converter.py +6 -3
- docspan-0.4.0/src/docspan/backends/google_docs/cross_doc_links.py +286 -0
- docspan-0.4.0/src/docspan/backends/google_docs/docs_request_builder.py +2768 -0
- docspan-0.4.0/src/docspan/backends/google_docs/docs_structure_parser.py +730 -0
- docspan-0.4.0/src/docspan/backends/google_docs/heading_anchors.py +482 -0
- docspan-0.4.0/src/docspan/backends/google_docs/image_source.py +260 -0
- docspan-0.4.0/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +603 -0
- docspan-0.4.0/src/docspan/backends/google_docs/mermaid_renderer.py +100 -0
- docspan-0.4.0/src/docspan/backends/google_docs/nodes_to_markdown.py +538 -0
- docspan-0.4.0/src/docspan/backends/google_docs/projection.py +302 -0
- docspan-0.4.0/src/docspan/backends/google_docs/push_preview.py +380 -0
- docspan-0.4.0/src/docspan/backends/google_docs/registry.py +67 -0
- docspan-0.4.0/src/docspan/backends/google_docs/tabs.py +175 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/cli/main.py +231 -6
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/config.py +99 -1
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/core/orchestrator.py +31 -8
- docspan-0.4.0/tests/__init__.py +0 -0
- docspan-0.4.0/tests/fixtures/github_slugger_vectors.json +349 -0
- docspan-0.4.0/tests/test_checkbox_state.py +74 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_cli.py +380 -4
- docspan-0.4.0/tests/test_code_block_granularity.py +998 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_config.py +79 -2
- docspan-0.4.0/tests/test_confluence_backend.py +47 -0
- docspan-0.4.0/tests/test_confluence_mermaid_push_pipeline.py +101 -0
- docspan-0.4.0/tests/test_content_key_pooling_performance.py +115 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_converter.py +18 -0
- docspan-0.4.0/tests/test_cross_doc_link_issues.py +135 -0
- docspan-0.4.0/tests/test_cross_doc_links.py +310 -0
- docspan-0.4.0/tests/test_cross_doc_links_backend.py +481 -0
- docspan-0.4.0/tests/test_docs_request_builder.py +1188 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_docs_structure_parser.py +263 -0
- docspan-0.4.0/tests/test_gdocs_images.py +427 -0
- docspan-0.4.0/tests/test_gdocs_mermaid.py +119 -0
- docspan-0.4.0/tests/test_gdocs_push_pipeline.py +1380 -0
- docspan-0.4.0/tests/test_gdocs_tables_and_styles.py +454 -0
- docspan-0.4.0/tests/test_google_docs_backend.py +2009 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_google_onboarding.py +8 -2
- docspan-0.4.0/tests/test_heading_anchors.py +1320 -0
- docspan-0.4.0/tests/test_heading_identity.py +982 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_markdown_to_paragraph_parser.py +41 -0
- docspan-0.4.0/tests/test_nodes_to_markdown.py +408 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_orchestrator.py +85 -2
- docspan-0.4.0/tests/test_push_preview.py +689 -0
- docspan-0.4.0/tests/test_registry.py +93 -0
- docspan-0.4.0/tests/test_restyle_destruction_rate.py +149 -0
- docspan-0.4.0/tests/test_span_trailing_newline.py +111 -0
- docspan-0.4.0/tests/test_table_cell_spans.py +673 -0
- docspan-0.4.0/tests/test_tabs.py +250 -0
- {docspan-0.2.0 → docspan-0.4.0}/uv.lock +85 -74
- docspan-0.2.0/.github/workflows/ci.yml +0 -34
- docspan-0.2.0/.release-please-manifest.json +0 -3
- docspan-0.2.0/CHANGELOG.md +0 -67
- docspan-0.2.0/docs/backends/google-docs.md +0 -62
- docspan-0.2.0/src/docspan/backends/google_docs/backend.py +0 -473
- docspan-0.2.0/src/docspan/backends/google_docs/docs_request_builder.py +0 -529
- docspan-0.2.0/src/docspan/backends/google_docs/docs_structure_parser.py +0 -212
- docspan-0.2.0/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +0 -263
- docspan-0.2.0/src/docspan/backends/google_docs/push_preview.py +0 -201
- docspan-0.2.0/tests/test_docs_request_builder.py +0 -378
- docspan-0.2.0/tests/test_gdocs_tables_and_styles.py +0 -237
- docspan-0.2.0/tests/test_google_docs_backend.py +0 -340
- docspan-0.2.0/tests/test_push_preview.py +0 -338
- {docspan-0.2.0 → docspan-0.4.0}/.github/workflows/publish.yml +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/.github/workflows/release-please.yml +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/CONTRIBUTING.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/Procfile +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/RAILWAY_SETUP.md +0 -0
- /docspan-0.2.0/src/docspan/__main__.py → /docspan-0.4.0/doc.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/docs/commands.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/docs/configuration.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/docs/contributing.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/docs/install.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/docspan.yaml.example +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/markgate.yaml.example +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/mkdocs.yml +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/modules/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/modules/auth.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/modules/conflict_handler.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/modules/converter.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/modules/gdrive_client.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/modules/sync_engine.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/bidirectional-comments/plan.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/adversarial-review.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/plan.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/release-checklist.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/validation.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/requirements.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/research/architecture.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/research/features.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/research/google-docs-push.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/research/pitfalls.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/docspan-release/research/stack.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/decisions/ADR-001-merge3-dependency.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/decisions/ADR-002-base-content-sidecar-store.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/implementation/adversarial-review.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/implementation/plan.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/implementation/validation.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/requirements.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/research/architecture.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/research/features.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/research/pitfalls.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/markgate-sync/research/stack.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/decisions/ADR-002-comment-risk-flagging-not-anchor-preservation.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/feature-gap-report.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/adversarial-review.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/architecture-review.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/plan.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/pre-mortem.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/validation.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/requirements.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/architecture.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/build-vs-buy.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/features.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/pitfalls.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/stack.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/ux.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/workflow-runbook.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/release-please-config.json +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/requirements.txt +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/runtime.txt +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/__init__.py +0 -0
- /docspan-0.2.0/src/docspan/backends/confluence/__init__.py → /docspan-0.4.0/src/docspan/__main__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/__init__.py +0 -0
- {docspan-0.2.0/src/docspan/backends/confluence/services → docspan-0.4.0/src/docspan/backends/confluence}/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/comparator.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/converter.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/converters.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/interfaces.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/nodes.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/parser.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/validators.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/visitors.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/client.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/loader.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/validation.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/ast.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/frontmatter.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/mermaid.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/wikilinks.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/inline_parser.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/parser.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/markdown_file.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/page.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/path_utils.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/results.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/sync_status.py +0 -0
- {docspan-0.2.0/src/docspan/backends/google_docs → docspan-0.4.0/src/docspan/backends/confluence/services}/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/attachment_client.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/base_client.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/client.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/comment_client.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/crawler.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/label_client.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/page_client.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/space_client.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/url_parser.py +0 -0
- {docspan-0.2.0/src/docspan/cli → docspan-0.4.0/src/docspan/backends/google_docs}/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/google_docs/auth.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/google_docs/comments.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/backends/google_docs/onboarding.py +0 -0
- {docspan-0.2.0/tests → docspan-0.4.0/src/docspan/cli}/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/core/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/core/merge.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/core/paths.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/core/state.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/src/docspan/core/xdg.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/sync.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/terraform/gcp/README.md +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/terraform/gcp/main.tf +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/terraform/gcp/outputs.tf +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/terraform/gcp/variables.tf +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/terraform/main.tf +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/terraform/variables.tf +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/conftest.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_conflict_resolution.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_google_comments.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_google_oauth.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_merge.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_state.py +0 -0
- {docspan-0.2.0 → docspan-0.4.0}/tests/test_xdg_central_config.py +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
# No `branches:` filter — every pull request runs CI regardless of its base.
|
|
7
|
+
# With the filter, a stacked PR (one based on another PR's branch rather than
|
|
8
|
+
# on main) got no checks at all, so the only branch in a stack that was ever
|
|
9
|
+
# tested was the bottom one.
|
|
10
|
+
pull_request:
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
# A stack of PRs rebases repeatedly, and each push supersedes the run before it.
|
|
14
|
+
# Cancel the outdated run rather than paying for the whole matrix on a commit
|
|
15
|
+
# nobody will look at. `main` is excluded from cancellation so a push there
|
|
16
|
+
# always produces a complete result.
|
|
17
|
+
concurrency:
|
|
18
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
19
|
+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
lint:
|
|
23
|
+
name: Lint
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
with:
|
|
28
|
+
fetch-depth: 0 # needed for hatch-vcs version detection
|
|
29
|
+
|
|
30
|
+
- name: Install uv
|
|
31
|
+
uses: astral-sh/setup-uv@v4
|
|
32
|
+
|
|
33
|
+
- name: Install dependencies
|
|
34
|
+
run: uv sync --extra dev
|
|
35
|
+
|
|
36
|
+
# Runs once rather than once per Python version — ruff's result does not
|
|
37
|
+
# depend on the interpreter it runs under (`target-version` is pinned in
|
|
38
|
+
# pyproject.toml), so the other five runs were duplicate work.
|
|
39
|
+
- name: ruff check
|
|
40
|
+
run: uv run ruff check src tests
|
|
41
|
+
|
|
42
|
+
test:
|
|
43
|
+
name: Test (${{ matrix.python-version }})
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
strategy:
|
|
46
|
+
fail-fast: false # one version failing shouldn't hide the others' results
|
|
47
|
+
matrix:
|
|
48
|
+
# pyproject declares requires-python = ">=3.9", so every version it
|
|
49
|
+
# claims to support gets tested. 3.13 and 3.14 were verified green
|
|
50
|
+
# before being added here.
|
|
51
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
52
|
+
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
with:
|
|
56
|
+
fetch-depth: 0 # needed for hatch-vcs version detection
|
|
57
|
+
|
|
58
|
+
- name: Install uv
|
|
59
|
+
uses: astral-sh/setup-uv@v4
|
|
60
|
+
|
|
61
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
62
|
+
run: uv python install ${{ matrix.python-version }}
|
|
63
|
+
|
|
64
|
+
- name: Install dependencies
|
|
65
|
+
run: uv sync --extra dev
|
|
66
|
+
|
|
67
|
+
- name: Test
|
|
68
|
+
run: uv run pytest --tb=short -q
|
|
69
|
+
|
|
70
|
+
types:
|
|
71
|
+
name: Types (mypy ratchet)
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v4
|
|
75
|
+
with:
|
|
76
|
+
fetch-depth: 0
|
|
77
|
+
|
|
78
|
+
- name: Install uv
|
|
79
|
+
uses: astral-sh/setup-uv@v4
|
|
80
|
+
|
|
81
|
+
- name: Install dependencies
|
|
82
|
+
run: uv sync --extra dev
|
|
83
|
+
|
|
84
|
+
# mypy cannot be a plain pass/fail gate here: `mypy src` currently reports
|
|
85
|
+
# 266 errors across 32 files, so adding it as one would leave CI
|
|
86
|
+
# permanently red, and `continue-on-error` would make it decorative.
|
|
87
|
+
#
|
|
88
|
+
# A ratchet is a real gate instead — it fails on any *new* error while
|
|
89
|
+
# tolerating the existing backlog, and the baseline only ever goes down.
|
|
90
|
+
# Delete this job and gate on mypy's exit code once the baseline reaches 0.
|
|
91
|
+
- name: mypy (must not exceed baseline)
|
|
92
|
+
run: |
|
|
93
|
+
set -uo pipefail
|
|
94
|
+
baseline=$(tr -dc '0-9' < .mypy-error-baseline)
|
|
95
|
+
if [ -z "$baseline" ]; then
|
|
96
|
+
echo "::error::.mypy-error-baseline does not contain a number"
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
output=$(uv run mypy src 2>&1 || true)
|
|
101
|
+
echo "$output" | tail -40
|
|
102
|
+
|
|
103
|
+
if printf '%s\n' "$output" | grep -q '^Success: no issues found'; then
|
|
104
|
+
count=0
|
|
105
|
+
else
|
|
106
|
+
count=$(printf '%s\n' "$output" \
|
|
107
|
+
| sed -n 's/^Found \([0-9]\{1,\}\) error.*/\1/p' | tail -1)
|
|
108
|
+
fi
|
|
109
|
+
if [ -z "$count" ]; then
|
|
110
|
+
echo "::error::could not parse an error count out of mypy's output"
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
echo "mypy errors: $count (baseline $baseline)"
|
|
115
|
+
if [ "$count" -gt "$baseline" ]; then
|
|
116
|
+
echo "::error::mypy errors rose from $baseline to $count." \
|
|
117
|
+
"Fix the new errors, or justify and raise .mypy-error-baseline."
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
if [ "$count" -lt "$baseline" ]; then
|
|
121
|
+
echo "::notice::mypy errors fell from $baseline to $count." \
|
|
122
|
+
"Please lower .mypy-error-baseline to $count to lock the gain in."
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
package:
|
|
126
|
+
name: Package
|
|
127
|
+
runs-on: ubuntu-latest
|
|
128
|
+
steps:
|
|
129
|
+
- uses: actions/checkout@v4
|
|
130
|
+
with:
|
|
131
|
+
fetch-depth: 0 # hatch-vcs derives the version from tags
|
|
132
|
+
|
|
133
|
+
- name: Install uv
|
|
134
|
+
uses: astral-sh/setup-uv@v4
|
|
135
|
+
|
|
136
|
+
# publish.yml only builds on `release: published`, so until now a change
|
|
137
|
+
# that broke packaging stayed invisible until release day.
|
|
138
|
+
- name: Build sdist and wheel
|
|
139
|
+
run: uv build
|
|
140
|
+
|
|
141
|
+
# Catches a dependency edited in pyproject.toml without re-locking, which
|
|
142
|
+
# would otherwise surface as `uv sync` resolving something the lockfile
|
|
143
|
+
# never pinned.
|
|
144
|
+
- name: Lockfile is in sync with pyproject
|
|
145
|
+
run: uv lock --check
|
|
146
|
+
|
|
147
|
+
# uv.lock records the index each package came from. Resolving it behind a
|
|
148
|
+
# private or corporate mirror rewrites every URL to that host, and
|
|
149
|
+
# committing the result both leaks the internal hostname into this public
|
|
150
|
+
# repo and breaks CI, which cannot reach it. `git add -A` will happily
|
|
151
|
+
# sweep that in, and the diff is thousands of near-identical lines that
|
|
152
|
+
# nobody reads.
|
|
153
|
+
- name: Lockfile references only the public index
|
|
154
|
+
run: |
|
|
155
|
+
set -uo pipefail
|
|
156
|
+
if grep -n 'registry = "' uv.lock | grep -v 'https://pypi.org/simple'; then
|
|
157
|
+
echo "::error file=uv.lock::uv.lock references a non-public index." \
|
|
158
|
+
"Re-resolve it with UV_INDEX_URL=https://pypi.org/simple uv lock."
|
|
159
|
+
exit 1
|
|
160
|
+
fi
|
|
161
|
+
echo "uv.lock: public index only"
|
|
@@ -65,3 +65,8 @@ site/
|
|
|
65
65
|
*.tfstate.backup
|
|
66
66
|
*.tfvars
|
|
67
67
|
.terraform.lock.hcl
|
|
68
|
+
|
|
69
|
+
# Session scaffolding — regenerated per backlog-session spawn/attach, never a
|
|
70
|
+
# repo deliverable (see commits 8f80a5b, bd693ca on other branches)
|
|
71
|
+
.backlog-context.md
|
|
72
|
+
.claude/commands/backlog/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
266
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.4.0](https://github.com/tstapler/docspan/compare/docspan-v0.3.0...docspan-v0.4.0) (2026-08-13)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **docspan:** add docspan map command and push auto-create for unmapped files ([#80](https://github.com/tstapler/docspan/issues/80)) ([aa4258b](https://github.com/tstapler/docspan/commit/aa4258b3ff381c3e6afc389ad55c426abb5850ba))
|
|
14
|
+
* **google-docs:** add inline image push/pull support ([#101](https://github.com/tstapler/docspan/issues/101)) ([e5256af](https://github.com/tstapler/docspan/commit/e5256afa18076051b891f44511786e4f2750a489))
|
|
15
|
+
* **google-docs:** render mermaid diagrams as inline PNGs on push ([9298d1b](https://github.com/tstapler/docspan/commit/9298d1b0c645231056571a2c5afaa296e14d751b))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **config:** round-trip YAML comments on save_config writes ([780ddcc](https://github.com/tstapler/docspan/commit/780ddcc18b760f184627eb9c9a2b870249c6ed51))
|
|
21
|
+
* **confluence:** confirm mermaid render pipeline is a stub, not a rasterizer ([#94](https://github.com/tstapler/docspan/issues/94)) ([fb5ca80](https://github.com/tstapler/docspan/commit/fb5ca8002e91e050447766d7e498ff30eadae9dc))
|
|
22
|
+
* **google-docs:** bound difflib's cubic-ish blowup on duplicate-heavy documents ([#84](https://github.com/tstapler/docspan/issues/84)) ([834df71](https://github.com/tstapler/docspan/commit/834df71d2737b09ee68b14dd1f509bc3928249da))
|
|
23
|
+
* **google-docs:** distinguish delete-and-reinsert churn from real removal in push preview ([#86](https://github.com/tstapler/docspan/issues/86)) ([052b64d](https://github.com/tstapler/docspan/commit/052b64d591bc88ab6962e5173c2494b2fedab1c3))
|
|
24
|
+
* **google-docs:** escape backticks in monospace spans on both pull paths ([#103](https://github.com/tstapler/docspan/issues/103)) ([74d007d](https://github.com/tstapler/docspan/commit/74d007d162d511641650d2afb8b9f8b482c3e48e))
|
|
25
|
+
* **google-docs:** land PR [#70](https://github.com/tstapler/docspan/issues/70) restyle repair, verify AC0-8 (issue [#52](https://github.com/tstapler/docspan/issues/52)) ([#99](https://github.com/tstapler/docspan/issues/99)) ([8203c93](https://github.com/tstapler/docspan/commit/8203c93567a3d962bb3bb5754338298260f610aa))
|
|
26
|
+
* **google-docs:** order same-anchor insert groups after restyle/delete groups ([#83](https://github.com/tstapler/docspan/issues/83)) ([b8eace0](https://github.com/tstapler/docspan/commit/b8eace0505f7196eeb5bfe659f2d284a4b8c07ad))
|
|
27
|
+
* **google-docs:** render multi-paragraph table cells as HTML tables ([#79](https://github.com/tstapler/docspan/issues/79)) ([45e072d](https://github.com/tstapler/docspan/commit/45e072de2ae845443d6b5862a6c70234a55fb501))
|
|
28
|
+
* **google-docs:** resolve cross-tab heading anchors on push ([#102](https://github.com/tstapler/docspan/issues/102)) ([c1be541](https://github.com/tstapler/docspan/commit/c1be541d7e8d227369367ac8d99700405ac31195))
|
|
29
|
+
* **google-docs:** resolve relative cross-document markdown links to target Google Doc URLs ([#98](https://github.com/tstapler/docspan/issues/98)) ([27e7f15](https://github.com/tstapler/docspan/commit/27e7f1551549d7bff9d64b5e43abfd09f4f19e36))
|
|
30
|
+
* **google-docs:** richer at-risk-comment warning, no anchor migration ([#92](https://github.com/tstapler/docspan/issues/92)) ([#95](https://github.com/tstapler/docspan/issues/95)) ([be854db](https://github.com/tstapler/docspan/commit/be854db8cc4dd5a8252685847455e51d444decee))
|
|
31
|
+
* **google-docs:** split/preserve fenced code blocks in list items and blockquotes ([#87](https://github.com/tstapler/docspan/issues/87)) ([0a01f9f](https://github.com/tstapler/docspan/commit/0a01f9f6c927489c9234049aed56b4fe8c895b58))
|
|
32
|
+
* **google-docs:** stop force-push from corrupting tab-scoped checkbox docs ([#97](https://github.com/tstapler/docspan/issues/97)) ([63ab43b](https://github.com/tstapler/docspan/commit/63ab43bc755dad67ce22d516b6ea4f1eaeba9c3e))
|
|
33
|
+
* **google-docs:** stop replace branch from duplicating the doc-end-clamped newline ([#85](https://github.com/tstapler/docspan/issues/85)) ([68f0de9](https://github.com/tstapler/docspan/commit/68f0de985be5b13df0d4e1b3c4b0177a2e0eced8))
|
|
34
|
+
|
|
35
|
+
## [0.3.0](https://github.com/tstapler/docspan/compare/docspan-v0.2.0...docspan-v0.3.0) (2026-08-11)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Features
|
|
39
|
+
|
|
40
|
+
* **gdocs:** keep inline styling inside table cells ([#51](https://github.com/tstapler/docspan/issues/51)) ([880eeb1](https://github.com/tstapler/docspan/commit/880eeb1feb3a3f700a1641f360abf5bc32cab191)), closes [#49](https://github.com/tstapler/docspan/issues/49)
|
|
41
|
+
* **google-docs:** add Google Doc tab support (tab_id) ([#14](https://github.com/tstapler/docspan/issues/14)) ([cee1db6](https://github.com/tstapler/docspan/commit/cee1db6dde0c957088159339527a6b8dbef70b47))
|
|
42
|
+
* **google-docs:** resolve internal markdown anchors to heading links ([#36](https://github.com/tstapler/docspan/issues/36)) ([5faaa6c](https://github.com/tstapler/docspan/commit/5faaa6c7ded880eba048d20807ff76a4a5f480a8))
|
|
43
|
+
* **google-docs:** restyle a paragraph in place instead of retyping it ([edf0b13](https://github.com/tstapler/docspan/commit/edf0b13520aa1a60b1fb02ee51dc1bd4a967c412))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Bug Fixes
|
|
47
|
+
|
|
48
|
+
* **cli:** accept --config and --prefix before the subcommand as well as after ([00f6524](https://github.com/tstapler/docspan/commit/00f65241e41c5bb2deec5642eb36e8ea64a58744))
|
|
49
|
+
* **gdocs:** stop a repeated line stealing a live heading's identity ([#50](https://github.com/tstapler/docspan/issues/50)) ([2992d39](https://github.com/tstapler/docspan/commit/2992d3974dc9f7dea2547b5b8b3881f7c141f810))
|
|
50
|
+
* **google-docs:** align pass 2 by content so trimmed deletes can't mis-style ([ba56e08](https://github.com/tstapler/docspan/commit/ba56e08b7cc482c6bd569e9e5560e1c71a768ebf))
|
|
51
|
+
* **google-docs:** append past the last node without merging it into the last paragraph ([1aef861](https://github.com/tstapler/docspan/commit/1aef861981ee95463e7c2c9caa67bf32d90c9d2a))
|
|
52
|
+
* **google-docs:** apply inline styling when it is the only change ([c205552](https://github.com/tstapler/docspan/commit/c205552aed5c0f483ba5213bd39f2aff16587efe))
|
|
53
|
+
* **google-docs:** clear the inherited bullet on inserted non-list paragraphs ([dda8a50](https://github.com/tstapler/docspan/commit/dda8a505e8f822c077478656158b87cf6d74132b))
|
|
54
|
+
* **google-docs:** insert before a Table/ToC/SectionBreak into the body, not into it ([701e2d0](https://github.com/tstapler/docspan/commit/701e2d01433e9efc2c42a159ffb4f92d90972c56))
|
|
55
|
+
* **google-docs:** keep the paragraph newline out of the spans ([#35](https://github.com/tstapler/docspan/issues/35)) ([f73272a](https://github.com/tstapler/docspan/commit/f73272a0422e5c525a63bcedb6cb68fdb7ff517b))
|
|
56
|
+
* **google-docs:** make push idempotent for documents with fenced code blocks ([#41](https://github.com/tstapler/docspan/issues/41)) ([e98406e](https://github.com/tstapler/docspan/commit/e98406e2858958421c3bdf5e17c94f880056e540))
|
|
57
|
+
* **google-docs:** make render_prefix part of node/content identity ([#67](https://github.com/tstapler/docspan/issues/67)) ([6205850](https://github.com/tstapler/docspan/commit/62058503c38372dec7de64e4a8ea1b6603c7c2fc))
|
|
58
|
+
* **google-docs:** normalize away the render glyph Docs writes before a native code block ([#48](https://github.com/tstapler/docspan/issues/48)) ([31b4edd](https://github.com/tstapler/docspan/commit/31b4edd1bc944f1b48605d3a1ffe0d2283e161fe))
|
|
59
|
+
* **google-docs:** order pass-1 requests by their anchor, not their own index ([58e2b6a](https://github.com/tstapler/docspan/commit/58e2b6af2271dc200c225c39c44d1a567ae5722a))
|
|
60
|
+
* **google-docs:** pair Nth live table with Nth target regardless of emptiness ([#77](https://github.com/tstapler/docspan/issues/77)) ([46f96b6](https://github.com/tstapler/docspan/commit/46f96b6982bfade9656c09ea543bcdf9b692e876))
|
|
61
|
+
* **google-docs:** prefer a code-rendered node over a duplicate-text prose node ([#75](https://github.com/tstapler/docspan/issues/75)) ([5853566](https://github.com/tstapler/docspan/commit/5853566d4aaa3f57755373bccdaedde42709e9c5))
|
|
62
|
+
* **google-docs:** project empty paragraphs out of the diff instead of deleting them ([d8b1b5f](https://github.com/tstapler/docspan/commit/d8b1b5feb56439da34ae7be64c092b80927c30a7))
|
|
63
|
+
* **google-docs:** project the re-fetched live doc before pass-2 style alignment ([#69](https://github.com/tstapler/docspan/issues/69)) ([cf36561](https://github.com/tstapler/docspan/commit/cf36561b953e7d2c2da4c11f60c2a74bca3a2259))
|
|
64
|
+
* **google-docs:** recover native checkbox checked state on pull via markdown export ([#78](https://github.com/tstapler/docspan/issues/78)) ([c7352f7](https://github.com/tstapler/docspan/commit/c7352f79d38c19c092ade81cf1368dc3c72ad853))
|
|
65
|
+
* **google-docs:** render @-mention person chips as name/email text ([#15](https://github.com/tstapler/docspan/issues/15)) ([f5d7427](https://github.com/tstapler/docspan/commit/f5d742758855aeab435c50fba3a1cb51b9a79eef))
|
|
66
|
+
* **google-docs:** render fenced code blocks on tab-scoped pull instead of per-line inline code ([#74](https://github.com/tstapler/docspan/issues/74)) ([3bae98a](https://github.com/tstapler/docspan/commit/3bae98ac01007ca33288c6d87b2bf8288291c216))
|
|
67
|
+
* **google-docs:** render TITLE/SUBTITLE as headings instead of silently demoting them ([cc6cd0b](https://github.com/tstapler/docspan/commit/cc6cd0b6e8fdb8337cfa871f77c4466445041922))
|
|
68
|
+
* **google-docs:** report a dropped over-long span, and share the delete-trim arithmetic ([daa77a6](https://github.com/tstapler/docspan/commit/daa77a6398f9ed967deef1376b39d73e66050aea))
|
|
69
|
+
* **google-docs:** stop deleting the newline that anchors a table/ToC/section break ([9eba496](https://github.com/tstapler/docspan/commit/9eba496ee1d4db3fb788e910c359d9487f217baa))
|
|
70
|
+
* **google-docs:** stop replace inserts from duplicating a clamp-spared newline ([#76](https://github.com/tstapler/docspan/issues/76)) ([f440f68](https://github.com/tstapler/docspan/commit/f440f68b3fcd9fce0333dfb95b60a40e5638c7e3))
|
|
71
|
+
|
|
72
|
+
## [0.2.0](https://github.com/tstapler/docspan/compare/docspan-v0.1.0...docspan-v0.2.0) (2026-07-22)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
### Features
|
|
76
|
+
|
|
77
|
+
* Add Railway Volume support for persistent state storage ([3a4b76e](https://github.com/tstapler/docspan/commit/3a4b76eb0cbc14afa02f3aa3de2c4607808fad9f))
|
|
78
|
+
* Add retry mechanism and improved error handling for Google Drive API ([e8a7b5f](https://github.com/tstapler/docspan/commit/e8a7b5f177ad2c3b8a356852eceb827326f8ce76))
|
|
79
|
+
* Auto-reload Google Sheet mappings on each sync cycle ([a2647c6](https://github.com/tstapler/docspan/commit/a2647c6c1435eb5624a389a4e673a57ead012123))
|
|
80
|
+
* **config:** XDG storage paths + central config with project prefixes ([#7](https://github.com/tstapler/docspan/issues/7)) ([0aa9165](https://github.com/tstapler/docspan/commit/0aa9165d24df95386b514d283cf846a2cdc809f7))
|
|
81
|
+
* **confluence:** port adf/markdown/services from markdown-confluence ([e9d1a85](https://github.com/tstapler/docspan/commit/e9d1a85a9747ac75a6d95d6351d18483297726a4))
|
|
82
|
+
* **google_docs:** checklist round-trip + comment/glyph-risk push gate ([#8](https://github.com/tstapler/docspan/issues/8)) ([bd2a885](https://github.com/tstapler/docspan/commit/bd2a885d6a2e18b758f12fc4a2aaf588c045d059))
|
|
83
|
+
* **google_docs:** docspan comments respond — reply/resolve round-trip ([#13](https://github.com/tstapler/docspan/issues/13)) ([7a662ed](https://github.com/tstapler/docspan/commit/7a662edb14b6f0078fcca110023cfc7e54724726))
|
|
84
|
+
* **google-docs:** add per-user OAuth auth option ([#4](https://github.com/tstapler/docspan/issues/4)) ([830369c](https://github.com/tstapler/docspan/commit/830369cba1817224ae0d02f0b14b6a84de84a4eb))
|
|
85
|
+
* **google-docs:** push markdown tables and inline links/formatting ([#3](https://github.com/tstapler/docspan/issues/3)) ([5b74246](https://github.com/tstapler/docspan/commit/5b74246eb1070355b39c5e84292e59f443457875))
|
|
86
|
+
* **google-docs:** read comments into a {file}.comments.md sidecar on pull ([#5](https://github.com/tstapler/docspan/issues/5)) ([aca2264](https://github.com/tstapler/docspan/commit/aca226412ef74c80603678d7ae1defaa25e38954))
|
|
87
|
+
* scaffold markgate package from google-docs-obsidian-sync fork ([44dd3c5](https://github.com/tstapler/docspan/commit/44dd3c586a670b4689154b2db9bc6cb8673d9702))
|
|
88
|
+
* **sync:** Google Docs structural-diff push, Confluence comments, three-way merge ([9a20e34](https://github.com/tstapler/docspan/commit/9a20e3452f2d92a240128d7c8e2f9c4b63a547f9))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
### Bug Fixes
|
|
92
|
+
|
|
93
|
+
* **ci:** add __future__ annotations for Python 3.9 compat in test ([9ceca65](https://github.com/tstapler/docspan/commit/9ceca65ca5ad2040c1c2ec215fc097b02ba1a0c4))
|
|
94
|
+
* **ci:** apply ruff autofix across all src and test files ([bee2784](https://github.com/tstapler/docspan/commit/bee2784b940d872045ce33faf0ce53f65150d80d))
|
|
95
|
+
* **ci:** resolve ruff lint failures and enable Actions PR creation ([8727e7b](https://github.com/tstapler/docspan/commit/8727e7bcabf1ddec4b4116b06e605ff24d0eaffe))
|
|
96
|
+
* **google-docs:** don't drop blockquote paragraphs on push ([#9](https://github.com/tstapler/docspan/issues/9)) ([e3b2597](https://github.com/tstapler/docspan/commit/e3b259799acfcdaa7f884edb49633f349860a978))
|
|
97
|
+
* **google-docs:** fix inline-style paragraph misalignment on push ([#10](https://github.com/tstapler/docspan/issues/10)) ([4f79ef8](https://github.com/tstapler/docspan/commit/4f79ef8b4669eee4d6fe361af5bae68bb4486019))
|
|
98
|
+
* **google-docs:** fix mid-document insert off-by-one causing paragraph merges ([#12](https://github.com/tstapler/docspan/issues/12)) ([c74bea2](https://github.com/tstapler/docspan/commit/c74bea2d6464df0c19abce31a39aea0bc18d1e46))
|
|
99
|
+
* **google-docs:** restore inline styling and unwrap redirect links on pull ([#11](https://github.com/tstapler/docspan/issues/11)) ([b90466c](https://github.com/tstapler/docspan/commit/b90466cd9765a290b02634bbe9b3869185e308bc))
|
|
100
|
+
* Improve nested list indentation in Google Docs to Markdown conversion ([d6a7539](https://github.com/tstapler/docspan/commit/d6a7539d4beade3426e5d0db838f1d9974f7294b))
|
|
101
|
+
* Remove CONFIG_YAML dependency, prefer individual env vars ([d5d5d4a](https://github.com/tstapler/docspan/commit/d5d5d4ae25a7c1d7a213071f968eba56a9289da3))
|
|
102
|
+
* Resolve service account storage quota error by storing sync state locally ([00e9cb6](https://github.com/tstapler/docspan/commit/00e9cb65033dfb6cca8e0aae2258cde458cfb342))
|
|
103
|
+
|
|
104
|
+
## [Unreleased]
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
- **google-docs:** internal markdown anchors (`[A1](#a1-current-state)`) now resolve to
|
|
108
|
+
Google Docs heading links instead of being written as a `#fragment` URL the Doc cannot
|
|
109
|
+
follow. Slugs follow `github-slugger`, checked against vectors generated from the real
|
|
110
|
+
implementation. `TITLE`/`SUBTITLE` paragraphs count as anchor targets, and both the
|
|
111
|
+
modern `Link.heading` and the legacy `Link.headingId` union members are read, so an
|
|
112
|
+
anchor survives a pull whether or not the fetch used `includeTabsContent`.
|
|
113
|
+
- **google-docs:** an anchor that names no heading is written as plain text with no link
|
|
114
|
+
and reported — `docspan push --dry-run` lists it, `docspan push` exits non-zero with a
|
|
115
|
+
warning naming the anchor and the heading anchors that *are* available. It is never
|
|
116
|
+
written as a link a reader can click and land nowhere, and never reported as a clean ✓.
|
|
117
|
+
- **google-docs:** both pull paths now emit the heading's slug. A default (no `tab_id`) pull
|
|
118
|
+
goes through Drive's HTML export, which carries the Doc's opaque `#h.abc123` through
|
|
119
|
+
verbatim; it is upgraded to the slug, so the pulled markdown works as markdown.
|
|
120
|
+
|
|
121
|
+
### Changed
|
|
122
|
+
- **google-docs:** pass 2 parses and aligns the document once per push instead of three
|
|
123
|
+
times. The discarded work sat inside the window between pass 2's read and its write, where
|
|
124
|
+
a concurrent edit costs a conflict on a document pass 1 has already changed.
|
|
125
|
+
|
|
126
|
+
### Known limitations
|
|
127
|
+
Each of these is tracked as a follow-up rather than half-addressed here.
|
|
128
|
+
- An anchor into a heading in a *different tab* of the same document cannot be resolved and
|
|
129
|
+
is reported unresolved. The flat `headingId` member resolves against the tab named in the
|
|
130
|
+
request, so expressing one needs the tabs-aware `Link.heading` member.
|
|
131
|
+
- A pull cannot express a `bookmark`/`bookmarkId` link, a link to a tab, or any link inside
|
|
132
|
+
a table cell, so those are dropped from the pulled file without a report.
|
|
133
|
+
- Confluence writes an internal anchor as a literal `#fragment` href, which it does not
|
|
134
|
+
resolve.
|
|
135
|
+
- An anchor that resolves to nothing is written as plain text, so a later pull replaces the
|
|
136
|
+
author's `[text](#anchor)` with `text`. The push reports it; nothing does afterwards.
|
|
137
|
+
- Such a push exits non-zero on every run, with no flag to suppress it.
|
|
138
|
+
- A heading containing an HTML entity reference (`## Team & process`) or inline HTML
|
|
139
|
+
(`## <code>push()</code> …`) is slugged from the markdown *source* rather than the rendered
|
|
140
|
+
text, so its slug differs from GitHub's. Because duplicate numbering depends on the
|
|
141
|
+
headings before it, that can land an anchor on a neighbouring heading. Pre-existing; a fix
|
|
142
|
+
attempt was reverted on this branch because it needs the slug text and the
|
|
143
|
+
document text separated, which is its own change.
|
|
144
|
+
|
|
145
|
+
## [0.1.0] - 2026-06-07
|
|
146
|
+
|
|
147
|
+
### Added
|
|
148
|
+
- `docspan push` — push local markdown files to Google Docs or Confluence
|
|
149
|
+
- `docspan pull` — pull remote documents into local markdown files with three-way merge
|
|
150
|
+
- `docspan status` — show current mapping status in a table
|
|
151
|
+
- `docspan auth setup` — interactive authentication setup for `google_docs` and `confluence` backends
|
|
152
|
+
- `docspan conflicts list` — list files with unresolved merge conflicts
|
|
153
|
+
- `docspan conflicts resolve` — resolve merge conflicts with `remote`, `local`, or `merged` strategy
|
|
154
|
+
- Google Docs backend: push and pull via Google Docs API (service account auth)
|
|
155
|
+
- Confluence backend: push and pull via Atlassian REST API (API token auth)
|
|
156
|
+
- Three-way merge for bidirectional sync conflict detection
|
|
157
|
+
- Confluence comment sidecar: pull writes inline and footer comments to `{file}.comments.md`
|
|
158
|
+
- `markgate.yaml` config file format with per-mapping direction control (`push`/`pull`/`both`)
|
|
159
|
+
- Sync state tracking via `.markgate-state.json` and content-addressed base store in `.markgate-base/`
|
|
160
|
+
|
|
161
|
+
### Known Limitations
|
|
162
|
+
- Google Docs: comments on edited paragraphs are destroyed on push (paragraph-level diff; comments on unchanged paragraphs are preserved)
|
|
163
|
+
- Push: no image support — local image files cannot be pushed to Google Docs or Confluence
|
|
164
|
+
- Push: no table support — markdown tables are not rendered in Google Docs
|
|
165
|
+
- Confluence: requires an Atlassian API token; no OAuth flow
|
|
166
|
+
- Confluence: comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
|
|
167
|
+
- Config file is named `markgate.yaml` (not `docspan.yaml`) and state file is `.markgate-state.json` (not `.docspan-state.json`). These will be renamed in v0.2.0.
|
|
168
|
+
|
|
169
|
+
[Unreleased]: https://github.com/tstapler/docspan/compare/v0.1.0...HEAD
|
|
170
|
+
[0.1.0]: https://github.com/tstapler/docspan/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: docspan
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Push and pull markdown to Google Docs and Confluence from a single CLI
|
|
5
5
|
Project-URL: Homepage, https://github.com/tstapler/docspan
|
|
6
6
|
Project-URL: Repository, https://github.com/tstapler/docspan
|
|
@@ -18,6 +18,8 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
23
|
Classifier: Topic :: Software Development :: Documentation
|
|
22
24
|
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
23
25
|
Requires-Python: >=3.9
|
|
@@ -34,6 +36,7 @@ Requires-Dist: python-dateutil>=2.8.2
|
|
|
34
36
|
Requires-Dist: pyyaml>=6.0
|
|
35
37
|
Requires-Dist: requests>=2.25.0
|
|
36
38
|
Requires-Dist: rich>=13.0.0
|
|
39
|
+
Requires-Dist: ruamel-yaml>=0.18.0
|
|
37
40
|
Requires-Dist: typer>=0.9.0
|
|
38
41
|
Provides-Extra: dev
|
|
39
42
|
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
@@ -282,6 +285,7 @@ Auth resolution order: `credentials_path` → `ACCOUNT_A_CREDENTIALS[_PATH]` env
|
|
|
282
285
|
| `backend` | string | — | yes | `"google_docs"` or `"confluence"` |
|
|
283
286
|
| `remote_id` | string | — | yes | Google Doc ID or Confluence page ID |
|
|
284
287
|
| `direction` | enum | `"both"` | no | `"push"`, `"pull"`, or `"both"` |
|
|
288
|
+
| `tab_id` | string | `null` | no | Google Docs tab id (e.g. `"t.moqlkhpwn82e"`) to target on a multi-tab doc. Ignored by the Confluence backend. If unset and the doc has more than one tab, push/pull still succeed but report a `"warning"` naming the tabs found, instead of silently syncing whichever tab is first. |
|
|
285
289
|
|
|
286
290
|
---
|
|
287
291
|
|
|
@@ -303,14 +307,17 @@ docspan generates these files in your project directory after first sync:
|
|
|
303
307
|
> [!NOTE]
|
|
304
308
|
> **Known limitations in v0.1.0**
|
|
305
309
|
>
|
|
306
|
-
> - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved). `docspan push --dry-run` and a default fail-closed `--force`-gated block now warn before this happens — it is still not prevented.
|
|
307
|
-
> -
|
|
310
|
+
> - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved). `docspan push --dry-run` and a default fail-closed `--force`-gated block now warn before this happens, naming every at-risk comment (id, author, quoted snippet) per flagged paragraph — it is still not prevented. Re-anchoring or recreating the comment was investigated and rejected: neither `comments().update` nor `comments().create` can produce a comment Google Docs' own UI renders as anchored to arbitrary text (see [ADR-003](project_plans/wedding-planning-workflow/decisions/ADR-003-no-comment-anchor-migration.md)), so no migration ships.
|
|
311
|
+
> - Google Docs: images push and pull (`` uploads to Drive; `https://` URLs are referenced directly). SVG, missing, and oversized (>50MB) images are reported as warnings rather than blocking the push. An image mixed into a paragraph alongside running text is not supported — only a standalone `` on its own line
|
|
312
|
+
> - Google Docs: a pulled image's markdown link is Google's `contentUri` for that embedded object, which Google's API docs say may change over time even when the image itself hasn't. docspan's push diff keys image identity on `alt`/size rather than this URI, so a rotated URI alone will not cause a paragraph to be needlessly deleted and reinserted (and its comments lost) on the next push — but the URI written into your markdown file can itself go stale between pulls, and a stale-but-unchanged URI line can still show up as a one-sided edit in `docspan conflicts resolve`'s three-way diff even though nothing meaningful changed
|
|
313
|
+
> - Push: no image support for Confluence — local images cannot be pushed
|
|
308
314
|
> - Push: no table support — markdown tables are not rendered in Google Docs
|
|
309
315
|
> - Confluence: requires an Atlassian API token; no OAuth flow
|
|
310
316
|
> - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
|
|
311
317
|
> - Checklist state (`- [ ]`/`- [x]`) round-trips as literal text — Google Docs' native checkbox glyph is intentionally not used because its checked/unchecked state cannot be read back via the API (see ADR-001)
|
|
312
318
|
> - `push --dry-run` now shows a real structural diff and flags paragraphs with open comments at risk; `push` blocks by default on a flagged paragraph unless `--force` is passed
|
|
313
319
|
> - If a push succeeds but a post-push check finds the open-comment count dropped, docspan reports this as a `⚠` warning — never a plain green success — so it's never mistaken for a clean push
|
|
320
|
+
> - Google Docs: multi-tab docs need `tab_id` set explicitly per mapping — find it in the doc's URL (`...#tab=t.XXXXXXXXXX` after clicking the tab) or from the warning message docspan prints when `tab_id` is left unset. Without it, pull/push default to the doc's first tab (`pull`'s default path additionally can't target a tab at all — it uses Drive's HTML export, which only ever returns the first tab; set `tab_id` to instead pull via the structural API, which can target any tab)
|
|
314
321
|
> - Google Docs OAuth requires each user to create their own GCP project (`docspan auth setup google_docs` → Personal/OAuth) and stays in Google's "Testing" publishing status — capped at 100 test users, with Google's "app isn't verified" warning shown on first sign-in. This avoids the annual CASA security assessment required to verify apps requesting Drive/Docs' restricted read-write scopes (a real recurring cost), at the price of a few extra manual setup minutes per user instead of a single embedded, zero-config client. Revisit if/when adoption outgrows a per-user-project model — options are paying for verification, or narrowing to the unrestricted `drive.file` scope via Google's Picker API (bigger rework: requires the user to explicitly select their doc through a picker rather than referencing it by ID in config)
|
|
315
322
|
|
|
316
323
|
---
|
|
@@ -233,6 +233,7 @@ Auth resolution order: `credentials_path` → `ACCOUNT_A_CREDENTIALS[_PATH]` env
|
|
|
233
233
|
| `backend` | string | — | yes | `"google_docs"` or `"confluence"` |
|
|
234
234
|
| `remote_id` | string | — | yes | Google Doc ID or Confluence page ID |
|
|
235
235
|
| `direction` | enum | `"both"` | no | `"push"`, `"pull"`, or `"both"` |
|
|
236
|
+
| `tab_id` | string | `null` | no | Google Docs tab id (e.g. `"t.moqlkhpwn82e"`) to target on a multi-tab doc. Ignored by the Confluence backend. If unset and the doc has more than one tab, push/pull still succeed but report a `"warning"` naming the tabs found, instead of silently syncing whichever tab is first. |
|
|
236
237
|
|
|
237
238
|
---
|
|
238
239
|
|
|
@@ -254,14 +255,17 @@ docspan generates these files in your project directory after first sync:
|
|
|
254
255
|
> [!NOTE]
|
|
255
256
|
> **Known limitations in v0.1.0**
|
|
256
257
|
>
|
|
257
|
-
> - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved). `docspan push --dry-run` and a default fail-closed `--force`-gated block now warn before this happens — it is still not prevented.
|
|
258
|
-
> -
|
|
258
|
+
> - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved). `docspan push --dry-run` and a default fail-closed `--force`-gated block now warn before this happens, naming every at-risk comment (id, author, quoted snippet) per flagged paragraph — it is still not prevented. Re-anchoring or recreating the comment was investigated and rejected: neither `comments().update` nor `comments().create` can produce a comment Google Docs' own UI renders as anchored to arbitrary text (see [ADR-003](project_plans/wedding-planning-workflow/decisions/ADR-003-no-comment-anchor-migration.md)), so no migration ships.
|
|
259
|
+
> - Google Docs: images push and pull (`` uploads to Drive; `https://` URLs are referenced directly). SVG, missing, and oversized (>50MB) images are reported as warnings rather than blocking the push. An image mixed into a paragraph alongside running text is not supported — only a standalone `` on its own line
|
|
260
|
+
> - Google Docs: a pulled image's markdown link is Google's `contentUri` for that embedded object, which Google's API docs say may change over time even when the image itself hasn't. docspan's push diff keys image identity on `alt`/size rather than this URI, so a rotated URI alone will not cause a paragraph to be needlessly deleted and reinserted (and its comments lost) on the next push — but the URI written into your markdown file can itself go stale between pulls, and a stale-but-unchanged URI line can still show up as a one-sided edit in `docspan conflicts resolve`'s three-way diff even though nothing meaningful changed
|
|
261
|
+
> - Push: no image support for Confluence — local images cannot be pushed
|
|
259
262
|
> - Push: no table support — markdown tables are not rendered in Google Docs
|
|
260
263
|
> - Confluence: requires an Atlassian API token; no OAuth flow
|
|
261
264
|
> - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
|
|
262
265
|
> - Checklist state (`- [ ]`/`- [x]`) round-trips as literal text — Google Docs' native checkbox glyph is intentionally not used because its checked/unchecked state cannot be read back via the API (see ADR-001)
|
|
263
266
|
> - `push --dry-run` now shows a real structural diff and flags paragraphs with open comments at risk; `push` blocks by default on a flagged paragraph unless `--force` is passed
|
|
264
267
|
> - If a push succeeds but a post-push check finds the open-comment count dropped, docspan reports this as a `⚠` warning — never a plain green success — so it's never mistaken for a clean push
|
|
268
|
+
> - Google Docs: multi-tab docs need `tab_id` set explicitly per mapping — find it in the doc's URL (`...#tab=t.XXXXXXXXXX` after clicking the tab) or from the warning message docspan prints when `tab_id` is left unset. Without it, pull/push default to the doc's first tab (`pull`'s default path additionally can't target a tab at all — it uses Drive's HTML export, which only ever returns the first tab; set `tab_id` to instead pull via the structural API, which can target any tab)
|
|
265
269
|
> - Google Docs OAuth requires each user to create their own GCP project (`docspan auth setup google_docs` → Personal/OAuth) and stays in Google's "Testing" publishing status — capped at 100 test users, with Google's "app isn't verified" warning shown on first sign-in. This avoids the annual CASA security assessment required to verify apps requesting Drive/Docs' restricted read-write scopes (a real recurring cost), at the price of a few extra manual setup minutes per user instead of a single embedded, zero-config client. Revisit if/when adoption outgrows a per-user-project model — options are paying for verification, or narrowing to the unrestricted `drive.file` scope via Google's Picker API (bigger rework: requires the user to explicitly select their doc through a picker rather than referencing it by ID in config)
|
|
266
270
|
|
|
267
271
|
---
|
|
@@ -65,3 +65,4 @@ export CONFLUENCE_API_TOKEN=your-token
|
|
|
65
65
|
- **Comment sidecar is informational only**: Comments pulled from Confluence are written to `{file}.comments.md` but cannot be pushed back via docspan.
|
|
66
66
|
- **Push replaces page content**: The full page body is replaced on every push. Inline comment positions in Confluence may shift after a push.
|
|
67
67
|
- **Complex macros not preserved faithfully**: Confluence macros (status, panels, expand, etc.) are converted to approximate markdown equivalents on pull and may not round-trip cleanly on push.
|
|
68
|
+
- **Mermaid diagrams are not rendered**: a ` ```mermaid ` fence is pushed as a plain ADF code block (language `mermaid`), i.e. the raw diagram source as visible text — not an image, and not Confluence's native mermaid macro. `render_mermaid_diagrams` in `PublishConfig` currently has no effect on this: the code path that would rasterize a diagram (`MermaidParser.render_diagram()` in `markdown/extensions/mermaid.py`) is a stub that never runs — it's dead code, unreachable from the real parse pipeline (`markdown/parser.py` builds `MermaidNode` directly). Fixing this requires a real renderer (e.g. `mermaid-cli` or a hosted rendering service) wired to upload the result as a Confluence attachment before ADF conversion; tracked as follow-up work, not yet implemented.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Google Docs Backend
|
|
2
|
+
|
|
3
|
+
## How it works
|
|
4
|
+
|
|
5
|
+
The Google Docs backend authenticates either via a Google service account JSON key or via per-user OAuth (an `InstalledAppFlow` that acts as you, similar to `gws`) — whichever `markgate.yaml` configures (`credentials_path` for the service account, `oauth_client_secret_path` for OAuth). Push uses a paragraph-level structural diff that computes the minimal set of `batchUpdate` requests needed to transform the current document into the target content. This approach preserves comments attached to paragraphs that have not changed. Pull exports the Google Doc as HTML and converts it to markdown.
|
|
6
|
+
|
|
7
|
+
## Auth Setup
|
|
8
|
+
|
|
9
|
+
Run `docspan auth setup google_docs` to see setup instructions.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Google Docs Auth Setup
|
|
13
|
+
========================================
|
|
14
|
+
Run this in an interactive terminal for a guided setup, or configure manually:
|
|
15
|
+
|
|
16
|
+
Per-user OAuth (recommended — acts as you, like gws):
|
|
17
|
+
1. Create an OAuth client (Desktop app); download client_secret.json
|
|
18
|
+
2. docspan auth setup google_docs --oauth --client-secret /path/to/client_secret.json
|
|
19
|
+
(or set backends.google_docs.oauth_client_secret_path in markgate.yaml)
|
|
20
|
+
|
|
21
|
+
Service account (automation):
|
|
22
|
+
1. Create a service account + JSON key; enable the Docs & Drive APIs
|
|
23
|
+
2. Share your docs with the service-account email
|
|
24
|
+
3. Set credentials_path in markgate.yaml (or ACCOUNT_A_CREDENTIALS_PATH env)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Service account credentials can also be provided inline via `ACCOUNT_A_CREDENTIALS` (the JSON itself, not a path) instead of `ACCOUNT_A_CREDENTIALS_PATH`.
|
|
28
|
+
|
|
29
|
+
## Required Scopes
|
|
30
|
+
|
|
31
|
+
Every credential path (`GoogleAuthenticator`, `OAuthAuthenticator`) requests the same read/write scopes (`PUSH_SCOPES`, aliased as `SCOPES`/`DEFAULT_SCOPES`), whether the operation is push or pull:
|
|
32
|
+
|
|
33
|
+
- `https://www.googleapis.com/auth/documents` — read and write Google Docs
|
|
34
|
+
- `https://www.googleapis.com/auth/drive` — read/write Drive (comment reads/writes, file metadata; not just export)
|
|
35
|
+
- `https://www.googleapis.com/auth/spreadsheets.readonly` — read Sheets embedded/linked in a Doc
|
|
36
|
+
|
|
37
|
+
`auth.py` also defines a narrower read-only `PULL_SCOPES`, but nothing in the codebase wires it up today — pull requests the same full grant as push, not a readonly subset. Comment reads/writes reuse this same grant too; no separate scope is added for them.
|
|
38
|
+
|
|
39
|
+
## `markgate.yaml` Example
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
backends:
|
|
43
|
+
google_docs:
|
|
44
|
+
credentials_path: /path/to/service-account.json
|
|
45
|
+
# token_path: .markgate/google_token.json # default, rarely changed
|
|
46
|
+
|
|
47
|
+
mappings:
|
|
48
|
+
- local: docs/design-doc.md
|
|
49
|
+
backend: google_docs
|
|
50
|
+
remote_id: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74O
|
|
51
|
+
direction: both
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Limitations
|
|
55
|
+
|
|
56
|
+
!!! warning
|
|
57
|
+
- **Comments destroyed on push for edited paragraphs**: The structural diff preserves comments on unchanged paragraphs, but any paragraph that is deleted and reinserted loses its comments. This is a known v0.1.0 limitation.
|
|
58
|
+
- **Images push and pull**: `` uploads the local file to Drive and references it by URI; an `https://` URL is referenced directly, bypassing upload. Only a standalone image on its own line is supported — one mixed into a paragraph alongside running text is left as plain text. Missing files, files over 50MB, and unsupported formats (SVG) are reported as push warnings rather than blocking the write or crashing.
|
|
59
|
+
- **Mermaid diagrams push as rendered PNGs**: a fenced ` ```mermaid ` block is rendered to a raster PNG (via `mermaid-cli`/`mmdc`, shelled out to — install with `npm install -g @mermaid-js/mermaid-cli`, or it's fetched on demand through `npx`) and pushed as an inline image, since `insertInlineImage` has no native mermaid or SVG support. A render failure (missing Node.js/mermaid-cli, invalid diagram syntax, timeout) is reported as a push warning, not a crash. There is no pull-side reconstruction — a mermaid diagram round-trips back to markdown as a plain image reference, not a ` ```mermaid ` fence.
|
|
60
|
+
- **Pulled image URIs can go stale**: a pulled `` link is Google's `contentUri` for that embedded object, which Google's API docs say may change over time even when the image itself is unchanged. The push structural diff keys image identity on `alt`/width/height, not this URI, so a rotated `contentUri` alone will not cause the paragraph to be deleted and reinserted (which would destroy any comment anchored to it) — but the stale URI persisted in your markdown file can still surface as a one-sided edit in `docspan conflicts resolve`'s three-way diff.
|
|
61
|
+
- **Table cells hold one paragraph**: a markdown table cell is pushed as a single
|
|
62
|
+
paragraph, and inline formatting inside it (bold, monospace, links, internal
|
|
63
|
+
`#anchor` references) is applied on the second pass. Two limits follow: a cell
|
|
64
|
+
whose content spans more than one paragraph in the Doc cannot be styled, and a
|
|
65
|
+
table created by the current push gets its cell styling on the *next* push —
|
|
66
|
+
docspan reports both rather than failing silently.
|
|
67
|
+
- **Rate limiting**: The Google Docs API allows 300 requests per minute per project. Large documents with many changed paragraphs may trigger rate limit errors.
|
|
@@ -57,7 +57,9 @@ See the [Install](install.md) page for full auth setup instructions and the [Com
|
|
|
57
57
|
|
|
58
58
|
!!! warning "Known limitations in v0.1.0"
|
|
59
59
|
- Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved)
|
|
60
|
-
-
|
|
60
|
+
- Google Docs: images push and pull (local files upload to Drive; `https://` URLs are referenced directly)
|
|
61
|
+
- Google Docs: a pulled image's markdown link is Google's `contentUri`, which can change over time even when the image hasn't — push doesn't misdetect this as a real change, but the stale URI in your local file can still show up as a one-sided edit during conflict resolution
|
|
62
|
+
- Push: no image support for Confluence — local images cannot be pushed
|
|
61
63
|
- Push: no table support — markdown tables are not rendered in Google Docs
|
|
62
64
|
- Confluence: requires an Atlassian API token; no OAuth flow
|
|
63
65
|
- Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
|
|
@@ -37,6 +37,10 @@ The Google Docs push path drops two things, both blocking real-world design-doc
|
|
|
37
37
|
|
|
38
38
|
### Tables (Part 2)
|
|
39
39
|
- New `DocsTableNode(rows: List[List[str]], start_index, end_index)` in the structure module.
|
|
40
|
+
- **Superseded.** `rows` is now `List[List[TableCell]]`, where `TableCell` carries
|
|
41
|
+
`text` *and* `spans`. Plain-text cells meant every mark inside one was dropped —
|
|
42
|
+
including internal `#anchor` cross-references, which rendered as dead text in the
|
|
43
|
+
Doc while the identical reference in a paragraph resolved. See #49.
|
|
40
44
|
- Parser emits `DocsTableNode` for `table` tokens (header row + body rows, cell = plain text).
|
|
41
45
|
- `DocsStructureParser` parses live `table` elements into `DocsTableNode` (rows from
|
|
42
46
|
`table.tableRows[].tableCells[].content` paragraphs), with real start/end indices.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
Research (`research/stack.md` §2, `research/build-vs-buy.md` §1c, cross-checked against multiple independent sources — Google's own REST reference, a tanaikech Apps Script gist, a Latenode community thread) confirms:
|
|
13
13
|
|
|
14
14
|
- The Docs API's `createParagraphBullets` request does support a distinct `bulletPreset: "BULLET_CHECKBOX"` value — checkboxes *can* be created programmatically.
|
|
15
|
-
-
|
|
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
|
|
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
|
|
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.
|