mcp-gee-sweet 0.9.2.dev286__tar.gz → 0.9.2.dev289__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.
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/kai.md +1 -1
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/PKG-INFO +1 -1
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/doc-to-markdown.md +17 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/docs_content.md +63 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/roadmap.md +1 -1
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/pyproject.toml +1 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +153 -43
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_markdown_export.py +184 -5
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/uv.lock +24 -1
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/cleanup-worktrees.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/merge-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/notify-kai.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/notify-partner.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/orchestrator.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/prep-for-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/qa-kickoff.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/release.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/retro.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/roadmap-status.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/start-worktree.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/team-member.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/verify-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/settings.json +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/amy.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/aziz.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/bob.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/dev.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/joy.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/qa.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.dockerignore +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/workflows/ci.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/workflows/docs.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/workflows/publish-dev.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/workflows/release.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.gitignore +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.python-version +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/CLAUDE.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/CODE_OF_CONDUCT.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/Dockerfile +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/LICENSE +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/Makefile +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/assets/logo.png +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/auth.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/client-setup.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/configuration.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-async-tool-execution.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-cache-invalidation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-comments-first-class.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-dev-team.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-gmail-domain.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-grid-data-size-cap.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-local-fs-test-harness.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-pillow-image-dependency.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-prompt-qa-role.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-release-docs-roles.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-repositioning.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-review-finding-ticket-bar.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-sync-folder-recursion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-tool-error-visibility.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/role-file-history.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/blockquote-representation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/borderless-table-columns.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/heading-anchor-resolution.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/image-conversion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/markdown-support.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/module-history.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/oauth-reauthorize-from-tool-call.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/sse-consent-off-event-loop.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/gmail-test-plan.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04-docs.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04-infra.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-09-04.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/retro-v0.9.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/runs/v0.8.1.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/runs/v0.9.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/calendar.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/docs_layout.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/docs_style.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/docs_tables.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/drive_activity.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/drive_files.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/drive_sharing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/drive_transfer.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/gmail.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/infra.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/sheets_mgmt.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/sheets_read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/style-guide.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/tools.md +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/pyrightconfig.json +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/check_supports_all_drives.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/lane_context_hook.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/qa_cancel_insert_local_images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/qa_gmail_fixtures.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/setup_team.sh +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/write_team_mcp_configs.sh +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/server.json +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/.env.template +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/auth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/http_transport.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/reauth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/server.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/sse_shutdown.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/calendar.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/gmail.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/pagination.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/conftest.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/test_activity.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/test_files.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/test_sharing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/test_transfer.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/fakes.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/integration/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/integration/conftest.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/integration/test_local_fs_harness.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/sheets/test_data.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/sheets/test_structure.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_auth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_calendar.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_check_supports_all_drives.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_concurrency.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_anchors.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_comments.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_content.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_core.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_editing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_indices.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_layout.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_named_ranges.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_style.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_tables.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_gmail.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_lane_context_hook.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_qa_gmail_fixtures.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_reauth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_response_limits.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_server.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_server_json.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_sse_shutdown.py +0 -0
- {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_team_mcp_configs.py +0 -0
|
@@ -25,7 +25,7 @@ The incidents behind the rules in this file are in [`docs/decisions/role-file-hi
|
|
|
25
25
|
- Team-process PRs (edits to `CLAUDE.md`, `.claude/team-roles/*.md`, `.claude/commands/*.md`) are Bob's own merge track, not Kai's — Bob reviews, labels `prompt-qa-approved`, and merges those himself (see `.claude/team-roles/bob.md` step 4). This used to be Kai's job too; per direct user instruction, 2026-07-21, it isn't anymore.
|
|
26
26
|
- Cutting a release once Aziz has signed off (`/release`)
|
|
27
27
|
- Worktree cleanup after merge (`cleanup-worktrees`) — this must never remove the persistent dev-team slot worktrees (the full `WORKTREE_ROLES` list in `scripts/setup_team.sh`, currently eight), only ad hoc worker worktrees
|
|
28
|
-
- Release readiness and documentation quality are Aziz's and Amy's territory, not Kai's — when cutting a release, hand off to Aziz (`/team-member Aziz`) for the review/QA-tier/sign-off pass rather than doing it here; route doc gaps found along the way to Amy (`documentation` label) rather than fixing them inline.
|
|
28
|
+
- Release readiness and documentation quality are Aziz's and Amy's territory, not Kai's — when cutting a release, hand off to Aziz (`/team-member Aziz`) for the review/QA-tier/sign-off pass rather than doing it here; route doc gaps found along the way to Amy (`documentation` label) rather than fixing them inline. Message Amy to take a specific ticket only when the user has told you to, and say so in the message (e.g. "the user asked me to hand you #N"): `amy.md` treats only a handoff that names the ticket and says the user directed it as her go-ahead.
|
|
29
29
|
|
|
30
30
|
Do NOT pick up ticket implementation directly in this session — hand it to a worker session via `start-worktree` (or `next-issue`, which does this automatically), or to a dev-team lane via `/team-member` if the team is active.
|
|
31
31
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mcp-gee-sweet
|
|
3
|
-
Version: 0.9.2.
|
|
3
|
+
Version: 0.9.2.dev289
|
|
4
4
|
Summary: MCP server for Google Workspace — Sheets, Drive, Docs (HTML or Markdown), and Calendar.
|
|
5
5
|
Project-URL: Homepage, https://github.com/khuisman/mcp-gee-sweet
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/khuisman/mcp-gee-sweet/issues
|
|
@@ -120,6 +120,23 @@ Three gaps were kept **as documented limitations, not implementation shortcuts**
|
|
|
120
120
|
next to ordinary text in the same paragraph — the one case it can't resolve (a paragraph
|
|
121
121
|
containing *only* a single inline code span, nothing else) is the genuinely ambiguous case
|
|
122
122
|
documented above, not a bug in the heuristic.
|
|
123
|
+
- **Code delimiters** (#923, #940, #938): a code span's content is literal, so the serializer
|
|
124
|
+
can only pick a delimiter and padding. A fenced block's fence and an inline span's delimiter
|
|
125
|
+
are one backtick longer than the longest backtick run in the content (fences at least 3).
|
|
126
|
+
An inline span is padded with one space a side when its content starts or ends with a
|
|
127
|
+
backtick, or starts *and* ends with a space (CommonMark strips one space from each end in that
|
|
128
|
+
case). Adjacent code runs sharing a link merge into one span, since two spans side by side
|
|
129
|
+
would read as one. An inline code run's line breaks become `<br>` between separate spans: a
|
|
130
|
+
span can't hold a line break, and a raw newline could start a new block. In a table cell, a
|
|
131
|
+
code line containing `|` renders as raw HTML `<code>`, because the GFM `\|` escape shows its
|
|
132
|
+
backslash in renderers that don't implement it. Text inside raw HTML is still parsed as
|
|
133
|
+
Markdown, so every ASCII punctuation character in that line becomes a numeric entity
|
|
134
|
+
(`|`, `*`, `\`, ...). Line breaks at either end of a linked code run go outside the
|
|
135
|
+
link. Because the exporter writes its own `<br>`/`<code>` HTML, plain text escapes `~`, a `<`
|
|
136
|
+
that could open a tag, and an `&` that starts an entity, so typed markup can't pass for
|
|
137
|
+
exporter markup. The export tests
|
|
138
|
+
check these against a strict CommonMark parser (`markdown-it-py`), since Python-Markdown
|
|
139
|
+
strips whitespace in code spans and closes a fence only on an exact-length match.
|
|
123
140
|
- **Tight vs. loose lists**: consecutive `BulletItem` nodes are joined with a single `"\n"`
|
|
124
141
|
rather than the `"\n\n"` used between other block types — CommonMark still parses a
|
|
125
142
|
blank-line-separated run of list items as one list either way (just "loose", wrapped in `<p>`
|
|
@@ -3541,6 +3541,69 @@ Edge spacers (single <br> each): markdown = "| Trailing test<br> |\n| --- |\n| <
|
|
|
3541
3541
|
|
|
3542
3542
|
---
|
|
3543
3543
|
|
|
3544
|
+
### TC-DOC206: Inline code keeps a space at both ends, and adjacent code runs export as one span (#938)
|
|
3545
|
+
**Setup:** raw HTML via `write_doc_content(doc_id={DOC_ID}, content_format='html', content="<p>pad <code> both ends </code> end</p><p>adj <code>x`</code><b><code>`y</code></b> end</p>")`. The bold change splits the second paragraph's code into two Courier New runs.
|
|
3546
|
+
|
|
3547
|
+
**Prompt**
|
|
3548
|
+
> "Export doc {DOC_ID} as Markdown"
|
|
3549
|
+
|
|
3550
|
+
**Checks**
|
|
3551
|
+
- `markdown` contains ``pad ` both ends ` end``: one extra space a side, since CommonMark strips one space from each end when both ends have one
|
|
3552
|
+
- `markdown` contains ```` adj ```x``y``` end ````: the two runs merge into one span, with a three-backtick delimiter around the two-backtick run
|
|
3553
|
+
- Rendering `markdown` with a strict CommonMark renderer (e.g. `markdown-it-py`'s `commonmark` preset, not Python `markdown`, which strips code-span whitespace) gives `<code> both ends </code>` and `<code>x``y</code>`
|
|
3554
|
+
|
|
3555
|
+
**Cleanup:** write fixture content back over `{DOC_ID}`
|
|
3556
|
+
|
|
3557
|
+
**Result (2026-10-10) ✅ PASS — Kit, PR #968 round 1 (`d2449f7`).** `get_doc_as_markdown` returned ``pad ` both ends ` end\n\nadj ```x``y``` end``. markdown-it-py 4.2.0's `commonmark` preset rendered `<code> both ends </code>` and `<code>x``y</code>`. Fixture content was written back.
|
|
3558
|
+
|
|
3559
|
+
**Result (2026-10-10) ✅ PASS — Kit, PR #968 round 2 (`759362a`).** Re-run because the fix changed `_render_run`. Output was identical to round 1. Fixture content was written back.
|
|
3560
|
+
|
|
3561
|
+
---
|
|
3562
|
+
|
|
3563
|
+
### TC-DOC207: Inline code in a table cell keeps a literal `|` and puts line breaks outside the code (#938)
|
|
3564
|
+
**Setup:** raw HTML via `write_doc_content(doc_id={DOC_ID}, content_format='html', content="<table><tr><td>Head</td></tr><tr><td>pipe <code>a|b</code> and a|b</td></tr><tr><td><code>x</code><br><code>y|z</code></td></tr></table>")`. The last cell has two code-styled paragraphs.
|
|
3565
|
+
|
|
3566
|
+
**Prompt**
|
|
3567
|
+
> "Export doc {DOC_ID} as Markdown"
|
|
3568
|
+
|
|
3569
|
+
**Checks**
|
|
3570
|
+
- `markdown` contains the row `| pipe <code>a|b</code> and a\|b |`: the code's pipe is an HTML entity inside raw `<code>`, with no backslash, and the plain-text pipe is still escaped
|
|
3571
|
+
- `markdown` contains the row `` | `x`<br><code>y|z</code> | ``: the cell's paragraph break is a `<br>` between two code elements, not inside one
|
|
3572
|
+
- Rendering `markdown` with a GFM-table renderer (e.g. `markdown-it-py`'s `commonmark` preset with `table` enabled) gives a one-column table whose cells contain `<code>a|b</code>` and `<code>x</code><br><code>y|z</code>`. No cell shows a backslash
|
|
3573
|
+
|
|
3574
|
+
Not covered live: a soft break (Shift+Enter) inside an inline code run in a paragraph that also has plain text. No tool can build one (`style_doc_range` doesn't set a font), so unit tests cover it (`TestInlineCodeCommonMark::test_newline_in_inline_code_does_not_start_a_new_block`).
|
|
3575
|
+
|
|
3576
|
+
**Cleanup:** write fixture content back over `{DOC_ID}`
|
|
3577
|
+
|
|
3578
|
+
**Result (2026-10-10) ✅ PASS (checks as written); ❌ probe beyond the case — Kit, PR #968 round 1 (`d2449f7`).** `get_doc_as_markdown` returned both expected rows exactly: `| pipe <code>a|b</code> and a\|b |` and `` | `x`<br><code>y|z</code> | ``. markdown-it-py's `commonmark` preset with `table` enabled rendered a one-column table with `<code>a|b</code>` and `<code>x</code><br><code>y|z</code>`, and no backslash. Probe: a cell with code `a|*b* _c_` exported as `<code>a|*b* _c_</code>`, which renders `<em>` inside the `<code>`. A cell with code `C:\|x` exported as `<code>C:\|x</code>`, which renders as `C:&#124;x`. Both are sent back on the PR. Fixture content was written back.
|
|
3579
|
+
|
|
3580
|
+
**Result (2026-10-10) ✅ PASS — Kit, PR #968 round 2 (`759362a`).** Re-run because the fix changed the table-cell `<code>` path. Both expected rows came back exactly, and the render was the same as round 1. The round 1 probe is now TC-DOC208, which passes. Fixture content was written back.
|
|
3581
|
+
|
|
3582
|
+
---
|
|
3583
|
+
|
|
3584
|
+
### TC-DOC208: Markdown punctuation inside table-cell `<code>`, linked code after a cell break, and HTML-like plain text all export literally (#938)
|
|
3585
|
+
**Setup:** raw HTML via `write_doc_content(doc_id={DOC_ID}, content_format='html', content=...)` with this content. The `\` in `C:\|x` is one literal backslash character.
|
|
3586
|
+
|
|
3587
|
+
```html
|
|
3588
|
+
<table><tr><td>Head</td></tr><tr><td><code>a|*b* _c_</code></td></tr><tr><td><code>C:\|x</code></td></tr><tr><td><code>x</code><br><a href="https://example.com/b"><code>b</code></a></td></tr></table><p>Use <br> or &#124; and ~~old~~, AT&T a < b</p>
|
|
3589
|
+
```
|
|
3590
|
+
|
|
3591
|
+
**Prompt**
|
|
3592
|
+
> "Export doc {DOC_ID} as Markdown"
|
|
3593
|
+
|
|
3594
|
+
**Checks**
|
|
3595
|
+
- `markdown` contains the row `| <code>a|*b* _c_</code> |`: every punctuation character inside raw `<code>` is a numeric entity
|
|
3596
|
+
- `markdown` contains the row `| <code>C:\|x</code> |`: the backslash is an entity too, so it can't escape the pipe's `&`
|
|
3597
|
+
- `markdown` contains the row `` | `x`<br>[`b`](https://example.com/b) | ``: the `<br>` from the cell's paragraph break is outside the link
|
|
3598
|
+
- `markdown` contains the paragraph `Use \<br> or \| and \~\~old\~\~, AT&T a < b`: `<` before a tag, `&` starting an entity, and `~` are escaped; `AT&T` and `a < b` are not
|
|
3599
|
+
- Rendering `markdown` with `markdown-it-py`'s `commonmark` preset with `table` enabled gives cells `<code>a|*b* _c_</code>`, `<code>C:\|x</code>`, and `<code>x</code><br><a href="https://example.com/b"><code>b</code></a>`, with no `<em>` inside any `<code>`, and the paragraph renders as literal text (no line break, no `|`, no strikethrough)
|
|
3600
|
+
|
|
3601
|
+
**Cleanup:** write fixture content back over `{DOC_ID}`
|
|
3602
|
+
|
|
3603
|
+
**Result (2026-10-10) ✅ PASS — Kit, PR #968 round 2 (`759362a`).** `get_doc_as_markdown` returned all four expected lines exactly. markdown-it-py 4.2.0 (`commonmark` + `table` + `strikethrough`) rendered the cells `<code>a|*b* _c_</code>`, `<code>C:\|x</code>`, and `<code>x</code><br><a href="https://example.com/b"><code>b</code></a>`, and the paragraph `Use <br> or &#124; and ~~old~~, AT&T a < b`, with no `<em>`, `<del>`, or line break. Fixture content was written back.
|
|
3604
|
+
|
|
3605
|
+
---
|
|
3606
|
+
|
|
3544
3607
|
## Orphan fileId on inline-image sharing failure (#649)
|
|
3545
3608
|
|
|
3546
3609
|
Mirrors #420's fix in `drive/transfer.py` (see TC-D249/TC-D250 in `docs/qa/tests/drive_transfer.md`) applied to the same create()-succeeds-but-follow-up-fails shape in `docs/images.py`'s `upload_and_share_image` and `docs/content.py`'s `_resolve_image_source`: a transient failure in the `permissions().create()`/`files().get()` sharing step, after `files().create()` (or an already-uploaded `_upload_local_file`) had already succeeded, previously returned a bare `{"error": ...}` with no way to find the Drive file that now genuinely exists. Not reliably reproducible live (would require forcing a transient API failure in the exact window between create and share); verified by unit test instead, matching TC-D249/TC-D250's own convention.
|
|
@@ -192,7 +192,7 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
|
|
|
192
192
|
- [x] `create_paragraph_bullets` (`style.py`) has the same isolated-run collapse-to-`nestingLevel 0` bug #439 fixed in `emitter.py` only — port PR #711's anchor-paragraph approach to the sibling path (PR #723) ([#713](https://github.com/khuisman/mcp-gee-sweet/issues/713))
|
|
193
193
|
- [x] Lock in the isolated-run rendering rule — an isolated bullet run renders at its true depth (`depth N` → `nestingLevel N`) at *every* depth, never clamped (decided 2026-09-09: consistency over avoiding orphaned indent; the markup generator owns the depth it emits). Extend TC-DOC190 with a `min_depth >= 2` case; state the rule in `emitter.py`'s bullet-pass comment and the `docs/emitter.py` paragraph of `CLAUDE.md` (PR #731) ([#714](https://github.com/khuisman/mcp-gee-sweet/issues/714))
|
|
194
194
|
- [x] `create_doc` markdown: a fenced code block whose body is raw HTML tags renders as loose plain paragraphs (fence ignored, indentation flattened) instead of a monospaced code block — python-markdown's raw-HTML-block handling appears to pre-empt `fenced_code`. Surfaced during #714 triage (PR #730) ([#719](https://github.com/khuisman/mcp-gee-sweet/issues/719))
|
|
195
|
-
- [
|
|
195
|
+
- [x] **Bundle:** Docs write side (emitter, html_parser, images, content), one PR closing [#509](https://github.com/khuisman/mcp-gee-sweet/issues/509), [#567](https://github.com/khuisman/mcp-gee-sweet/issues/567), [#581](https://github.com/khuisman/mcp-gee-sweet/issues/581), [#638](https://github.com/khuisman/mcp-gee-sweet/issues/638), [#715](https://github.com/khuisman/mcp-gee-sweet/issues/715) — lane A (PR #961) ([#894](https://github.com/khuisman/mcp-gee-sweet/issues/894))
|
|
196
196
|
- [x] **Bundle:** Calendar hardening, one PR closing [#459](https://github.com/khuisman/mcp-gee-sweet/issues/459), [#615](https://github.com/khuisman/mcp-gee-sweet/issues/615), [#626](https://github.com/khuisman/mcp-gee-sweet/issues/626), [#627](https://github.com/khuisman/mcp-gee-sweet/issues/627), [#726](https://github.com/khuisman/mcp-gee-sweet/issues/726) — lane B (PR #912) ([#890](https://github.com/khuisman/mcp-gee-sweet/issues/890))
|
|
197
197
|
- [x] `delete_file(permanent=True)` surfaces a misleading 404 when the caller can trash but not permanently delete (Shared Drive Content manager) — an agent can read it as "already gone" and skip cleanup; check `capabilities.canDelete` first — lane B ([#876](https://github.com/khuisman/mcp-gee-sweet/issues/876)) (PR #888)
|
|
198
198
|
- [x] SSE: SIGTERM with an SSE stream open logs an "Exception in ASGI application" traceback; the server still exits cleanly, pre-existing before #833 — lane A (PR #911) ([#868](https://github.com/khuisman/mcp-gee-sweet/issues/868))
|
|
@@ -21,11 +21,21 @@ Known, deliberate gaps (documented rather than silently dropped):
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
23
|
import re
|
|
24
|
+
import string
|
|
24
25
|
|
|
25
26
|
from .ast import BulletItem, Cell, DocNode, Heading, Image, NamedBlock, Paragraph, Run, Table
|
|
26
27
|
from .emitter import _build_phantom_set
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
# Characters in plain text that Markdown would otherwise read as syntax. "~" is
|
|
30
|
+
# GFM strikethrough. "<" only when it could open an HTML tag or autolink, and
|
|
31
|
+
# "&" only when it starts an entity reference, so ordinary prose ("a < b",
|
|
32
|
+
# "AT&T") stays readable. Exported text must not be mistaken for the <br> and
|
|
33
|
+
# <code> HTML the exporter writes itself (#938).
|
|
34
|
+
_MD_ESCAPE = re.compile(
|
|
35
|
+
r"[\\`*_\[\]~]"
|
|
36
|
+
r"|<(?=[A-Za-z/!?])"
|
|
37
|
+
r"|&(?=#[0-9]{1,7};|#[xX][0-9a-fA-F]{1,6};|[A-Za-z][A-Za-z0-9]*;)"
|
|
38
|
+
)
|
|
29
39
|
_BACKTICK_RUN = re.compile(r"`+")
|
|
30
40
|
|
|
31
41
|
# A paragraph whose rendered text starts with one of these (unescaped) would be
|
|
@@ -93,23 +103,16 @@ def _render_code_block(node: Paragraph) -> str:
|
|
|
93
103
|
Run text did."""
|
|
94
104
|
segments: list[str] = []
|
|
95
105
|
buffer = ""
|
|
96
|
-
|
|
97
|
-
def fenced(text: str) -> str:
|
|
98
|
-
# Literal fences in the content must never close the surrounding block.
|
|
99
|
-
ticks = max((len(match.group()) for match in _BACKTICK_RUN.finditer(text)), default=0)
|
|
100
|
-
fence = "`" * max(3, ticks + 1)
|
|
101
|
-
return f"{fence}\n{text}\n{fence}"
|
|
102
|
-
|
|
103
106
|
for item in node.runs:
|
|
104
107
|
if isinstance(item, Run):
|
|
105
108
|
buffer += item.text
|
|
106
109
|
else:
|
|
107
110
|
if buffer:
|
|
108
|
-
segments.append(
|
|
111
|
+
segments.append(_fenced_code_block(buffer))
|
|
109
112
|
buffer = ""
|
|
110
113
|
segments.append(f"})")
|
|
111
114
|
if buffer:
|
|
112
|
-
segments.append(
|
|
115
|
+
segments.append(_fenced_code_block(buffer))
|
|
113
116
|
return "\n\n".join(segments)
|
|
114
117
|
|
|
115
118
|
|
|
@@ -171,7 +174,7 @@ def _md_link_dest(url: str) -> str:
|
|
|
171
174
|
|
|
172
175
|
def _render_inline(items: list[Run | Image]) -> str:
|
|
173
176
|
parts: list[str] = []
|
|
174
|
-
for item in items:
|
|
177
|
+
for item in _coalesce_code_runs(items):
|
|
175
178
|
if isinstance(item, Image):
|
|
176
179
|
parts.append(f"})")
|
|
177
180
|
else:
|
|
@@ -179,22 +182,63 @@ def _render_inline(items: list[Run | Image]) -> str:
|
|
|
179
182
|
return "".join(parts)
|
|
180
183
|
|
|
181
184
|
|
|
182
|
-
def
|
|
185
|
+
def _is_code_run(item: Run | Image | Table) -> bool:
|
|
186
|
+
return isinstance(item, Run) and item.font_family == "Courier New"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _coalesce_code_runs(items: list) -> list:
|
|
190
|
+
"""Merge adjacent code runs that share a link. Docs splits a run at any
|
|
191
|
+
style change (bold, color), but rendered side by side two code spans
|
|
192
|
+
would read as one: `` `a``b` `` parses as the single span "a``b", since
|
|
193
|
+
the first span's closing delimiter and the second's opening one form one
|
|
194
|
+
backtick run. A code span can't carry those styles anyway, so the merge
|
|
195
|
+
loses nothing. Runs with different links stay apart; the link brackets
|
|
196
|
+
already separate them."""
|
|
197
|
+
merged: list = []
|
|
198
|
+
for item in items:
|
|
199
|
+
prev = merged[-1] if merged else None
|
|
200
|
+
if (
|
|
201
|
+
_is_code_run(item)
|
|
202
|
+
and prev is not None
|
|
203
|
+
and _is_code_run(prev)
|
|
204
|
+
and prev.link_url == item.link_url
|
|
205
|
+
):
|
|
206
|
+
merged[-1] = Run(
|
|
207
|
+
text=prev.text + item.text, font_family="Courier New", link_url=prev.link_url
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
merged.append(item)
|
|
211
|
+
return merged
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _render_run(run: Run, in_table: bool = False) -> str:
|
|
215
|
+
"""Markdown for one run. `in_table` makes the result safe inside a
|
|
216
|
+
pipe-table cell (see `_table_safe`)."""
|
|
183
217
|
if run.text == "":
|
|
184
218
|
return ""
|
|
185
219
|
if run.font_family == "Courier New":
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
220
|
+
return _render_code_run(run, in_table)
|
|
221
|
+
body = _render_plain_run(run)
|
|
222
|
+
return _table_safe(body) if in_table else body
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _render_code_run(run: Run, in_table: bool) -> str:
|
|
226
|
+
# Line breaks at either end go outside the link, so a cell's paragraph
|
|
227
|
+
# break (a leading "\n" from doc_to_ast) or a trailing soft break doesn't
|
|
228
|
+
# render as a <br> inside the <a>.
|
|
229
|
+
inner = run.text.strip("\n")
|
|
230
|
+
if not inner:
|
|
231
|
+
return "<br>" * len(run.text)
|
|
232
|
+
lead = "<br>" * (len(run.text) - len(run.text.lstrip("\n")))
|
|
233
|
+
trail = "<br>" * (len(run.text) - len(run.text.rstrip("\n")))
|
|
234
|
+
body = _render_code_run_text(inner, in_table)
|
|
235
|
+
if run.link_url:
|
|
236
|
+
dest = _md_link_dest(run.link_url)
|
|
237
|
+
body = f"[{body}]({_table_safe(dest) if in_table else dest})"
|
|
238
|
+
return f"{lead}{body}{trail}"
|
|
239
|
+
|
|
197
240
|
|
|
241
|
+
def _render_plain_run(run: Run) -> str:
|
|
198
242
|
# CommonMark emphasis delimiters can't have whitespace on the inner side
|
|
199
243
|
# (e.g. "**Bold **" doesn't close — the space right before "**" means it
|
|
200
244
|
# renders as literal asterisks, not bold). Keep leading/trailing
|
|
@@ -220,7 +264,64 @@ def _render_run(run: Run) -> str:
|
|
|
220
264
|
|
|
221
265
|
|
|
222
266
|
def _escape(text: str) -> str:
|
|
223
|
-
return _MD_ESCAPE.sub(
|
|
267
|
+
return _MD_ESCAPE.sub(lambda m: "\\" + m.group(), text)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _longest_backtick_run(text: str) -> int:
|
|
271
|
+
return max((len(match.group()) for match in _BACKTICK_RUN.finditer(text)), default=0)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _fenced_code_block(text: str) -> str:
|
|
275
|
+
# A fence must be longer than any backtick run in the content, or a
|
|
276
|
+
# literal ``` line inside the block would close it early.
|
|
277
|
+
fence = "`" * max(3, _longest_backtick_run(text) + 1)
|
|
278
|
+
return f"{fence}\n{text}\n{fence}"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _code_span(text: str) -> str:
|
|
282
|
+
"""One CommonMark code span for `text` (which has no newline).
|
|
283
|
+
|
|
284
|
+
A code span's content is literal: backslash escapes don't work inside
|
|
285
|
+
it, so the only tools are the delimiter length and padding. The
|
|
286
|
+
delimiter is one backtick longer than the longest run in the content, so
|
|
287
|
+
no literal run can close the span. Padding is exactly one space a side
|
|
288
|
+
because CommonMark strips exactly one space from each end when both ends
|
|
289
|
+
have one (and the content isn't all spaces). That stripping is what
|
|
290
|
+
separates an edge backtick from the delimiter, and it would also eat a
|
|
291
|
+
real leading and trailing space, so both cases get padded."""
|
|
292
|
+
fence = "`" * (_longest_backtick_run(text) + 1)
|
|
293
|
+
pad = text.startswith("`") or text.endswith("`")
|
|
294
|
+
pad = pad or (text.startswith(" ") and text.endswith(" ") and text.strip(" ") != "")
|
|
295
|
+
if pad:
|
|
296
|
+
text = f" {text} "
|
|
297
|
+
return f"{fence}{text}{fence}"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _render_code_run_text(text: str, in_table: bool) -> str:
|
|
301
|
+
"""Inline code for `text`, which may span lines.
|
|
302
|
+
|
|
303
|
+
A code span can't hold a line break: CommonMark turns one into a space,
|
|
304
|
+
and in a paragraph a raw newline can start a new block (a "# " heading,
|
|
305
|
+
a "- " item) before the span is parsed. So each line becomes its own
|
|
306
|
+
span, joined by <br> the same way a table cell joins its lines.
|
|
307
|
+
|
|
308
|
+
In a table cell, a "|" splits the cell before inline parsing, and the
|
|
309
|
+
GFM `\\|` escape inside a code span shows its backslash in renderers
|
|
310
|
+
that don't implement that rule. A line holding a "|" is written as raw
|
|
311
|
+
HTML <code> instead, which no table parser splits on. Text inside raw
|
|
312
|
+
HTML is still parsed as Markdown, so every ASCII punctuation character
|
|
313
|
+
becomes a numeric entity: an entity is never Markdown syntax, and a
|
|
314
|
+
backslash can't escape the "&" that starts one."""
|
|
315
|
+
spans: list[str] = []
|
|
316
|
+
for line in text.split("\n"):
|
|
317
|
+
if not line:
|
|
318
|
+
spans.append("")
|
|
319
|
+
elif in_table and "|" in line:
|
|
320
|
+
escaped = "".join(f"&#{ord(c)};" if c in string.punctuation else c for c in line)
|
|
321
|
+
spans.append(f"<code>{escaped}</code>")
|
|
322
|
+
else:
|
|
323
|
+
spans.append(_code_span(line))
|
|
324
|
+
return "<br>".join(spans)
|
|
224
325
|
|
|
225
326
|
|
|
226
327
|
def _render_table(table: Table) -> str:
|
|
@@ -249,29 +350,38 @@ def _render_table(table: Table) -> str:
|
|
|
249
350
|
|
|
250
351
|
def _render_cell(cell: Cell) -> str:
|
|
251
352
|
parts: list[str] = []
|
|
252
|
-
for child in cell.children:
|
|
353
|
+
for child in _coalesce_code_runs(cell.children):
|
|
253
354
|
if isinstance(child, Table):
|
|
254
355
|
parts.append(_NESTED_TABLE_PLACEHOLDER)
|
|
255
356
|
elif isinstance(child, Image):
|
|
256
|
-
parts.append(f"})")
|
|
357
|
+
parts.append(_table_safe(f"})"))
|
|
257
358
|
else:
|
|
258
|
-
parts.append(_render_run(child))
|
|
259
|
-
#
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
359
|
+
parts.append(_render_run(child, in_table=True))
|
|
360
|
+
# Every part is already table-safe, with its newlines turned into <br>,
|
|
361
|
+
# so strip() here only removes incidental whitespace, never an edge
|
|
362
|
+
# spacer paragraph (#594 QA round 2; see _table_safe).
|
|
363
|
+
return "".join(parts).strip()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _table_safe(text: str) -> str:
|
|
367
|
+
"""Make rendered non-code Markdown safe inside a pipe-table cell.
|
|
368
|
+
|
|
369
|
+
Pipe-table cells are single-line: a literal newline (from a multi-
|
|
370
|
+
paragraph cell, see doc_to_ast.py's _cell_content_to_children) would
|
|
371
|
+
break the table's row structure, so it becomes a <br> instead. "|" is
|
|
372
|
+
escaped for the same structural reason.
|
|
373
|
+
|
|
374
|
+
This runs per part, before _render_cell's strip(), not on the joined
|
|
375
|
+
cell text after it (#594 QA round 2): a spacer paragraph at the very
|
|
376
|
+
start or end of a cell shows up as a leading/trailing "\\n", and
|
|
377
|
+
stripping first silently ate exactly that newline. "<br>" is not
|
|
378
|
+
whitespace, so converting first keeps the spacer.
|
|
379
|
+
|
|
380
|
+
Code runs never go through this: _render_code_run_text writes its own
|
|
381
|
+
<br> and table-safe <code>, and escaping its output again would put a
|
|
382
|
+
literal backslash inside the code (#938). Only a code link's
|
|
383
|
+
destination is passed through here."""
|
|
384
|
+
return text.replace("\n", "<br>").replace("|", "\\|")
|
|
275
385
|
|
|
276
386
|
|
|
277
387
|
def _render_comment(comment: dict) -> str:
|
{mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_markdown_export.py
RENAMED
|
@@ -7,6 +7,7 @@ from xml.etree import ElementTree
|
|
|
7
7
|
|
|
8
8
|
import markdown
|
|
9
9
|
import pytest
|
|
10
|
+
from markdown_it import MarkdownIt
|
|
10
11
|
|
|
11
12
|
from mcp_gee_sweet.tools import docs as docs_module
|
|
12
13
|
from mcp_gee_sweet.tools.docs.ast import (
|
|
@@ -21,6 +22,16 @@ from mcp_gee_sweet.tools.docs.ast import (
|
|
|
21
22
|
from mcp_gee_sweet.tools.docs.ast_to_markdown import ast_to_markdown
|
|
22
23
|
from mcp_gee_sweet.tools.docs.doc_to_ast import document_to_ast
|
|
23
24
|
|
|
25
|
+
# Strict CommonMark, plus GFM pipe tables. Python-Markdown (used by some older
|
|
26
|
+
# tests below) strips whitespace inside code spans and closes a fence only on an
|
|
27
|
+
# exact-length match, so it can't catch padding or fence-length regressions
|
|
28
|
+
# (#938).
|
|
29
|
+
_COMMONMARK = MarkdownIt("commonmark").enable("table")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _code(text, **kwargs):
|
|
33
|
+
return Run(text=text, font_family="Courier New", **kwargs)
|
|
34
|
+
|
|
24
35
|
|
|
25
36
|
@pytest.mark.parametrize("ticks", [3, 4, 6])
|
|
26
37
|
@pytest.mark.parametrize("interleaved_image", [False, True])
|
|
@@ -30,12 +41,14 @@ def test_code_block_preserves_literal_backtick_fences(ticks, interleaved_image):
|
|
|
30
41
|
if interleaved_image:
|
|
31
42
|
runs += [Image(src="https://example.com/image.png"), *runs]
|
|
32
43
|
rendered = ast_to_markdown([Paragraph(runs=runs)])
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
blocks =
|
|
44
|
+
assert rendered.startswith("`" * (ticks + 1) + "\nbefore\n")
|
|
45
|
+
tokens = _COMMONMARK.parse(rendered)
|
|
46
|
+
blocks = [t.content for t in tokens if t.type == "fence"]
|
|
36
47
|
expected = [content + "\n"] * (2 if interleaved_image else 1)
|
|
37
|
-
assert
|
|
38
|
-
assert
|
|
48
|
+
assert blocks == expected
|
|
49
|
+
assert sum(t.type == "image" for t in tokens for t in (t.children or [])) == int(
|
|
50
|
+
interleaved_image
|
|
51
|
+
)
|
|
39
52
|
|
|
40
53
|
|
|
41
54
|
def _para(text, style=None, bullet=None, para_style=None):
|
|
@@ -957,3 +970,169 @@ def test_inline_code_backticks_survive_markdown_rendering(text, link_url):
|
|
|
957
970
|
assert code.text == text
|
|
958
971
|
if link_url:
|
|
959
972
|
assert rendered.find("a").attrib["href"] == link_url
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def _inline_code(markdown_text):
|
|
976
|
+
"""Each code span's content in the first paragraph, as strict CommonMark parses it."""
|
|
977
|
+
inline = next(t for t in _COMMONMARK.parse(markdown_text) if t.type == "inline")
|
|
978
|
+
return [t.content for t in inline.children if t.type == "code_inline"]
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
class TestInlineCodeCommonMark:
|
|
982
|
+
"""#938: code-span edge cases checked against a strict CommonMark parser."""
|
|
983
|
+
|
|
984
|
+
@pytest.mark.parametrize(
|
|
985
|
+
"text",
|
|
986
|
+
[
|
|
987
|
+
"call(`value`)",
|
|
988
|
+
"`leading",
|
|
989
|
+
"trailing`",
|
|
990
|
+
"``nested``",
|
|
991
|
+
" both ends ",
|
|
992
|
+
" leading only",
|
|
993
|
+
"trailing only ",
|
|
994
|
+
" ",
|
|
995
|
+
" `x` ",
|
|
996
|
+
" double ",
|
|
997
|
+
],
|
|
998
|
+
)
|
|
999
|
+
@pytest.mark.parametrize("link_url", [None, "https://example.com/code"])
|
|
1000
|
+
def test_code_span_content_survives_exactly(self, text, link_url):
|
|
1001
|
+
nodes = [Paragraph(runs=[Run(text="see "), _code(text, link_url=link_url), Run(text=".")])]
|
|
1002
|
+
assert _inline_code(ast_to_markdown(nodes)) == [text]
|
|
1003
|
+
|
|
1004
|
+
def test_adjacent_code_runs_merge_into_one_span(self):
|
|
1005
|
+
# Docs splits a run at a style change; Markdown can't show bold code.
|
|
1006
|
+
nodes = [Paragraph(runs=[Run(text="a "), _code("x`"), _code("`y", bold=True)])]
|
|
1007
|
+
assert _inline_code(ast_to_markdown(nodes)) == ["x``y"]
|
|
1008
|
+
|
|
1009
|
+
def test_adjacent_code_runs_with_same_link_merge(self):
|
|
1010
|
+
url = "https://example.com"
|
|
1011
|
+
nodes = [
|
|
1012
|
+
Paragraph(runs=[Run(text="a "), _code("x", link_url=url), _code("y", link_url=url)])
|
|
1013
|
+
]
|
|
1014
|
+
assert ast_to_markdown(nodes) == f"a [`xy`]({url})\n"
|
|
1015
|
+
|
|
1016
|
+
def test_adjacent_code_runs_with_different_links_stay_separate(self):
|
|
1017
|
+
nodes = [
|
|
1018
|
+
Paragraph(
|
|
1019
|
+
runs=[
|
|
1020
|
+
Run(text="a "),
|
|
1021
|
+
_code("x", link_url="https://a.example"),
|
|
1022
|
+
_code("y", link_url="https://b.example"),
|
|
1023
|
+
_code("z"),
|
|
1024
|
+
]
|
|
1025
|
+
)
|
|
1026
|
+
]
|
|
1027
|
+
assert _inline_code(ast_to_markdown(nodes)) == ["x", "y", "z"]
|
|
1028
|
+
|
|
1029
|
+
def test_code_run_merge_does_not_mutate_input(self):
|
|
1030
|
+
first, second = _code("x"), _code("y")
|
|
1031
|
+
ast_to_markdown([Paragraph(runs=[Run(text="a "), first, second])])
|
|
1032
|
+
assert (first.text, second.text) == ("x", "y")
|
|
1033
|
+
|
|
1034
|
+
def test_newline_in_inline_code_does_not_start_a_new_block(self):
|
|
1035
|
+
nodes = [Paragraph(runs=[Run(text="a "), _code("x\n# y\n- z\n\nw"), Run(text=" b")])]
|
|
1036
|
+
rendered = ast_to_markdown(nodes)
|
|
1037
|
+
tokens = _COMMONMARK.parse(rendered)
|
|
1038
|
+
assert [t.type for t in tokens] == ["paragraph_open", "inline", "paragraph_close"]
|
|
1039
|
+
assert _inline_code(rendered) == ["x", "# y", "- z", "w"]
|
|
1040
|
+
assert rendered == "a `x`<br>`# y`<br>`- z`<br><br>`w` b\n"
|
|
1041
|
+
|
|
1042
|
+
def test_newline_in_inline_code_inside_blockquote_stays_one_line(self):
|
|
1043
|
+
nodes = [Paragraph(runs=[Run(text="a "), _code("x\ny")], blockquote_depth=1)]
|
|
1044
|
+
assert ast_to_markdown(nodes) == "> a `x`<br>`y`\n"
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
class TestTableCellCodeCommonMark:
|
|
1048
|
+
"""#938: inline code inside a GFM pipe-table cell."""
|
|
1049
|
+
|
|
1050
|
+
def _render_cell(self, children):
|
|
1051
|
+
from mcp_gee_sweet.tools.docs.ast import Cell, Row
|
|
1052
|
+
|
|
1053
|
+
table = Table(
|
|
1054
|
+
rows=[
|
|
1055
|
+
Row(cells=[Cell(children=[Run(text="h")])]),
|
|
1056
|
+
Row(cells=[Cell(children=children)]),
|
|
1057
|
+
]
|
|
1058
|
+
)
|
|
1059
|
+
rendered = ast_to_markdown([table])
|
|
1060
|
+
html = _COMMONMARK.render(rendered)
|
|
1061
|
+
return rendered, html[html.index("<td>") + 4 : html.index("</td>")]
|
|
1062
|
+
|
|
1063
|
+
def test_pipe_in_code_has_no_backslash(self):
|
|
1064
|
+
rendered, cell = self._render_cell([_code("a|b")])
|
|
1065
|
+
assert "\\" not in rendered
|
|
1066
|
+
assert cell == "<code>a|b</code>"
|
|
1067
|
+
|
|
1068
|
+
def test_pipe_code_escapes_html_special_characters(self):
|
|
1069
|
+
_, cell = self._render_cell([_code("<a> & b|c")])
|
|
1070
|
+
assert cell == "<code><a> & b|c</code>"
|
|
1071
|
+
|
|
1072
|
+
def test_code_without_pipe_stays_a_code_span(self):
|
|
1073
|
+
rendered, cell = self._render_cell([_code(" `x` ")])
|
|
1074
|
+
assert "| `` `x` `` |" in rendered
|
|
1075
|
+
assert cell == "<code> `x` </code>"
|
|
1076
|
+
|
|
1077
|
+
def test_newline_in_code_becomes_br_outside_the_code(self):
|
|
1078
|
+
_, cell = self._render_cell([Run(text="t "), _code("x\ny")])
|
|
1079
|
+
assert cell == "t <code>x</code><br><code>y</code>"
|
|
1080
|
+
|
|
1081
|
+
def test_code_paragraphs_in_one_cell_join_with_br(self):
|
|
1082
|
+
# doc_to_ast prefixes the second paragraph's first run with "\n".
|
|
1083
|
+
_, cell = self._render_cell([_code("x"), _code("\ny|z")])
|
|
1084
|
+
assert cell == "<code>x</code><br><code>y|z</code>"
|
|
1085
|
+
|
|
1086
|
+
def test_plain_pipe_next_to_code_is_still_escaped(self):
|
|
1087
|
+
_, cell = self._render_cell([Run(text="a|b "), _code("c")])
|
|
1088
|
+
assert cell == "a|b <code>c</code>"
|
|
1089
|
+
|
|
1090
|
+
def test_linked_code_with_pipe_in_url_stays_in_one_cell(self):
|
|
1091
|
+
rendered, cell = self._render_cell([_code("c", link_url="https://e.example/a|b")])
|
|
1092
|
+
assert rendered.splitlines()[2].count("|") - rendered.splitlines()[2].count("\\|") == 2
|
|
1093
|
+
assert cell == '<a href="https://e.example/a%7Cb"><code>c</code></a>'
|
|
1094
|
+
|
|
1095
|
+
def test_pipe_code_keeps_markdown_punctuation_literal(self):
|
|
1096
|
+
# Text inside raw <code> is still parsed as Markdown (PR #968 QA round 1).
|
|
1097
|
+
text = "a|*b* _c_ [x](y) ~~d~~ `e` <i> &"
|
|
1098
|
+
_, cell = self._render_cell([_code(text)])
|
|
1099
|
+
assert cell == "<code>a|*b* _c_ [x](y) ~~d~~ `e` <i> &amp;</code>"
|
|
1100
|
+
|
|
1101
|
+
def test_backslash_before_pipe_in_code_survives(self):
|
|
1102
|
+
_, cell = self._render_cell([_code("C:\\|x")])
|
|
1103
|
+
assert cell == "<code>C:\\|x</code>"
|
|
1104
|
+
|
|
1105
|
+
def test_linked_code_paragraph_break_lands_outside_the_link(self):
|
|
1106
|
+
url = "https://e.example"
|
|
1107
|
+
_, cell = self._render_cell([_code("x"), _code("\nb", link_url=url)])
|
|
1108
|
+
assert cell == f'<code>x</code><br><a href="{url}"><code>b</code></a>'
|
|
1109
|
+
|
|
1110
|
+
def test_plain_run_rendered_in_table_is_table_safe(self):
|
|
1111
|
+
from mcp_gee_sweet.tools.docs.ast_to_markdown import _render_run
|
|
1112
|
+
|
|
1113
|
+
assert _render_run(Run(text="a|b\nc"), in_table=True) == "a\\|b<br>c"
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
class TestPlainTextEscapingCommonMark:
|
|
1117
|
+
"""PR #968 QA round 1: plain text that looks like HTML, an entity, or GFM
|
|
1118
|
+
strikethrough must export as literal text."""
|
|
1119
|
+
|
|
1120
|
+
def test_html_entity_and_tilde_render_literally(self):
|
|
1121
|
+
text = "Use <br> or | and & and ~~old~~ </p> <!-- x -->"
|
|
1122
|
+
rendered = ast_to_markdown([Paragraph(runs=[Run(text=text)])])
|
|
1123
|
+
inline = next(t for t in _COMMONMARK.parse(rendered) if t.type == "inline")
|
|
1124
|
+
assert [t.type for t in inline.children] == ["text"]
|
|
1125
|
+
assert inline.children[0].content == text
|
|
1126
|
+
|
|
1127
|
+
def test_prose_ampersand_and_less_than_stay_unescaped(self):
|
|
1128
|
+
rendered = ast_to_markdown([Paragraph(runs=[Run(text="AT&T: a < b & c")])])
|
|
1129
|
+
assert rendered == "AT&T: a < b & c\n"
|
|
1130
|
+
|
|
1131
|
+
def test_linked_code_trailing_soft_break_lands_outside_the_link(self):
|
|
1132
|
+
url = "https://e.example"
|
|
1133
|
+
nodes = [Paragraph(runs=[Run(text="a "), _code("x\n", link_url=url), Run(text="b")])]
|
|
1134
|
+
rendered = ast_to_markdown(nodes)
|
|
1135
|
+
assert rendered == f"a [`x`]({url})<br>b\n"
|
|
1136
|
+
assert _COMMONMARK.render(rendered) == (
|
|
1137
|
+
f'<p>a <a href="{url}"><code>x</code></a><br>b</p>\n'
|
|
1138
|
+
)
|