docspan 0.3.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/.release-please-manifest.json +3 -0
- {docspan-0.3.0 → docspan-0.4.0}/CHANGELOG.md +27 -0
- {docspan-0.3.0 → docspan-0.4.0}/PKG-INFO +6 -3
- {docspan-0.3.0 → docspan-0.4.0}/README.md +4 -2
- {docspan-0.3.0 → docspan-0.4.0}/docs/backends/confluence.md +1 -0
- docspan-0.4.0/docs/backends/google-docs.md +67 -0
- {docspan-0.3.0 → docspan-0.4.0}/docs/index.md +3 -1
- docspan-0.4.0/project_plans/wedding-planning-workflow/decisions/ADR-003-no-comment-anchor-migration.md +128 -0
- {docspan-0.3.0 → docspan-0.4.0}/pyproject.toml +1 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/base.py +17 -2
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/backend.py +21 -1
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/models.py +5 -1
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/backend.py +342 -53
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/client.py +72 -1
- {docspan-0.3.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.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/docs_request_builder.py +822 -264
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/docs_structure_parser.py +95 -3
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/heading_anchors.py +26 -11
- 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.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/nodes_to_markdown.py +236 -65
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/projection.py +2 -1
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/push_preview.py +138 -28
- docspan-0.4.0/src/docspan/backends/google_docs/registry.py +67 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/tabs.py +46 -1
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/cli/main.py +165 -2
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/config.py +95 -1
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/core/orchestrator.py +23 -1
- docspan-0.4.0/tests/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_cli.py +216 -2
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_code_block_granularity.py +255 -22
- {docspan-0.3.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.3.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.3.0 → docspan-0.4.0}/tests/test_docs_request_builder.py +556 -5
- docspan-0.4.0/tests/test_gdocs_images.py +427 -0
- docspan-0.4.0/tests/test_gdocs_mermaid.py +119 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_gdocs_push_pipeline.py +17 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_gdocs_tables_and_styles.py +70 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_google_docs_backend.py +862 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_google_onboarding.py +8 -2
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_heading_anchors.py +166 -0
- docspan-0.4.0/tests/test_heading_identity.py +982 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_markdown_to_paragraph_parser.py +8 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_nodes_to_markdown.py +112 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_orchestrator.py +4 -1
- 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.3.0 → docspan-0.4.0}/tests/test_table_cell_spans.py +153 -22
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_tabs.py +130 -0
- {docspan-0.3.0 → docspan-0.4.0}/uv.lock +85 -74
- docspan-0.3.0/.release-please-manifest.json +0 -3
- docspan-0.3.0/docs/backends/google-docs.md +0 -67
- docspan-0.3.0/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +0 -357
- docspan-0.3.0/tests/test_heading_identity.py +0 -456
- docspan-0.3.0/tests/test_push_preview.py +0 -338
- {docspan-0.3.0 → docspan-0.4.0}/.github/workflows/ci.yml +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/.github/workflows/publish.yml +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/.github/workflows/release-please.yml +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/.gitignore +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/.mypy-error-baseline +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/CONTRIBUTING.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/Procfile +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/RAILWAY_SETUP.md +0 -0
- /docspan-0.3.0/src/docspan/__main__.py → /docspan-0.4.0/doc.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/docs/commands.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/docs/configuration.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/docs/contributing.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/docs/install.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/docspan.yaml.example +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/markgate.yaml.example +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/mkdocs.yml +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/modules/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/modules/auth.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/modules/conflict_handler.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/modules/converter.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/modules/gdrive_client.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/modules/sync_engine.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/bidirectional-comments/plan.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/adversarial-review.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/plan.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/release-checklist.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/implementation/validation.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/requirements.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/research/architecture.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/research/features.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/research/google-docs-push.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/research/pitfalls.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/docspan-release/research/stack.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/gdocs-tables-inline-styles/plan.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/decisions/ADR-001-merge3-dependency.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/decisions/ADR-002-base-content-sidecar-store.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/implementation/adversarial-review.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/implementation/plan.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/implementation/validation.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/requirements.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/research/architecture.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/research/features.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/research/pitfalls.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/markgate-sync/research/stack.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/decisions/ADR-001-checklist-state-as-literal-text.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/decisions/ADR-002-comment-risk-flagging-not-anchor-preservation.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/feature-gap-report.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/adversarial-review.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/architecture-review.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/plan.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/pre-mortem.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/implementation/validation.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/requirements.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/architecture.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/build-vs-buy.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/features.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/pitfalls.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/stack.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/research/ux.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/project_plans/wedding-planning-workflow/workflow-runbook.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/release-please-config.json +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/requirements.txt +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/runtime.txt +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/__init__.py +0 -0
- /docspan-0.3.0/src/docspan/backends/confluence/__init__.py → /docspan-0.4.0/src/docspan/__main__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/__init__.py +0 -0
- {docspan-0.3.0/src/docspan/backends/confluence/services → docspan-0.4.0/src/docspan/backends/confluence}/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/comparator.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/converter.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/converters.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/interfaces.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/nodes.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/parser.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/validators.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/adf/visitors.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/client.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/loader.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/config/validation.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/ast.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/frontmatter.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/mermaid.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/extensions/wikilinks.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/inline_parser.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/markdown/parser.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/markdown_file.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/page.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/path_utils.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/results.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/models/sync_status.py +0 -0
- {docspan-0.3.0/src/docspan/backends/google_docs → docspan-0.4.0/src/docspan/backends/confluence/services}/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/attachment_client.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/base_client.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/client.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/comment_client.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/crawler.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/label_client.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/page_client.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/space_client.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/confluence/services/confluence/url_parser.py +0 -0
- {docspan-0.3.0/src/docspan/cli → docspan-0.4.0/src/docspan/backends/google_docs}/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/auth.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/checkbox_state.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/comments.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/backends/google_docs/onboarding.py +0 -0
- {docspan-0.3.0/tests → docspan-0.4.0/src/docspan/cli}/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/core/__init__.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/core/merge.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/core/paths.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/core/state.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/src/docspan/core/xdg.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/sync.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/terraform/gcp/README.md +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/terraform/gcp/main.tf +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/terraform/gcp/outputs.tf +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/terraform/gcp/variables.tf +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/terraform/main.tf +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/terraform/variables.tf +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/conftest.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/fixtures/github_slugger_vectors.json +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_checkbox_state.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_conflict_resolution.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_docs_structure_parser.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_google_comments.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_google_oauth.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_merge.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_span_trailing_newline.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_state.py +0 -0
- {docspan-0.3.0 → docspan-0.4.0}/tests/test_xdg_central_config.py +0 -0
|
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
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
|
+
|
|
8
35
|
## [0.3.0](https://github.com/tstapler/docspan/compare/docspan-v0.2.0...docspan-v0.3.0) (2026-08-11)
|
|
9
36
|
|
|
10
37
|
|
|
@@ -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
|
|
@@ -36,6 +36,7 @@ Requires-Dist: python-dateutil>=2.8.2
|
|
|
36
36
|
Requires-Dist: pyyaml>=6.0
|
|
37
37
|
Requires-Dist: requests>=2.25.0
|
|
38
38
|
Requires-Dist: rich>=13.0.0
|
|
39
|
+
Requires-Dist: ruamel-yaml>=0.18.0
|
|
39
40
|
Requires-Dist: typer>=0.9.0
|
|
40
41
|
Provides-Extra: dev
|
|
41
42
|
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
@@ -306,8 +307,10 @@ docspan generates these files in your project directory after first sync:
|
|
|
306
307
|
> [!NOTE]
|
|
307
308
|
> **Known limitations in v0.1.0**
|
|
308
309
|
>
|
|
309
|
-
> - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved). `docspan push --dry-run` and a default fail-closed `--force`-gated block now warn before this happens — it is still not prevented.
|
|
310
|
-
> -
|
|
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
|
|
311
314
|
> - Push: no table support — markdown tables are not rendered in Google Docs
|
|
312
315
|
> - Confluence: requires an Atlassian API token; no OAuth flow
|
|
313
316
|
> - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
|
|
@@ -255,8 +255,10 @@ docspan generates these files in your project directory after first sync:
|
|
|
255
255
|
> [!NOTE]
|
|
256
256
|
> **Known limitations in v0.1.0**
|
|
257
257
|
>
|
|
258
|
-
> - Google Docs: comments on edited paragraphs are lost on push (paragraph-level structural diff; comments on unchanged paragraphs are preserved). `docspan push --dry-run` and a default fail-closed `--force`-gated block now warn before this happens — it is still not prevented.
|
|
259
|
-
> -
|
|
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
|
|
260
262
|
> - Push: no table support — markdown tables are not rendered in Google Docs
|
|
261
263
|
> - Confluence: requires an Atlassian API token; no OAuth flow
|
|
262
264
|
> - Confluence: the comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
|
|
@@ -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
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# ADR-003: Do not implement comment re-anchoring/migration on push — richer warning only
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-08-12
|
|
4
|
+
**Status**: Accepted
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
`docspan` issue #92 asks whether a comment anchored to a paragraph that push
|
|
9
|
+
resolves to delete+insert (per `_repair`'s docstring in
|
|
10
|
+
`docs_request_builder.py`: any diff opcode that doesn't collapse to `"equal"`
|
|
11
|
+
becomes a literal `deleteContentRange` + `insertText`) can be preserved
|
|
12
|
+
across that push, either by mutating the
|
|
13
|
+
Drive comment's existing `anchor`/`quotedFileContent` via `comments().update`,
|
|
14
|
+
or by creating a replacement comment via `comments().create` with a custom
|
|
15
|
+
`anchor`/`quotedFileContent` pointing at the reinserted paragraph.
|
|
16
|
+
|
|
17
|
+
A live-API spike against a real Drive document/comment was the intended way
|
|
18
|
+
to answer this, but no OAuth credentials or test Drive account are available
|
|
19
|
+
in this sandboxed environment, so no live `comments().update`/`comments().create`
|
|
20
|
+
call was made. This is stated plainly, not glossed over: the finding below
|
|
21
|
+
rests on documented API contract and this repo's own prior investigation,
|
|
22
|
+
not a fresh live call. **This is a hard environmental blocker, not a gap in
|
|
23
|
+
effort** — closing it for real requires a human to supply live OAuth
|
|
24
|
+
credentials plus a scratch Drive document/comment outside this sandbox and
|
|
25
|
+
run the two calls below; no amount of further reasoning in this environment
|
|
26
|
+
can substitute for that. If/when that access exists, the spike is exactly
|
|
27
|
+
two calls:
|
|
28
|
+
```
|
|
29
|
+
comments().update(fileId=doc_id, commentId=c.id, fields="anchor", body={"anchor": target_anchor})
|
|
30
|
+
comments().create(fileId=doc_id, body={"content": c.content, "anchor": target_anchor, "quotedFileContent": {"value": snippet}})
|
|
31
|
+
```
|
|
32
|
+
followed by reloading the comment in the Docs UI to see whether it renders anchored.
|
|
33
|
+
|
|
34
|
+
### Confidence labeling
|
|
35
|
+
|
|
36
|
+
- **UNVERIFIED (this session)**: whether a live `comments().update`/`comments().create`
|
|
37
|
+
call against a real document actually behaves as documented — no such call was made here.
|
|
38
|
+
- **VERIFIED (secondary source, source 1 below)**: Google's own API reference and guide
|
|
39
|
+
text, read directly from developers.google.com, states anchors are immutable and that
|
|
40
|
+
`update`'s only documented-writable field is `content`.
|
|
41
|
+
- **VERIFIED (this repo's own prior work, source 2 below)**: `project_plans/bidirectional-comments/plan.md`
|
|
42
|
+
§2 already reached and recorded the same conclusion, citing a third-party reproduction
|
|
43
|
+
(`googleworkspace/cli#169`) as its own evidence, independent of this ADR.
|
|
44
|
+
|
|
45
|
+
Two sources answer the question decisively without a live call:
|
|
46
|
+
|
|
47
|
+
1. **Google's own guide**, [Manage comments and replies](https://developers.google.com/workspace/drive/api/guides/manage-comments):
|
|
48
|
+
anchors are documented as immutable ("Anchors are immutable, and their
|
|
49
|
+
position relative to the content of a document cannot be guaranteed
|
|
50
|
+
between revisions"), and — separately — Google Workspace editor apps
|
|
51
|
+
(Docs included) render an API-set `anchor` as an **unanchored** comment in
|
|
52
|
+
the UI, regardless of whether the anchor value itself is well-formed.
|
|
53
|
+
2. **This repo's own prior research**, `project_plans/bidirectional-comments/plan.md`
|
|
54
|
+
§2 (dated before this ADR, marked "High confidence — documented +
|
|
55
|
+
reproduced"): "`anchor` is ignored on Google Docs — a new anchored
|
|
56
|
+
comment renders as 'Original content deleted' / no highlight," citing
|
|
57
|
+
Google's guide plus a reproduced case at
|
|
58
|
+
[googleworkspace/cli#169](https://github.com/googleworkspace/cli/issues/169).
|
|
59
|
+
That plan already reached the same conclusion for *new* top-level
|
|
60
|
+
comments (ship unanchored-only, v1); this ADR extends the same finding
|
|
61
|
+
to the *migrate-an-existing-comment* case #92 asks about.
|
|
62
|
+
|
|
63
|
+
Both sources agree, from two different angles (official docs; independent
|
|
64
|
+
reproduction), that neither `comments().update` nor `comments().create` can
|
|
65
|
+
produce a comment that Google Docs' own UI will render as anchored to
|
|
66
|
+
arbitrary text. `comments().update`'s only documented writable field (per
|
|
67
|
+
its REST reference) is `content` — no update path for `anchor` is
|
|
68
|
+
documented at all.
|
|
69
|
+
|
|
70
|
+
Options considered:
|
|
71
|
+
|
|
72
|
+
1. **Re-anchor via `comments().update`.** Blocked: `anchor` isn't a
|
|
73
|
+
documented-writable field on `update`, and even if it were, "anchors are
|
|
74
|
+
immutable" per Google's own guide.
|
|
75
|
+
2. **Recreate via `comments().create` + resolve/delete the original.**
|
|
76
|
+
Technically possible to call, but the new comment renders **unanchored**
|
|
77
|
+
in the Docs UI (source 1 and 2 above) — a materially different, arguably
|
|
78
|
+
worse UX than today's silent loss: instead of nothing, the user gets a
|
|
79
|
+
stray document-level comment, under a new `id`/`createdTime`, that fires
|
|
80
|
+
a fresh "new comment" notification to every watcher, with no visual
|
|
81
|
+
anchor to the paragraph it's about.
|
|
82
|
+
3. **Warn-and-proceed only (status quo mechanism, richer message).** No new
|
|
83
|
+
Drive write capability, no new failure mode, no notification-spam
|
|
84
|
+
regression. Ship the AC2 enrichment (list every at-risk comment per
|
|
85
|
+
paragraph, not just the first) on top of this.
|
|
86
|
+
|
|
87
|
+
## Decision
|
|
88
|
+
|
|
89
|
+
Use **Option 3**. No comment migration ships. `find_high_risk_paragraphs`/
|
|
90
|
+
`render_high_risk` (`push_preview.py`) are extended to enumerate every
|
|
91
|
+
at-risk comment per flagged paragraph (id, author, snippet) instead of
|
|
92
|
+
stopping at the first match, but the underlying warn-before-`--force`
|
|
93
|
+
mechanism from ADR-002 is unchanged.
|
|
94
|
+
|
|
95
|
+
## Rationale
|
|
96
|
+
|
|
97
|
+
- Option 1 is foreclosed by the API contract itself, not by appetite or
|
|
98
|
+
risk tolerance — there's no live spike result that could reverse this;
|
|
99
|
+
the field is undocumented-as-writable and documented-as-immutable.
|
|
100
|
+
- Option 2 is technically shippable but trades a *quiet* known limitation
|
|
101
|
+
for a *loud* new one (unanchored stray comment + notification spam) —
|
|
102
|
+
worse for the exact "silent vs. loud" trade ADR-002 already reasoned
|
|
103
|
+
through, but on the wrong side of it: ADR-002 chose loud-and-blocking
|
|
104
|
+
over silent-and-undetected specifically because a blocked push costs the
|
|
105
|
+
user two minutes, which is a good trade; recreate-based migration would
|
|
106
|
+
cost watchers a false "new comment" notification and the user a
|
|
107
|
+
duplicate, unanchored comment, which is not a favorable trade for a
|
|
108
|
+
problem that's still only partially solved (the anchor is still gone).
|
|
109
|
+
- Option 3 keeps the fix surface exactly where ADR-002 already put it —
|
|
110
|
+
read-only risk detection — and directly satisfies #92's fallback
|
|
111
|
+
instruction: "If re-anchoring isn't feasible, improve the existing
|
|
112
|
+
warn-before-force message."
|
|
113
|
+
|
|
114
|
+
## Consequences
|
|
115
|
+
|
|
116
|
+
- AC5 ("if migration ships, a comment is preserved across push") is
|
|
117
|
+
satisfied by this ADR's explicit decision not to ship migration, per
|
|
118
|
+
#92's own conditional phrasing ("If migration ships...").
|
|
119
|
+
- No new Drive API write surface is added; `PUSH_SCOPES` is unchanged
|
|
120
|
+
(already covers today's `create_reply` calls).
|
|
121
|
+
- `push_preview.py`'s `HighRiskParagraph`/`find_high_risk_paragraphs`/
|
|
122
|
+
`render_high_risk` gain a richer, multi-comment rendering (AC2) without
|
|
123
|
+
changing which paragraphs get classified as high-risk (AC3) or how the
|
|
124
|
+
post-push open-comment-count backstop works (AC4).
|
|
125
|
+
- If Google ever documents a writable `anchor` field on `comments().update`,
|
|
126
|
+
or changes how Workspace editors render API-set anchors, this decision
|
|
127
|
+
should be revisited — it is contingent on the current, cited API
|
|
128
|
+
behavior, not a permanent architectural constraint.
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
|
-
from dataclasses import dataclass
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import Literal, Optional
|
|
7
|
+
from typing import List, Literal, Optional
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class SyncDirection(str, Enum):
|
|
@@ -29,6 +29,10 @@ class PushResult:
|
|
|
29
29
|
doc_id: str
|
|
30
30
|
message: Optional[str] = None
|
|
31
31
|
url: Optional[str] = None
|
|
32
|
+
# Drive file ids for images uploaded as part of this push but not yet
|
|
33
|
+
# deleted -- populated only when push failed after upload, so a retry
|
|
34
|
+
# can clean them up or reuse them instead of leaking orphaned files.
|
|
35
|
+
retryable_temp_drive_file_ids: List[str] = field(default_factory=list)
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
@dataclass
|
|
@@ -39,6 +43,13 @@ class PullResult:
|
|
|
39
43
|
message: Optional[str] = None
|
|
40
44
|
|
|
41
45
|
|
|
46
|
+
@dataclass
|
|
47
|
+
class CreateResult:
|
|
48
|
+
doc_id: str
|
|
49
|
+
title: str
|
|
50
|
+
url: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
|
|
42
53
|
class Backend(ABC):
|
|
43
54
|
"""
|
|
44
55
|
Base class for all docspan platform backends.
|
|
@@ -67,6 +78,10 @@ class Backend(ABC):
|
|
|
67
78
|
def pull(self, doc_id: str, local_path: str, **kwargs) -> PullResult:
|
|
68
79
|
"""Fetch the remote document and write it as local markdown."""
|
|
69
80
|
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def create(self, title: str, **kwargs: object) -> CreateResult:
|
|
83
|
+
"""Create a new remote document/page and return its id/title/url."""
|
|
84
|
+
|
|
70
85
|
@abstractmethod
|
|
71
86
|
def auth_setup(self, config_path: Optional[str] = None) -> None:
|
|
72
87
|
"""Interactive / instructional setup wizard for this backend.
|
|
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Optional
|
|
|
10
10
|
|
|
11
11
|
import markdownify as md_lib
|
|
12
12
|
|
|
13
|
-
from docspan.backends.base import Backend, PullResult, PushResult
|
|
13
|
+
from docspan.backends.base import Backend, CreateResult, PullResult, PushResult
|
|
14
14
|
from docspan.backends.confluence.adf.converter import AdfConverter
|
|
15
15
|
from docspan.backends.confluence.config.models import ConfluenceConfig as InternalConfluenceConfig
|
|
16
16
|
from docspan.backends.confluence.markdown.parser import MarkdownParser
|
|
@@ -193,6 +193,26 @@ class ConfluenceBackend(Backend):
|
|
|
193
193
|
page = self._client.get_page(doc_id)
|
|
194
194
|
return str(page["version"]["number"])
|
|
195
195
|
|
|
196
|
+
def create(self, title: str, **kwargs: object) -> CreateResult:
|
|
197
|
+
"""Create a new, empty Confluence page and return its id/title/url."""
|
|
198
|
+
self._ensure_client()
|
|
199
|
+
assert self._client is not None
|
|
200
|
+
space_key = kwargs.get("space") or self.config.space_key
|
|
201
|
+
if not space_key:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
"Confluence page creation requires a space key (--space or backends.confluence.space_key)."
|
|
204
|
+
)
|
|
205
|
+
empty_adf = AdfConverter().convert(MarkdownParser().parse(""))
|
|
206
|
+
page = ConfluencePage(title=title, content=empty_adf, parent_id="", space_key=str(space_key))
|
|
207
|
+
result = self._client.create_page(page)
|
|
208
|
+
page_id = result["id"]
|
|
209
|
+
base_url = self.config.base_url or ""
|
|
210
|
+
return CreateResult(
|
|
211
|
+
doc_id=page_id,
|
|
212
|
+
title=result.get("title", title),
|
|
213
|
+
url=f"{base_url}/pages/{page_id}",
|
|
214
|
+
)
|
|
215
|
+
|
|
196
216
|
def auth_setup(self, config_path: "Optional[str]" = None) -> None:
|
|
197
217
|
"""Interactive Confluence auth setup — prompts for credentials and prints YAML snippet."""
|
|
198
218
|
print("\nConfluence auth setup")
|
|
@@ -75,7 +75,11 @@ class PublishConfig:
|
|
|
75
75
|
auto_fix_hierarchy: Whether to automatically fix page hierarchy based on directory structure
|
|
76
76
|
auto_migrate_legacy: Whether to automatically migrate legacy editor pages to new editor before publishing
|
|
77
77
|
duplicate_similarity_threshold: Threshold for considering pages as duplicates (0.0-1.0, default 0.8)
|
|
78
|
-
render_mermaid_diagrams: Whether to render mermaid diagrams as images
|
|
78
|
+
render_mermaid_diagrams: Whether to render mermaid diagrams as images. NOTE: currently a
|
|
79
|
+
no-op — no code path reads this flag. Mermaid fences are always emitted as a plain
|
|
80
|
+
ADF code block (see docs/backends/confluence.md#limitations). Kept as a reserved
|
|
81
|
+
config key for when rendering is implemented, rather than removed, so existing
|
|
82
|
+
markgate.yaml files that set it don't start failing validation.
|
|
79
83
|
process_assets: Whether to process assets (images, diagrams) for embedding
|
|
80
84
|
ignore_patterns: List of file patterns to ignore (supports glob patterns like **/TODO.md)
|
|
81
85
|
archive_ignored: Whether to archive/delete ignored files from Confluence if they have page IDs
|