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