docspan 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docspan-0.3.0/.github/workflows/ci.yml +161 -0
- {docspan-0.2.0 → docspan-0.3.0}/.gitignore +5 -0
- docspan-0.3.0/.mypy-error-baseline +1 -0
- docspan-0.3.0/.release-please-manifest.json +3 -0
- docspan-0.3.0/CHANGELOG.md +143 -0
- {docspan-0.2.0 → docspan-0.3.0}/PKG-INFO +5 -1
- {docspan-0.2.0 → docspan-0.3.0}/README.md +2 -0
- {docspan-0.2.0 → docspan-0.3.0}/docs/backends/google-docs.md +6 -1
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/gdocs-tables-inline-styles/plan.md +4 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/decisions/ADR-001-checklist-state-as-literal-text.md +7 -7
- {docspan-0.2.0 → docspan-0.3.0}/pyproject.toml +17 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/base.py +1 -1
- docspan-0.3.0/src/docspan/backends/google_docs/backend.py +939 -0
- docspan-0.3.0/src/docspan/backends/google_docs/checkbox_state.py +100 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/client.py +36 -3
- docspan-0.3.0/src/docspan/backends/google_docs/docs_request_builder.py +2210 -0
- docspan-0.3.0/src/docspan/backends/google_docs/docs_structure_parser.py +638 -0
- docspan-0.3.0/src/docspan/backends/google_docs/heading_anchors.py +467 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/markdown_to_paragraph_parser.py +106 -12
- docspan-0.3.0/src/docspan/backends/google_docs/nodes_to_markdown.py +367 -0
- docspan-0.3.0/src/docspan/backends/google_docs/projection.py +301 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/push_preview.py +70 -1
- docspan-0.3.0/src/docspan/backends/google_docs/tabs.py +130 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/cli/main.py +66 -4
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/config.py +4 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/core/orchestrator.py +9 -8
- docspan-0.3.0/tests/fixtures/github_slugger_vectors.json +349 -0
- docspan-0.3.0/tests/test_checkbox_state.py +74 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_cli.py +164 -2
- docspan-0.3.0/tests/test_code_block_granularity.py +765 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_docs_request_builder.py +266 -7
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_docs_structure_parser.py +263 -0
- docspan-0.3.0/tests/test_gdocs_push_pipeline.py +1363 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_gdocs_tables_and_styles.py +157 -10
- docspan-0.3.0/tests/test_google_docs_backend.py +1147 -0
- docspan-0.3.0/tests/test_heading_anchors.py +1154 -0
- docspan-0.3.0/tests/test_heading_identity.py +456 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_markdown_to_paragraph_parser.py +33 -0
- docspan-0.3.0/tests/test_nodes_to_markdown.py +296 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_orchestrator.py +81 -1
- docspan-0.3.0/tests/test_span_trailing_newline.py +111 -0
- docspan-0.3.0/tests/test_table_cell_spans.py +542 -0
- docspan-0.3.0/tests/test_tabs.py +120 -0
- docspan-0.2.0/.github/workflows/ci.yml +0 -34
- docspan-0.2.0/.release-please-manifest.json +0 -3
- docspan-0.2.0/CHANGELOG.md +0 -67
- docspan-0.2.0/src/docspan/backends/google_docs/backend.py +0 -473
- docspan-0.2.0/src/docspan/backends/google_docs/docs_request_builder.py +0 -529
- docspan-0.2.0/src/docspan/backends/google_docs/docs_structure_parser.py +0 -212
- docspan-0.2.0/tests/test_google_docs_backend.py +0 -340
- {docspan-0.2.0 → docspan-0.3.0}/.github/workflows/publish.yml +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/.github/workflows/release-please.yml +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/CONTRIBUTING.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/Procfile +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/RAILWAY_SETUP.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/docs/backends/confluence.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/docs/commands.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/docs/configuration.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/docs/contributing.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/docs/index.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/docs/install.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/docspan.yaml.example +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/markgate.yaml.example +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/mkdocs.yml +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/modules/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/modules/auth.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/modules/conflict_handler.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/modules/converter.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/modules/gdrive_client.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/modules/sync_engine.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/bidirectional-comments/plan.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/implementation/adversarial-review.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/implementation/plan.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/implementation/release-checklist.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/implementation/validation.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/requirements.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/research/architecture.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/research/features.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/research/google-docs-push.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/research/pitfalls.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/docspan-release/research/stack.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/decisions/ADR-001-merge3-dependency.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/decisions/ADR-002-base-content-sidecar-store.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/implementation/adversarial-review.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/implementation/plan.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/implementation/validation.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/requirements.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/research/architecture.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/research/features.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/research/pitfalls.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/markgate-sync/research/stack.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/decisions/ADR-002-comment-risk-flagging-not-anchor-preservation.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/feature-gap-report.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/implementation/adversarial-review.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/implementation/architecture-review.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/implementation/plan.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/implementation/pre-mortem.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/implementation/validation.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/requirements.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/research/architecture.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/research/build-vs-buy.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/research/features.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/research/pitfalls.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/research/stack.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/research/ux.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/project_plans/wedding-planning-workflow/workflow-runbook.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/release-please-config.json +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/requirements.txt +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/runtime.txt +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/__main__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/comparator.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/converter.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/converters.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/interfaces.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/nodes.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/parser.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/validators.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/adf/visitors.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/backend.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/client.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/config/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/config/loader.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/config/models.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/config/validation.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/ast.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/extensions/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/extensions/frontmatter.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/extensions/mermaid.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/extensions/wikilinks.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/inline_parser.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/markdown/parser.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/markdown_file.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/page.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/path_utils.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/results.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/models/sync_status.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/attachment_client.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/base_client.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/client.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/comment_client.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/crawler.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/label_client.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/page_client.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/space_client.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/confluence/services/confluence/url_parser.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/auth.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/comments.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/converter.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/backends/google_docs/onboarding.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/cli/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/core/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/core/merge.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/core/paths.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/core/state.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/src/docspan/core/xdg.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/sync.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/terraform/gcp/README.md +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/terraform/gcp/main.tf +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/terraform/gcp/outputs.tf +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/terraform/gcp/variables.tf +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/terraform/main.tf +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/terraform/variables.tf +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/__init__.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/conftest.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_config.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_conflict_resolution.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_converter.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_google_comments.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_google_oauth.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_google_onboarding.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_merge.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_push_preview.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_state.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/tests/test_xdg_central_config.py +0 -0
- {docspan-0.2.0 → docspan-0.3.0}/uv.lock +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
# No `branches:` filter — every pull request runs CI regardless of its base.
|
|
7
|
+
# With the filter, a stacked PR (one based on another PR's branch rather than
|
|
8
|
+
# on main) got no checks at all, so the only branch in a stack that was ever
|
|
9
|
+
# tested was the bottom one.
|
|
10
|
+
pull_request:
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
# A stack of PRs rebases repeatedly, and each push supersedes the run before it.
|
|
14
|
+
# Cancel the outdated run rather than paying for the whole matrix on a commit
|
|
15
|
+
# nobody will look at. `main` is excluded from cancellation so a push there
|
|
16
|
+
# always produces a complete result.
|
|
17
|
+
concurrency:
|
|
18
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
19
|
+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
lint:
|
|
23
|
+
name: Lint
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
with:
|
|
28
|
+
fetch-depth: 0 # needed for hatch-vcs version detection
|
|
29
|
+
|
|
30
|
+
- name: Install uv
|
|
31
|
+
uses: astral-sh/setup-uv@v4
|
|
32
|
+
|
|
33
|
+
- name: Install dependencies
|
|
34
|
+
run: uv sync --extra dev
|
|
35
|
+
|
|
36
|
+
# Runs once rather than once per Python version — ruff's result does not
|
|
37
|
+
# depend on the interpreter it runs under (`target-version` is pinned in
|
|
38
|
+
# pyproject.toml), so the other five runs were duplicate work.
|
|
39
|
+
- name: ruff check
|
|
40
|
+
run: uv run ruff check src tests
|
|
41
|
+
|
|
42
|
+
test:
|
|
43
|
+
name: Test (${{ matrix.python-version }})
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
strategy:
|
|
46
|
+
fail-fast: false # one version failing shouldn't hide the others' results
|
|
47
|
+
matrix:
|
|
48
|
+
# pyproject declares requires-python = ">=3.9", so every version it
|
|
49
|
+
# claims to support gets tested. 3.13 and 3.14 were verified green
|
|
50
|
+
# before being added here.
|
|
51
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
52
|
+
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
with:
|
|
56
|
+
fetch-depth: 0 # needed for hatch-vcs version detection
|
|
57
|
+
|
|
58
|
+
- name: Install uv
|
|
59
|
+
uses: astral-sh/setup-uv@v4
|
|
60
|
+
|
|
61
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
62
|
+
run: uv python install ${{ matrix.python-version }}
|
|
63
|
+
|
|
64
|
+
- name: Install dependencies
|
|
65
|
+
run: uv sync --extra dev
|
|
66
|
+
|
|
67
|
+
- name: Test
|
|
68
|
+
run: uv run pytest --tb=short -q
|
|
69
|
+
|
|
70
|
+
types:
|
|
71
|
+
name: Types (mypy ratchet)
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v4
|
|
75
|
+
with:
|
|
76
|
+
fetch-depth: 0
|
|
77
|
+
|
|
78
|
+
- name: Install uv
|
|
79
|
+
uses: astral-sh/setup-uv@v4
|
|
80
|
+
|
|
81
|
+
- name: Install dependencies
|
|
82
|
+
run: uv sync --extra dev
|
|
83
|
+
|
|
84
|
+
# mypy cannot be a plain pass/fail gate here: `mypy src` currently reports
|
|
85
|
+
# 266 errors across 32 files, so adding it as one would leave CI
|
|
86
|
+
# permanently red, and `continue-on-error` would make it decorative.
|
|
87
|
+
#
|
|
88
|
+
# A ratchet is a real gate instead — it fails on any *new* error while
|
|
89
|
+
# tolerating the existing backlog, and the baseline only ever goes down.
|
|
90
|
+
# Delete this job and gate on mypy's exit code once the baseline reaches 0.
|
|
91
|
+
- name: mypy (must not exceed baseline)
|
|
92
|
+
run: |
|
|
93
|
+
set -uo pipefail
|
|
94
|
+
baseline=$(tr -dc '0-9' < .mypy-error-baseline)
|
|
95
|
+
if [ -z "$baseline" ]; then
|
|
96
|
+
echo "::error::.mypy-error-baseline does not contain a number"
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
output=$(uv run mypy src 2>&1 || true)
|
|
101
|
+
echo "$output" | tail -40
|
|
102
|
+
|
|
103
|
+
if printf '%s\n' "$output" | grep -q '^Success: no issues found'; then
|
|
104
|
+
count=0
|
|
105
|
+
else
|
|
106
|
+
count=$(printf '%s\n' "$output" \
|
|
107
|
+
| sed -n 's/^Found \([0-9]\{1,\}\) error.*/\1/p' | tail -1)
|
|
108
|
+
fi
|
|
109
|
+
if [ -z "$count" ]; then
|
|
110
|
+
echo "::error::could not parse an error count out of mypy's output"
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
echo "mypy errors: $count (baseline $baseline)"
|
|
115
|
+
if [ "$count" -gt "$baseline" ]; then
|
|
116
|
+
echo "::error::mypy errors rose from $baseline to $count." \
|
|
117
|
+
"Fix the new errors, or justify and raise .mypy-error-baseline."
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
if [ "$count" -lt "$baseline" ]; then
|
|
121
|
+
echo "::notice::mypy errors fell from $baseline to $count." \
|
|
122
|
+
"Please lower .mypy-error-baseline to $count to lock the gain in."
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
package:
|
|
126
|
+
name: Package
|
|
127
|
+
runs-on: ubuntu-latest
|
|
128
|
+
steps:
|
|
129
|
+
- uses: actions/checkout@v4
|
|
130
|
+
with:
|
|
131
|
+
fetch-depth: 0 # hatch-vcs derives the version from tags
|
|
132
|
+
|
|
133
|
+
- name: Install uv
|
|
134
|
+
uses: astral-sh/setup-uv@v4
|
|
135
|
+
|
|
136
|
+
# publish.yml only builds on `release: published`, so until now a change
|
|
137
|
+
# that broke packaging stayed invisible until release day.
|
|
138
|
+
- name: Build sdist and wheel
|
|
139
|
+
run: uv build
|
|
140
|
+
|
|
141
|
+
# Catches a dependency edited in pyproject.toml without re-locking, which
|
|
142
|
+
# would otherwise surface as `uv sync` resolving something the lockfile
|
|
143
|
+
# never pinned.
|
|
144
|
+
- name: Lockfile is in sync with pyproject
|
|
145
|
+
run: uv lock --check
|
|
146
|
+
|
|
147
|
+
# uv.lock records the index each package came from. Resolving it behind a
|
|
148
|
+
# private or corporate mirror rewrites every URL to that host, and
|
|
149
|
+
# committing the result both leaks the internal hostname into this public
|
|
150
|
+
# repo and breaks CI, which cannot reach it. `git add -A` will happily
|
|
151
|
+
# sweep that in, and the diff is thousands of near-identical lines that
|
|
152
|
+
# nobody reads.
|
|
153
|
+
- name: Lockfile references only the public index
|
|
154
|
+
run: |
|
|
155
|
+
set -uo pipefail
|
|
156
|
+
if grep -n 'registry = "' uv.lock | grep -v 'https://pypi.org/simple'; then
|
|
157
|
+
echo "::error file=uv.lock::uv.lock references a non-public index." \
|
|
158
|
+
"Re-resolve it with UV_INDEX_URL=https://pypi.org/simple uv lock."
|
|
159
|
+
exit 1
|
|
160
|
+
fi
|
|
161
|
+
echo "uv.lock: public index only"
|
|
@@ -65,3 +65,8 @@ site/
|
|
|
65
65
|
*.tfstate.backup
|
|
66
66
|
*.tfvars
|
|
67
67
|
.terraform.lock.hcl
|
|
68
|
+
|
|
69
|
+
# Session scaffolding — regenerated per backlog-session spawn/attach, never a
|
|
70
|
+
# repo deliverable (see commits 8f80a5b, bd693ca on other branches)
|
|
71
|
+
.backlog-context.md
|
|
72
|
+
.claude/commands/backlog/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
266
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.0](https://github.com/tstapler/docspan/compare/docspan-v0.2.0...docspan-v0.3.0) (2026-08-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **gdocs:** keep inline styling inside table cells ([#51](https://github.com/tstapler/docspan/issues/51)) ([880eeb1](https://github.com/tstapler/docspan/commit/880eeb1feb3a3f700a1641f360abf5bc32cab191)), closes [#49](https://github.com/tstapler/docspan/issues/49)
|
|
14
|
+
* **google-docs:** add Google Doc tab support (tab_id) ([#14](https://github.com/tstapler/docspan/issues/14)) ([cee1db6](https://github.com/tstapler/docspan/commit/cee1db6dde0c957088159339527a6b8dbef70b47))
|
|
15
|
+
* **google-docs:** resolve internal markdown anchors to heading links ([#36](https://github.com/tstapler/docspan/issues/36)) ([5faaa6c](https://github.com/tstapler/docspan/commit/5faaa6c7ded880eba048d20807ff76a4a5f480a8))
|
|
16
|
+
* **google-docs:** restyle a paragraph in place instead of retyping it ([edf0b13](https://github.com/tstapler/docspan/commit/edf0b13520aa1a60b1fb02ee51dc1bd4a967c412))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* **cli:** accept --config and --prefix before the subcommand as well as after ([00f6524](https://github.com/tstapler/docspan/commit/00f65241e41c5bb2deec5642eb36e8ea64a58744))
|
|
22
|
+
* **gdocs:** stop a repeated line stealing a live heading's identity ([#50](https://github.com/tstapler/docspan/issues/50)) ([2992d39](https://github.com/tstapler/docspan/commit/2992d3974dc9f7dea2547b5b8b3881f7c141f810))
|
|
23
|
+
* **google-docs:** align pass 2 by content so trimmed deletes can't mis-style ([ba56e08](https://github.com/tstapler/docspan/commit/ba56e08b7cc482c6bd569e9e5560e1c71a768ebf))
|
|
24
|
+
* **google-docs:** append past the last node without merging it into the last paragraph ([1aef861](https://github.com/tstapler/docspan/commit/1aef861981ee95463e7c2c9caa67bf32d90c9d2a))
|
|
25
|
+
* **google-docs:** apply inline styling when it is the only change ([c205552](https://github.com/tstapler/docspan/commit/c205552aed5c0f483ba5213bd39f2aff16587efe))
|
|
26
|
+
* **google-docs:** clear the inherited bullet on inserted non-list paragraphs ([dda8a50](https://github.com/tstapler/docspan/commit/dda8a505e8f822c077478656158b87cf6d74132b))
|
|
27
|
+
* **google-docs:** insert before a Table/ToC/SectionBreak into the body, not into it ([701e2d0](https://github.com/tstapler/docspan/commit/701e2d01433e9efc2c42a159ffb4f92d90972c56))
|
|
28
|
+
* **google-docs:** keep the paragraph newline out of the spans ([#35](https://github.com/tstapler/docspan/issues/35)) ([f73272a](https://github.com/tstapler/docspan/commit/f73272a0422e5c525a63bcedb6cb68fdb7ff517b))
|
|
29
|
+
* **google-docs:** make push idempotent for documents with fenced code blocks ([#41](https://github.com/tstapler/docspan/issues/41)) ([e98406e](https://github.com/tstapler/docspan/commit/e98406e2858958421c3bdf5e17c94f880056e540))
|
|
30
|
+
* **google-docs:** make render_prefix part of node/content identity ([#67](https://github.com/tstapler/docspan/issues/67)) ([6205850](https://github.com/tstapler/docspan/commit/62058503c38372dec7de64e4a8ea1b6603c7c2fc))
|
|
31
|
+
* **google-docs:** normalize away the render glyph Docs writes before a native code block ([#48](https://github.com/tstapler/docspan/issues/48)) ([31b4edd](https://github.com/tstapler/docspan/commit/31b4edd1bc944f1b48605d3a1ffe0d2283e161fe))
|
|
32
|
+
* **google-docs:** order pass-1 requests by their anchor, not their own index ([58e2b6a](https://github.com/tstapler/docspan/commit/58e2b6af2271dc200c225c39c44d1a567ae5722a))
|
|
33
|
+
* **google-docs:** pair Nth live table with Nth target regardless of emptiness ([#77](https://github.com/tstapler/docspan/issues/77)) ([46f96b6](https://github.com/tstapler/docspan/commit/46f96b6982bfade9656c09ea543bcdf9b692e876))
|
|
34
|
+
* **google-docs:** prefer a code-rendered node over a duplicate-text prose node ([#75](https://github.com/tstapler/docspan/issues/75)) ([5853566](https://github.com/tstapler/docspan/commit/5853566d4aaa3f57755373bccdaedde42709e9c5))
|
|
35
|
+
* **google-docs:** project empty paragraphs out of the diff instead of deleting them ([d8b1b5f](https://github.com/tstapler/docspan/commit/d8b1b5feb56439da34ae7be64c092b80927c30a7))
|
|
36
|
+
* **google-docs:** project the re-fetched live doc before pass-2 style alignment ([#69](https://github.com/tstapler/docspan/issues/69)) ([cf36561](https://github.com/tstapler/docspan/commit/cf36561b953e7d2c2da4c11f60c2a74bca3a2259))
|
|
37
|
+
* **google-docs:** recover native checkbox checked state on pull via markdown export ([#78](https://github.com/tstapler/docspan/issues/78)) ([c7352f7](https://github.com/tstapler/docspan/commit/c7352f79d38c19c092ade81cf1368dc3c72ad853))
|
|
38
|
+
* **google-docs:** render @-mention person chips as name/email text ([#15](https://github.com/tstapler/docspan/issues/15)) ([f5d7427](https://github.com/tstapler/docspan/commit/f5d742758855aeab435c50fba3a1cb51b9a79eef))
|
|
39
|
+
* **google-docs:** render fenced code blocks on tab-scoped pull instead of per-line inline code ([#74](https://github.com/tstapler/docspan/issues/74)) ([3bae98a](https://github.com/tstapler/docspan/commit/3bae98ac01007ca33288c6d87b2bf8288291c216))
|
|
40
|
+
* **google-docs:** render TITLE/SUBTITLE as headings instead of silently demoting them ([cc6cd0b](https://github.com/tstapler/docspan/commit/cc6cd0b6e8fdb8337cfa871f77c4466445041922))
|
|
41
|
+
* **google-docs:** report a dropped over-long span, and share the delete-trim arithmetic ([daa77a6](https://github.com/tstapler/docspan/commit/daa77a6398f9ed967deef1376b39d73e66050aea))
|
|
42
|
+
* **google-docs:** stop deleting the newline that anchors a table/ToC/section break ([9eba496](https://github.com/tstapler/docspan/commit/9eba496ee1d4db3fb788e910c359d9487f217baa))
|
|
43
|
+
* **google-docs:** stop replace inserts from duplicating a clamp-spared newline ([#76](https://github.com/tstapler/docspan/issues/76)) ([f440f68](https://github.com/tstapler/docspan/commit/f440f68b3fcd9fce0333dfb95b60a40e5638c7e3))
|
|
44
|
+
|
|
45
|
+
## [0.2.0](https://github.com/tstapler/docspan/compare/docspan-v0.1.0...docspan-v0.2.0) (2026-07-22)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### Features
|
|
49
|
+
|
|
50
|
+
* Add Railway Volume support for persistent state storage ([3a4b76e](https://github.com/tstapler/docspan/commit/3a4b76eb0cbc14afa02f3aa3de2c4607808fad9f))
|
|
51
|
+
* Add retry mechanism and improved error handling for Google Drive API ([e8a7b5f](https://github.com/tstapler/docspan/commit/e8a7b5f177ad2c3b8a356852eceb827326f8ce76))
|
|
52
|
+
* Auto-reload Google Sheet mappings on each sync cycle ([a2647c6](https://github.com/tstapler/docspan/commit/a2647c6c1435eb5624a389a4e673a57ead012123))
|
|
53
|
+
* **config:** XDG storage paths + central config with project prefixes ([#7](https://github.com/tstapler/docspan/issues/7)) ([0aa9165](https://github.com/tstapler/docspan/commit/0aa9165d24df95386b514d283cf846a2cdc809f7))
|
|
54
|
+
* **confluence:** port adf/markdown/services from markdown-confluence ([e9d1a85](https://github.com/tstapler/docspan/commit/e9d1a85a9747ac75a6d95d6351d18483297726a4))
|
|
55
|
+
* **google_docs:** checklist round-trip + comment/glyph-risk push gate ([#8](https://github.com/tstapler/docspan/issues/8)) ([bd2a885](https://github.com/tstapler/docspan/commit/bd2a885d6a2e18b758f12fc4a2aaf588c045d059))
|
|
56
|
+
* **google_docs:** docspan comments respond — reply/resolve round-trip ([#13](https://github.com/tstapler/docspan/issues/13)) ([7a662ed](https://github.com/tstapler/docspan/commit/7a662edb14b6f0078fcca110023cfc7e54724726))
|
|
57
|
+
* **google-docs:** add per-user OAuth auth option ([#4](https://github.com/tstapler/docspan/issues/4)) ([830369c](https://github.com/tstapler/docspan/commit/830369cba1817224ae0d02f0b14b6a84de84a4eb))
|
|
58
|
+
* **google-docs:** push markdown tables and inline links/formatting ([#3](https://github.com/tstapler/docspan/issues/3)) ([5b74246](https://github.com/tstapler/docspan/commit/5b74246eb1070355b39c5e84292e59f443457875))
|
|
59
|
+
* **google-docs:** read comments into a {file}.comments.md sidecar on pull ([#5](https://github.com/tstapler/docspan/issues/5)) ([aca2264](https://github.com/tstapler/docspan/commit/aca226412ef74c80603678d7ae1defaa25e38954))
|
|
60
|
+
* scaffold markgate package from google-docs-obsidian-sync fork ([44dd3c5](https://github.com/tstapler/docspan/commit/44dd3c586a670b4689154b2db9bc6cb8673d9702))
|
|
61
|
+
* **sync:** Google Docs structural-diff push, Confluence comments, three-way merge ([9a20e34](https://github.com/tstapler/docspan/commit/9a20e3452f2d92a240128d7c8e2f9c4b63a547f9))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
### Bug Fixes
|
|
65
|
+
|
|
66
|
+
* **ci:** add __future__ annotations for Python 3.9 compat in test ([9ceca65](https://github.com/tstapler/docspan/commit/9ceca65ca5ad2040c1c2ec215fc097b02ba1a0c4))
|
|
67
|
+
* **ci:** apply ruff autofix across all src and test files ([bee2784](https://github.com/tstapler/docspan/commit/bee2784b940d872045ce33faf0ce53f65150d80d))
|
|
68
|
+
* **ci:** resolve ruff lint failures and enable Actions PR creation ([8727e7b](https://github.com/tstapler/docspan/commit/8727e7bcabf1ddec4b4116b06e605ff24d0eaffe))
|
|
69
|
+
* **google-docs:** don't drop blockquote paragraphs on push ([#9](https://github.com/tstapler/docspan/issues/9)) ([e3b2597](https://github.com/tstapler/docspan/commit/e3b259799acfcdaa7f884edb49633f349860a978))
|
|
70
|
+
* **google-docs:** fix inline-style paragraph misalignment on push ([#10](https://github.com/tstapler/docspan/issues/10)) ([4f79ef8](https://github.com/tstapler/docspan/commit/4f79ef8b4669eee4d6fe361af5bae68bb4486019))
|
|
71
|
+
* **google-docs:** fix mid-document insert off-by-one causing paragraph merges ([#12](https://github.com/tstapler/docspan/issues/12)) ([c74bea2](https://github.com/tstapler/docspan/commit/c74bea2d6464df0c19abce31a39aea0bc18d1e46))
|
|
72
|
+
* **google-docs:** restore inline styling and unwrap redirect links on pull ([#11](https://github.com/tstapler/docspan/issues/11)) ([b90466c](https://github.com/tstapler/docspan/commit/b90466cd9765a290b02634bbe9b3869185e308bc))
|
|
73
|
+
* Improve nested list indentation in Google Docs to Markdown conversion ([d6a7539](https://github.com/tstapler/docspan/commit/d6a7539d4beade3426e5d0db838f1d9974f7294b))
|
|
74
|
+
* Remove CONFIG_YAML dependency, prefer individual env vars ([d5d5d4a](https://github.com/tstapler/docspan/commit/d5d5d4ae25a7c1d7a213071f968eba56a9289da3))
|
|
75
|
+
* Resolve service account storage quota error by storing sync state locally ([00e9cb6](https://github.com/tstapler/docspan/commit/00e9cb65033dfb6cca8e0aae2258cde458cfb342))
|
|
76
|
+
|
|
77
|
+
## [Unreleased]
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
- **google-docs:** internal markdown anchors (`[A1](#a1-current-state)`) now resolve to
|
|
81
|
+
Google Docs heading links instead of being written as a `#fragment` URL the Doc cannot
|
|
82
|
+
follow. Slugs follow `github-slugger`, checked against vectors generated from the real
|
|
83
|
+
implementation. `TITLE`/`SUBTITLE` paragraphs count as anchor targets, and both the
|
|
84
|
+
modern `Link.heading` and the legacy `Link.headingId` union members are read, so an
|
|
85
|
+
anchor survives a pull whether or not the fetch used `includeTabsContent`.
|
|
86
|
+
- **google-docs:** an anchor that names no heading is written as plain text with no link
|
|
87
|
+
and reported — `docspan push --dry-run` lists it, `docspan push` exits non-zero with a
|
|
88
|
+
warning naming the anchor and the heading anchors that *are* available. It is never
|
|
89
|
+
written as a link a reader can click and land nowhere, and never reported as a clean ✓.
|
|
90
|
+
- **google-docs:** both pull paths now emit the heading's slug. A default (no `tab_id`) pull
|
|
91
|
+
goes through Drive's HTML export, which carries the Doc's opaque `#h.abc123` through
|
|
92
|
+
verbatim; it is upgraded to the slug, so the pulled markdown works as markdown.
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
- **google-docs:** pass 2 parses and aligns the document once per push instead of three
|
|
96
|
+
times. The discarded work sat inside the window between pass 2's read and its write, where
|
|
97
|
+
a concurrent edit costs a conflict on a document pass 1 has already changed.
|
|
98
|
+
|
|
99
|
+
### Known limitations
|
|
100
|
+
Each of these is tracked as a follow-up rather than half-addressed here.
|
|
101
|
+
- An anchor into a heading in a *different tab* of the same document cannot be resolved and
|
|
102
|
+
is reported unresolved. The flat `headingId` member resolves against the tab named in the
|
|
103
|
+
request, so expressing one needs the tabs-aware `Link.heading` member.
|
|
104
|
+
- A pull cannot express a `bookmark`/`bookmarkId` link, a link to a tab, or any link inside
|
|
105
|
+
a table cell, so those are dropped from the pulled file without a report.
|
|
106
|
+
- Confluence writes an internal anchor as a literal `#fragment` href, which it does not
|
|
107
|
+
resolve.
|
|
108
|
+
- An anchor that resolves to nothing is written as plain text, so a later pull replaces the
|
|
109
|
+
author's `[text](#anchor)` with `text`. The push reports it; nothing does afterwards.
|
|
110
|
+
- Such a push exits non-zero on every run, with no flag to suppress it.
|
|
111
|
+
- A heading containing an HTML entity reference (`## Team & process`) or inline HTML
|
|
112
|
+
(`## <code>push()</code> …`) is slugged from the markdown *source* rather than the rendered
|
|
113
|
+
text, so its slug differs from GitHub's. Because duplicate numbering depends on the
|
|
114
|
+
headings before it, that can land an anchor on a neighbouring heading. Pre-existing; a fix
|
|
115
|
+
attempt was reverted on this branch because it needs the slug text and the
|
|
116
|
+
document text separated, which is its own change.
|
|
117
|
+
|
|
118
|
+
## [0.1.0] - 2026-06-07
|
|
119
|
+
|
|
120
|
+
### Added
|
|
121
|
+
- `docspan push` — push local markdown files to Google Docs or Confluence
|
|
122
|
+
- `docspan pull` — pull remote documents into local markdown files with three-way merge
|
|
123
|
+
- `docspan status` — show current mapping status in a table
|
|
124
|
+
- `docspan auth setup` — interactive authentication setup for `google_docs` and `confluence` backends
|
|
125
|
+
- `docspan conflicts list` — list files with unresolved merge conflicts
|
|
126
|
+
- `docspan conflicts resolve` — resolve merge conflicts with `remote`, `local`, or `merged` strategy
|
|
127
|
+
- Google Docs backend: push and pull via Google Docs API (service account auth)
|
|
128
|
+
- Confluence backend: push and pull via Atlassian REST API (API token auth)
|
|
129
|
+
- Three-way merge for bidirectional sync conflict detection
|
|
130
|
+
- Confluence comment sidecar: pull writes inline and footer comments to `{file}.comments.md`
|
|
131
|
+
- `markgate.yaml` config file format with per-mapping direction control (`push`/`pull`/`both`)
|
|
132
|
+
- Sync state tracking via `.markgate-state.json` and content-addressed base store in `.markgate-base/`
|
|
133
|
+
|
|
134
|
+
### Known Limitations
|
|
135
|
+
- Google Docs: comments on edited paragraphs are destroyed on push (paragraph-level diff; comments on unchanged paragraphs are preserved)
|
|
136
|
+
- Push: no image support — local image files cannot be pushed to Google Docs or Confluence
|
|
137
|
+
- Push: no table support — markdown tables are not rendered in Google Docs
|
|
138
|
+
- Confluence: requires an Atlassian API token; no OAuth flow
|
|
139
|
+
- Confluence: comment sidecar (`{file}.comments.md`) is informational only; comments cannot be pushed back
|
|
140
|
+
- Config file is named `markgate.yaml` (not `docspan.yaml`) and state file is `.markgate-state.json` (not `.docspan-state.json`). These will be renamed in v0.2.0.
|
|
141
|
+
|
|
142
|
+
[Unreleased]: https://github.com/tstapler/docspan/compare/v0.1.0...HEAD
|
|
143
|
+
[0.1.0]: https://github.com/tstapler/docspan/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: docspan
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Push and pull markdown to Google Docs and Confluence from a single CLI
|
|
5
5
|
Project-URL: Homepage, https://github.com/tstapler/docspan
|
|
6
6
|
Project-URL: Repository, https://github.com/tstapler/docspan
|
|
@@ -18,6 +18,8 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
23
|
Classifier: Topic :: Software Development :: Documentation
|
|
22
24
|
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
23
25
|
Requires-Python: >=3.9
|
|
@@ -282,6 +284,7 @@ Auth resolution order: `credentials_path` → `ACCOUNT_A_CREDENTIALS[_PATH]` env
|
|
|
282
284
|
| `backend` | string | — | yes | `"google_docs"` or `"confluence"` |
|
|
283
285
|
| `remote_id` | string | — | yes | Google Doc ID or Confluence page ID |
|
|
284
286
|
| `direction` | enum | `"both"` | no | `"push"`, `"pull"`, or `"both"` |
|
|
287
|
+
| `tab_id` | string | `null` | no | Google Docs tab id (e.g. `"t.moqlkhpwn82e"`) to target on a multi-tab doc. Ignored by the Confluence backend. If unset and the doc has more than one tab, push/pull still succeed but report a `"warning"` naming the tabs found, instead of silently syncing whichever tab is first. |
|
|
285
288
|
|
|
286
289
|
---
|
|
287
290
|
|
|
@@ -311,6 +314,7 @@ docspan generates these files in your project directory after first sync:
|
|
|
311
314
|
> - Checklist state (`- [ ]`/`- [x]`) round-trips as literal text — Google Docs' native checkbox glyph is intentionally not used because its checked/unchecked state cannot be read back via the API (see ADR-001)
|
|
312
315
|
> - `push --dry-run` now shows a real structural diff and flags paragraphs with open comments at risk; `push` blocks by default on a flagged paragraph unless `--force` is passed
|
|
313
316
|
> - If a push succeeds but a post-push check finds the open-comment count dropped, docspan reports this as a `⚠` warning — never a plain green success — so it's never mistaken for a clean push
|
|
317
|
+
> - Google Docs: multi-tab docs need `tab_id` set explicitly per mapping — find it in the doc's URL (`...#tab=t.XXXXXXXXXX` after clicking the tab) or from the warning message docspan prints when `tab_id` is left unset. Without it, pull/push default to the doc's first tab (`pull`'s default path additionally can't target a tab at all — it uses Drive's HTML export, which only ever returns the first tab; set `tab_id` to instead pull via the structural API, which can target any tab)
|
|
314
318
|
> - Google Docs OAuth requires each user to create their own GCP project (`docspan auth setup google_docs` → Personal/OAuth) and stays in Google's "Testing" publishing status — capped at 100 test users, with Google's "app isn't verified" warning shown on first sign-in. This avoids the annual CASA security assessment required to verify apps requesting Drive/Docs' restricted read-write scopes (a real recurring cost), at the price of a few extra manual setup minutes per user instead of a single embedded, zero-config client. Revisit if/when adoption outgrows a per-user-project model — options are paying for verification, or narrowing to the unrestricted `drive.file` scope via Google's Picker API (bigger rework: requires the user to explicitly select their doc through a picker rather than referencing it by ID in config)
|
|
315
319
|
|
|
316
320
|
---
|
|
@@ -233,6 +233,7 @@ Auth resolution order: `credentials_path` → `ACCOUNT_A_CREDENTIALS[_PATH]` env
|
|
|
233
233
|
| `backend` | string | — | yes | `"google_docs"` or `"confluence"` |
|
|
234
234
|
| `remote_id` | string | — | yes | Google Doc ID or Confluence page ID |
|
|
235
235
|
| `direction` | enum | `"both"` | no | `"push"`, `"pull"`, or `"both"` |
|
|
236
|
+
| `tab_id` | string | `null` | no | Google Docs tab id (e.g. `"t.moqlkhpwn82e"`) to target on a multi-tab doc. Ignored by the Confluence backend. If unset and the doc has more than one tab, push/pull still succeed but report a `"warning"` naming the tabs found, instead of silently syncing whichever tab is first. |
|
|
236
237
|
|
|
237
238
|
---
|
|
238
239
|
|
|
@@ -262,6 +263,7 @@ docspan generates these files in your project directory after first sync:
|
|
|
262
263
|
> - Checklist state (`- [ ]`/`- [x]`) round-trips as literal text — Google Docs' native checkbox glyph is intentionally not used because its checked/unchecked state cannot be read back via the API (see ADR-001)
|
|
263
264
|
> - `push --dry-run` now shows a real structural diff and flags paragraphs with open comments at risk; `push` blocks by default on a flagged paragraph unless `--force` is passed
|
|
264
265
|
> - If a push succeeds but a post-push check finds the open-comment count dropped, docspan reports this as a `⚠` warning — never a plain green success — so it's never mistaken for a clean push
|
|
266
|
+
> - Google Docs: multi-tab docs need `tab_id` set explicitly per mapping — find it in the doc's URL (`...#tab=t.XXXXXXXXXX` after clicking the tab) or from the warning message docspan prints when `tab_id` is left unset. Without it, pull/push default to the doc's first tab (`pull`'s default path additionally can't target a tab at all — it uses Drive's HTML export, which only ever returns the first tab; set `tab_id` to instead pull via the structural API, which can target any tab)
|
|
265
267
|
> - Google Docs OAuth requires each user to create their own GCP project (`docspan auth setup google_docs` → Personal/OAuth) and stays in Google's "Testing" publishing status — capped at 100 test users, with Google's "app isn't verified" warning shown on first sign-in. This avoids the annual CASA security assessment required to verify apps requesting Drive/Docs' restricted read-write scopes (a real recurring cost), at the price of a few extra manual setup minutes per user instead of a single embedded, zero-config client. Revisit if/when adoption outgrows a per-user-project model — options are paying for verification, or narrowing to the unrestricted `drive.file` scope via Google's Picker API (bigger rework: requires the user to explicitly select their doc through a picker rather than referencing it by ID in config)
|
|
266
268
|
|
|
267
269
|
---
|
|
@@ -58,5 +58,10 @@ mappings:
|
|
|
58
58
|
!!! warning
|
|
59
59
|
- **Comments destroyed on push for edited paragraphs**: The structural diff preserves comments on unchanged paragraphs, but any paragraph that is deleted and reinserted loses its comments. This is a known v0.1.0 limitation.
|
|
60
60
|
- **No image push support**: Local image files cannot be pushed. Images require publicly accessible URLs and additional Drive upload scope.
|
|
61
|
-
- **
|
|
61
|
+
- **Table cells hold one paragraph**: a markdown table cell is pushed as a single
|
|
62
|
+
paragraph, and inline formatting inside it (bold, monospace, links, internal
|
|
63
|
+
`#anchor` references) is applied on the second pass. Two limits follow: a cell
|
|
64
|
+
whose content spans more than one paragraph in the Doc cannot be styled, and a
|
|
65
|
+
table created by the current push gets its cell styling on the *next* push —
|
|
66
|
+
docspan reports both rather than failing silently.
|
|
62
67
|
- **Rate limiting**: The Google Docs API allows 300 requests per minute per project. Large documents with many changed paragraphs may trigger rate limit errors.
|
|
@@ -37,6 +37,10 @@ The Google Docs push path drops two things, both blocking real-world design-doc
|
|
|
37
37
|
|
|
38
38
|
### Tables (Part 2)
|
|
39
39
|
- New `DocsTableNode(rows: List[List[str]], start_index, end_index)` in the structure module.
|
|
40
|
+
- **Superseded.** `rows` is now `List[List[TableCell]]`, where `TableCell` carries
|
|
41
|
+
`text` *and* `spans`. Plain-text cells meant every mark inside one was dropped —
|
|
42
|
+
including internal `#anchor` cross-references, which rendered as dead text in the
|
|
43
|
+
Doc while the identical reference in a paragraph resolved. See #49.
|
|
40
44
|
- Parser emits `DocsTableNode` for `table` tokens (header row + body rows, cell = plain text).
|
|
41
45
|
- `DocsStructureParser` parses live `table` elements into `DocsTableNode` (rows from
|
|
42
46
|
`table.tableRows[].tableCells[].content` paragraphs), with real start/end indices.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
Research (`research/stack.md` §2, `research/build-vs-buy.md` §1c, cross-checked against multiple independent sources — Google's own REST reference, a tanaikech Apps Script gist, a Latenode community thread) confirms:
|
|
13
13
|
|
|
14
14
|
- The Docs API's `createParagraphBullets` request does support a distinct `bulletPreset: "BULLET_CHECKBOX"` value — checkboxes *can* be created programmatically.
|
|
15
|
-
-
|
|
15
|
+
- **`documents.get()` cannot expose checked/unchecked state.** It returns `glyphType: GLYPH_TYPE_UNSPECIFIED` for a checkbox bullet regardless of whether it is checked or unchecked. The JSON is reported identical across a check/uncheck action — the only observed difference is the document's `revisionId`, which carries no state information. This is confirmed symmetric between the REST API and the Apps Script `DocumentApp` service, meaning it is a limitation of that document model surface, not an SDK gap. (Later finding, see docspan issue "pull silently reports every checked box as unchecked": Drive's `files.export(mimeType='text/markdown')` is a *different* renderer that does expose it, as GFM `- [x]`/`- [ ]` — this is now used to recover state on docspan's default, non-tab-scoped pull path. It cannot target a single tab, so the tab-scoped pull path remains genuinely unrecoverable and continues to render every native checkbox unchecked.)
|
|
16
16
|
- The only indirect, *unofficial* signal is `textStyle.strikethrough` (checking a box in the UI applies strikethrough as a side effect) — but this is a UI convention, not a documented API contract, and Google Docs now offers a "checklist without strikethrough" UI option that breaks even this heuristic.
|
|
17
17
|
|
|
18
18
|
Options considered:
|
|
@@ -27,9 +27,9 @@ Use **Option 2 — literal text** as the default, document-wide representation.
|
|
|
27
27
|
|
|
28
28
|
**If the full-document survey (Task 0.1.2a) finds a mix of literal-text and native-glyph checklist paragraphs (finding (c) — the realistic case for a multi-author doc), this decision does not apply uniformly.** The literal-text scheme remains the default assumed representation, but any specific paragraph the survey identified as already being a native `BULLET_CHECKBOX` glyph is treated as **high-risk / requires manual handling**, not silently assumed to behave like the rest of the doc:
|
|
29
29
|
|
|
30
|
-
- Those paragraphs are listed by text prefix in `feature-gap-report.md` as known native-checkbox lines docspan does not track.
|
|
31
|
-
- Pull emits a `MixedChecklistWarning` (a `WARN`-level log line, plus a trailing marker comment in the pulled markdown, e.g. `<!-- docspan: native checkbox glyph, state not readable -->`) whenever it encounters a bullet paragraph whose resolved glyph is checkbox-shaped, so the blind spot is visible on every pull, not just documented once here and then forgotten.
|
|
32
|
-
- Tyler continues toggling those specific lines by hand in the Docs UI; docspan's pull renders them as plain, unmarked bulleted text (no marker corruption, just invisible state) until a future cycle designs real conversion support.
|
|
30
|
+
- Those paragraphs are listed by text prefix in `feature-gap-report.md` as known native-checkbox lines docspan does not track as literal text.
|
|
31
|
+
- Pull emits a `MixedChecklistWarning` (a `WARN`-level log line, plus a trailing marker comment in the pulled markdown, e.g. `<!-- docspan: native checkbox glyph, state not readable -->`) whenever it encounters a bullet paragraph whose resolved glyph is checkbox-shaped, so the blind spot is visible on every pull, not just documented once here and then forgotten. **This warning path is superseded on the default pull path** by the markdown-export recovery described above: when the recovered count matches and every line is matched, pull instead renders the real `- [x]`/`- [ ]` state with no warning at all. The warning (now phrased as a checkbox-count/match warning rather than an absolute "not readable" claim) still fires whenever that recovery can't proceed safely — a checkbox-count mismatch, an unmatched line, or a markdown-export transport failure — and always fires on the tab-scoped path, where recovery is never attempted.
|
|
32
|
+
- On the tab-scoped path specifically, Tyler continues toggling those specific lines by hand in the Docs UI; docspan's pull renders them as plain, unmarked bulleted text (no marker corruption, just invisible state) until a future cycle designs real conversion support for that path too.
|
|
33
33
|
- **As of the pre-mortem repair pass, `push()` itself also refuses (fail-closed, `--force`-gated) to write through any of these specific native-glyph paragraphs.** A live `GlyphShapeCheck`, folded into `find_high_risk_paragraphs()` alongside the existing comment-risk check, re-resolves each changed paragraph's glyph shape from `push()`'s own single fetch — not from this ADR's static survey table — and flags it the same way an open-comment paragraph is flagged (same `HighRiskParagraph`/`--force` mechanism, see plan.md Story 1.2.2/1.2.3). This closes pre-mortem.md #1: without it, checking off a paragraph the survey found to be a native glyph would layer a literal `[x]`/`[ ]` marker onto that glyph with no warning and no `--force` requirement.
|
|
34
34
|
|
|
35
35
|
## Rationale
|
|
@@ -57,7 +57,7 @@ Use **Option 2 — literal text** as the default, document-wide representation.
|
|
|
57
57
|
|
|
58
58
|
## Consequences
|
|
59
59
|
|
|
60
|
-
- No new fields are added to `DocsParagraphNode` (no `checked: Optional[bool]`), keeping the diff key (`style, text, is_list_item`) and all downstream request-building logic untouched.
|
|
60
|
+
- No new fields are added to `DocsParagraphNode` (no `checked: Optional[bool]`), keeping the diff key (`style, text, is_list_item`) and all downstream request-building logic untouched. Native-checkbox checked-state recovery on pull (see above) is implemented as a separate, purely functional post-processing step (`checkbox_state.py`) over the already-rendered markdown string, not as a new node field — this decision's diff-key claim still holds.
|
|
61
61
|
- A checklist toggle always produces a `deleteContentRange` + `insertText` (delete+reinsert) for that paragraph, exactly like any other text edit to that paragraph — this is the same mechanism that risks dropping an anchored comment on that paragraph (see ADR-002 for the mitigation).
|
|
62
|
-
- If a future cycle wants native checkbox glyphs purely for visual polish (not as source of truth), that can be added later as a strictly additive, cosmetic push-time flag without touching this decision
|
|
63
|
-
- **If the survey finds a mixed document (finding (c)):** the literal-text scheme is not a safe blanket assumption — specific paragraphs are exceptions, tracked by name, and flagged loudly (not silently) on every pull via `MixedChecklistWarning
|
|
62
|
+
- If a future cycle wants native checkbox glyphs purely for visual polish (not as source of truth), that can be added later as a strictly additive, cosmetic push-time flag without touching this decision. Push behavior for native checkboxes remains unchanged by the pull-side recovery above: an edit to one of these paragraphs is still routed through the existing `GlyphShapeCheck` high-risk gate, never silently applied.
|
|
63
|
+
- **If the survey finds a mixed document (finding (c)):** the literal-text scheme is not a safe blanket assumption — specific paragraphs are exceptions, tracked by name, and flagged loudly (not silently) on every pull via `MixedChecklistWarning` whenever native-checkbox state can't be safely recovered (mismatch, transport failure, or the tab-scoped path). Tyler must treat any doc-wide claim like "checklist state round-trips correctly" (Success Metric 3) as true for the literal-text paragraphs, and — on the default pull path, when recovery succeeds — for native-checkbox paragraphs too; only the tab-scoped path remains an unconditional gap. Since the pre-mortem repair pass, `push()` also refuses to write through one of these specific paragraphs without `--force` (`GlyphShapeCheck`, plan.md Story 1.2.2/1.2.3) — the mixed-doc case is guarded at write time and, for the default pull path, largely resolved at read time.
|
|
@@ -23,6 +23,8 @@ classifiers = [
|
|
|
23
23
|
"Programming Language :: Python :: 3.10",
|
|
24
24
|
"Programming Language :: Python :: 3.11",
|
|
25
25
|
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
26
28
|
"Topic :: Software Development :: Documentation",
|
|
27
29
|
"Topic :: Text Processing :: Markup :: Markdown",
|
|
28
30
|
]
|
|
@@ -94,3 +96,18 @@ disallow_untyped_defs = true
|
|
|
94
96
|
[tool.pytest.ini_options]
|
|
95
97
|
testpaths = ["tests"]
|
|
96
98
|
python_files = "test_*.py"
|
|
99
|
+
|
|
100
|
+
# Resolve against the public index explicitly, overriding whatever index the
|
|
101
|
+
# machine is configured with. uv's precedence is CLI > env > this file > the
|
|
102
|
+
# user's ~/.config/uv/uv.toml > system config, so a contributor whose machine
|
|
103
|
+
# points uv at a private or corporate mirror still gets a lockfile with
|
|
104
|
+
# pypi.org URLs.
|
|
105
|
+
#
|
|
106
|
+
# Without this, any `uv run`/`uv lock` on such a machine silently rewrites every
|
|
107
|
+
# URL in uv.lock to that mirror. Committing the result leaks the internal
|
|
108
|
+
# hostname into this public repo and breaks CI, which cannot reach it — and the
|
|
109
|
+
# diff is thousands of near-identical lines nobody reads. In CI there is no user
|
|
110
|
+
# config, so this is a no-op there. `ci.yml` greps uv.lock as the backstop.
|
|
111
|
+
[[tool.uv.index]]
|
|
112
|
+
url = "https://pypi.org/simple"
|
|
113
|
+
default = true
|