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.
Files changed (253) hide show
  1. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/kai.md +1 -1
  2. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/PKG-INFO +1 -1
  3. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/doc-to-markdown.md +17 -0
  4. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/docs_content.md +63 -0
  5. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/roadmap.md +1 -1
  6. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/pyproject.toml +1 -0
  7. {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
  8. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_markdown_export.py +184 -5
  9. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/uv.lock +24 -1
  10. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/cleanup-worktrees.md +0 -0
  11. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/merge-pr.md +0 -0
  12. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/notify-kai.md +0 -0
  13. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/notify-partner.md +0 -0
  14. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/orchestrator.md +0 -0
  15. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/prep-for-pr.md +0 -0
  16. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/qa-kickoff.md +0 -0
  17. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/release.md +0 -0
  18. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/retro.md +0 -0
  19. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/roadmap-status.md +0 -0
  20. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/start-worktree.md +0 -0
  21. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/team-member.md +0 -0
  22. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/verify-pr.md +0 -0
  23. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/settings.json +0 -0
  24. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/amy.md +0 -0
  25. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/aziz.md +0 -0
  26. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/bob.md +0 -0
  27. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/dev.md +0 -0
  28. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/joy.md +0 -0
  29. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/qa.md +0 -0
  30. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.dockerignore +0 -0
  31. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  32. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  33. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  34. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/dependabot.yml +0 -0
  35. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/workflows/ci.yml +0 -0
  36. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/workflows/docs.yml +0 -0
  37. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/workflows/publish-dev.yml +0 -0
  38. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.github/workflows/release.yml +0 -0
  39. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.gitignore +0 -0
  40. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.pre-commit-config.yaml +0 -0
  41. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/.python-version +0 -0
  42. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/CHANGELOG.md +0 -0
  43. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/CLAUDE.md +0 -0
  44. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/CODE_OF_CONDUCT.md +0 -0
  45. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/CONTRIBUTING.md +0 -0
  46. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/Dockerfile +0 -0
  47. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/LICENSE +0 -0
  48. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/Makefile +0 -0
  49. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/README.md +0 -0
  50. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/SECURITY.md +0 -0
  51. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docker-compose.yml +0 -0
  52. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/assets/logo.png +0 -0
  53. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/auth.md +0 -0
  54. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/client-setup.md +0 -0
  55. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/configuration.md +0 -0
  56. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-async-tool-execution.md +0 -0
  57. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
  58. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-cache-invalidation.md +0 -0
  59. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-chart-theming.md +0 -0
  60. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-comments-first-class.md +0 -0
  61. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-composite-tools.md +0 -0
  62. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
  63. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
  64. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-dev-team.md +0 -0
  65. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-docs-formatting.md +0 -0
  66. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-fork.md +0 -0
  67. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-gmail-domain.md +0 -0
  68. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  69. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-local-fs-test-harness.md +0 -0
  70. {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
  71. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-pillow-image-dependency.md +0 -0
  72. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-prompt-qa-role.md +0 -0
  73. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-publishing.md +0 -0
  74. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-release-docs-roles.md +0 -0
  75. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-repositioning.md +0 -0
  76. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  77. {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
  78. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-review-finding-ticket-bar.md +0 -0
  79. {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
  80. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-sync-folder-recursion.md +0 -0
  81. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-testing.md +0 -0
  82. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-tool-error-visibility.md +0 -0
  83. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/index.md +0 -0
  84. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/role-file-history.md +0 -0
  85. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/blockquote-representation.md +0 -0
  86. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/borderless-table-columns.md +0 -0
  87. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/docs-ast-pipeline.md +0 -0
  88. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/effectiveformat-spike.md +0 -0
  89. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/heading-anchor-resolution.md +0 -0
  90. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/image-conversion.md +0 -0
  91. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/index.md +0 -0
  92. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/markdown-support.md +0 -0
  93. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/module-history.md +0 -0
  94. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/oauth-reauthorize-from-tool-call.md +0 -0
  95. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/phase3-theme-system.md +0 -0
  96. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design/sse-consent-off-event-loop.md +0 -0
  97. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/design.md +0 -0
  98. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/index.md +0 -0
  99. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/known-limitations.md +0 -0
  100. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/notes-read.md +0 -0
  101. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/README.md +0 -0
  102. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
  103. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  104. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  105. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  106. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  107. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
  108. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
  109. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
  110. {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
  111. {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
  112. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
  113. {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
  114. {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
  115. {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
  116. {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
  117. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
  118. {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
  119. {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
  120. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures.template.md +0 -0
  121. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/gmail-test-plan.md +0 -0
  122. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/operations.yaml +0 -0
  123. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/playwright_oauth.md +0 -0
  124. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/.gitkeep +0 -0
  125. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-02.md +0 -0
  126. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-14.md +0 -0
  127. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-17.md +0 -0
  128. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-18.md +0 -0
  129. {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
  130. {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
  131. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-28.md +0 -0
  132. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04-docs.md +0 -0
  133. {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
  134. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04-infra.md +0 -0
  135. {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
  136. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04.md +0 -0
  137. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-09-04.md +0 -0
  138. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/retro-v0.8.0.md +0 -0
  139. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/retro-v0.9.0.md +0 -0
  140. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/run.md +0 -0
  141. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/runs/README.md +0 -0
  142. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/runs/v0.8.0.md +0 -0
  143. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/runs/v0.8.1.md +0 -0
  144. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/runs/v0.9.0.md +0 -0
  145. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/setup.md +0 -0
  146. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/calendar.md +0 -0
  147. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/docs_layout.md +0 -0
  148. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/docs_style.md +0 -0
  149. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/docs_tables.md +0 -0
  150. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/drive_activity.md +0 -0
  151. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/drive_files.md +0 -0
  152. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/drive_sharing.md +0 -0
  153. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/drive_transfer.md +0 -0
  154. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/gmail.md +0 -0
  155. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/infra.md +0 -0
  156. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/sheets_charts.md +0 -0
  157. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/sheets_mgmt.md +0 -0
  158. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/sheets_read.md +0 -0
  159. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/sheets_write.md +0 -0
  160. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/style-guide.md +0 -0
  161. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/docs/tools.md +0 -0
  162. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/mkdocs.yml +0 -0
  163. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/pyrightconfig.json +0 -0
  164. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/check_supports_all_drives.py +0 -0
  165. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/gen_tool_docs.py +0 -0
  166. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/lane_context_hook.py +0 -0
  167. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/oauth_setup.py +0 -0
  168. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/qa_cancel_insert_local_images.py +0 -0
  169. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/qa_gmail_fixtures.py +0 -0
  170. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/setup_team.sh +0 -0
  171. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/scripts/write_team_mcp_configs.sh +0 -0
  172. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/server.json +0 -0
  173. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/.env.template +0 -0
  174. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/__init__.py +0 -0
  175. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/auth.py +0 -0
  176. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/cache.py +0 -0
  177. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/http_transport.py +0 -0
  178. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/reauth.py +0 -0
  179. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/server.py +0 -0
  180. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/sse_shutdown.py +0 -0
  181. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  182. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/cache.py +0 -0
  183. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/calendar.py +0 -0
  184. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
  185. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  186. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
  187. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
  188. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
  189. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
  190. {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
  191. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
  192. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
  193. {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
  194. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/images.py +0 -0
  195. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
  196. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  197. {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
  198. {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
  199. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  200. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  201. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  202. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  203. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
  204. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  205. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  206. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/gmail.py +0 -0
  207. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/pagination.py +0 -0
  208. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  209. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  210. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
  211. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
  212. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
  213. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/__init__.py +0 -0
  214. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/conftest.py +0 -0
  215. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/__init__.py +0 -0
  216. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/test_activity.py +0 -0
  217. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/test_files.py +0 -0
  218. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/test_sharing.py +0 -0
  219. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/test_transfer.py +0 -0
  220. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/fakes.py +0 -0
  221. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/integration/__init__.py +0 -0
  222. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/integration/conftest.py +0 -0
  223. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/integration/test_local_fs_harness.py +0 -0
  224. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/sheets/__init__.py +0 -0
  225. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/sheets/test_data.py +0 -0
  226. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/sheets/test_helpers.py +0 -0
  227. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/sheets/test_structure.py +0 -0
  228. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_auth.py +0 -0
  229. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_cache.py +0 -0
  230. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_calendar.py +0 -0
  231. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_check_supports_all_drives.py +0 -0
  232. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_concurrency.py +0 -0
  233. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_anchors.py +0 -0
  234. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_comments.py +0 -0
  235. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_content.py +0 -0
  236. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_core.py +0 -0
  237. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_editing.py +0 -0
  238. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_images.py +0 -0
  239. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_indices.py +0 -0
  240. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_layout.py +0 -0
  241. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_named_ranges.py +0 -0
  242. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_style.py +0 -0
  243. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_tables.py +0 -0
  244. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_gen_tool_docs.py +0 -0
  245. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_gmail.py +0 -0
  246. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_lane_context_hook.py +0 -0
  247. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_qa_gmail_fixtures.py +0 -0
  248. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_reauth.py +0 -0
  249. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_response_limits.py +0 -0
  250. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_server.py +0 -0
  251. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_server_json.py +0 -0
  252. {mcp_gee_sweet-0.9.2.dev286 → mcp_gee_sweet-0.9.2.dev289}/tests/test_sse_shutdown.py +0 -0
  253. {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.dev286
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
+ (`&#124;`, `&#42;`, `&#92;`, ...). 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&#124;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&#124;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&#124;b</code> and a\|b |` and `` | `x`<br><code>y&#124;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&#124;*b* _c_</code>`, which renders `<em>` inside the `<code>`. A cell with code `C:\|x` exported as `<code>C:\&#124;x</code>`, which renders as `C:&amp;#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 &lt;br&gt; or &amp;#124; and ~~old~~, AT&amp;T a &lt; b</p>
3589
+ ```
3590
+
3591
+ **Prompt**
3592
+ > "Export doc {DOC_ID} as Markdown"
3593
+
3594
+ **Checks**
3595
+ - `markdown` contains the row `| <code>a&#124;&#42;b&#42; &#95;c&#95;</code> |`: every punctuation character inside raw `<code>` is a numeric entity
3596
+ - `markdown` contains the row `| <code>C&#58;&#92;&#124;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 \&#124; 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 &lt;br&gt; or &amp;#124; and ~~old~~, AT&amp;T a &lt; 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
- - [ ] **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 ([#894](https://github.com/khuisman/mcp-gee-sweet/issues/894))
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))
@@ -97,4 +97,5 @@ dev = [
97
97
  "pytest-cov>=7.1.0",
98
98
  "ruff>=0.16.5",
99
99
  "tomli>=2.0.1; python_version < '3.11'",
100
+ "markdown-it-py>=3.0.0",
100
101
  ]
@@ -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
- _MD_ESCAPE = re.compile(r"([\\`*_\[\]])")
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(fenced(buffer))
111
+ segments.append(_fenced_code_block(buffer))
109
112
  buffer = ""
110
113
  segments.append(f"![{_escape(item.alt or '')}]({_md_link_dest(item.src)})")
111
114
  if buffer:
112
- segments.append(fenced(buffer))
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"![{_escape(item.alt or '')}]({_md_link_dest(item.src)})")
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 _render_run(run: Run) -> str:
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
- # Use a delimiter longer than any literal backtick sequence in the
187
- # content, and separate edge backticks from that delimiter.
188
- ticks = (
189
- max((len(match.group()) for match in _BACKTICK_RUN.finditer(run.text)), default=0) + 1
190
- )
191
- fence = "`" * ticks
192
- content = run.text
193
- if content.startswith("`") or content.endswith("`"):
194
- content = f" {content} "
195
- body = f"{fence}{content}{fence}"
196
- return f"[{body}]({_md_link_dest(run.link_url)})" if run.link_url else body
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(r"\\\1", text)
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"![{_escape(child.alt or '')}]({_md_link_dest(child.src)})")
357
+ parts.append(_table_safe(f"![{_escape(child.alt or '')}]({_md_link_dest(child.src)})"))
257
358
  else:
258
- parts.append(_render_run(child))
259
- # Pipe-table cells are single-line: a literal newline (from a multi-
260
- # paragraph cell, see doc_to_ast.py's _cell_content_to_children) would
261
- # break the table's row structure, so it becomes a <br> instead. "|" is
262
- # escaped for the same structural reason.
263
- #
264
- # \n -> <br> conversion must happen BEFORE strip(), not after (#594 QA
265
- # round 2): a spacer paragraph at the very start or end of a cell (as
266
- # opposed to one sandwiched between two real lines) shows up here as a
267
- # leading/trailing "\n" on the joined text — stripping first, as an
268
- # earlier version of this function did, silently ate exactly that
269
- # newline, making an edge spacer indistinguishable from a cell with no
270
- # spacer at all. "<br>" is not whitespace, so converting first means
271
- # strip() only removes genuine incidental whitespace, never a spacer
272
- # that's already been turned into a <br> marker.
273
- text = "".join(parts).replace("\n", "<br>").replace("|", "\\|")
274
- return text.strip()
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:
@@ -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
- html = markdown.markdown(rendered, extensions=["fenced_code"])
34
- root = ElementTree.fromstring(f"<root>{html}</root>")
35
- blocks = root.findall("pre/code")
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 [block.text for block in blocks] == expected
38
- assert len(root.findall("p/img")) == int(interleaved_image)
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>&lt;a&gt; &amp; 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> &amp;"
1098
+ _, cell = self._render_cell([_code(text)])
1099
+ assert cell == "<code>a|*b* _c_ [x](y) ~~d~~ `e` &lt;i&gt; &amp;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 &#124; and &amp; 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
+ )