mcp-gee-sweet 0.9.6.dev134__tar.gz → 0.9.6.dev136__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 (400) hide show
  1. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/CLAUDE.md +4 -3
  2. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/PKG-INFO +1 -1
  3. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/known-limitations.md +29 -4
  4. mcp_gee_sweet-0.9.6.dev136/docs/qa/fixtures/md/tc-doc246/native-lines.html +4 -0
  5. mcp_gee_sweet-0.9.6.dev136/docs/qa/fixtures/md/tc-doc246/rules.md +11 -0
  6. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/docs_content.md +41 -3
  7. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/roadmap.md +4 -4
  8. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/ast.py +5 -0
  9. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +65 -25
  10. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +93 -4
  11. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/emitter.py +87 -6
  12. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/html_parser.py +25 -28
  13. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_core.py +16 -4
  14. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_markdown_export.py +57 -0
  15. mcp_gee_sweet-0.9.6.dev136/tests/test_docs_rule.py +403 -0
  16. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/cleanup-worktrees.md +0 -0
  17. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/merge-pr.md +0 -0
  18. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/notify-kai.md +0 -0
  19. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/notify-partner.md +0 -0
  20. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/orchestrator.md +0 -0
  21. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/prep-for-pr.md +0 -0
  22. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/qa-kickoff.md +0 -0
  23. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/release.md +0 -0
  24. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/retro.md +0 -0
  25. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/roadmap-status.md +0 -0
  26. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/security-advisory.md +0 -0
  27. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/start-worktree.md +0 -0
  28. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/team-member.md +0 -0
  29. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/commands/verify-pr.md +0 -0
  30. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/settings.json +0 -0
  31. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/team-roles/amy.md +0 -0
  32. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/team-roles/aziz.md +0 -0
  33. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/team-roles/bob.md +0 -0
  34. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/team-roles/dev.md +0 -0
  35. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/team-roles/joy.md +0 -0
  36. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/team-roles/kai.md +0 -0
  37. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/team-roles/qa.md +0 -0
  38. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.claude/team-roles/rio.md +0 -0
  39. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.dockerignore +0 -0
  40. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.gitattributes +0 -0
  41. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  42. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  43. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  44. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.github/dependabot.yml +0 -0
  45. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.github/workflows/ci.yml +0 -0
  46. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.github/workflows/docs.yml +0 -0
  47. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.github/workflows/mcpb.yml +0 -0
  48. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.github/workflows/publish.yml +0 -0
  49. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.gitignore +0 -0
  50. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.mcpbignore +0 -0
  51. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.pre-commit-config.yaml +0 -0
  52. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/.python-version +0 -0
  53. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/CHANGELOG.md +0 -0
  54. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/CODE_OF_CONDUCT.md +0 -0
  55. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/CONTRIBUTING.md +0 -0
  56. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/Dockerfile +0 -0
  57. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/LICENSE +0 -0
  58. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/Makefile +0 -0
  59. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/README.md +0 -0
  60. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/SECURITY.md +0 -0
  61. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docker-compose.yml +0 -0
  62. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/assets/logo.png +0 -0
  63. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/auth.md +0 -0
  64. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/client-setup.md +0 -0
  65. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/configuration.md +0 -0
  66. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-async-tool-execution.md +0 -0
  67. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
  68. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-cache-invalidation.md +0 -0
  69. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-chart-theming.md +0 -0
  70. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-comments-first-class.md +0 -0
  71. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-composite-tools.md +0 -0
  72. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
  73. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
  74. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-dev-team.md +0 -0
  75. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-docs-formatting.md +0 -0
  76. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-fork.md +0 -0
  77. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-gmail-domain.md +0 -0
  78. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  79. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-http-socket-timeout.md +0 -0
  80. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-impact-reach-triage.md +0 -0
  81. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-local-fs-test-harness.md +0 -0
  82. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
  83. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-pillow-image-dependency.md +0 -0
  84. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-prompt-qa-role.md +0 -0
  85. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-publishing.md +0 -0
  86. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-release-docs-roles.md +0 -0
  87. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-repositioning.md +0 -0
  88. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  89. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
  90. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-review-finding-ticket-bar.md +0 -0
  91. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
  92. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-sync-folder-recursion.md +0 -0
  93. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-testing.md +0 -0
  94. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/decision-tool-error-visibility.md +0 -0
  95. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/index.md +0 -0
  96. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/decisions/role-file-history.md +0 -0
  97. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/blockquote-representation.md +0 -0
  98. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/borderless-table-columns.md +0 -0
  99. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/cancelled-write-invalidation.md +0 -0
  100. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/doc-replace-backup.md +0 -0
  101. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/doc-revision-export.md +0 -0
  102. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/doc-to-markdown.md +0 -0
  103. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/docs-ast-pipeline.md +0 -0
  104. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/effectiveformat-spike.md +0 -0
  105. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/gmail-send-confirmation.md +0 -0
  106. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/heading-anchor-resolution.md +0 -0
  107. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/image-conversion.md +0 -0
  108. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/image-share-lifecycle.md +0 -0
  109. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/index.md +0 -0
  110. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/local-file-access-policy.md +0 -0
  111. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/markdown-parser.md +0 -0
  112. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/markdown-support.md +0 -0
  113. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/mcp-bundle.md +0 -0
  114. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/module-history.md +0 -0
  115. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/oauth-reauthorize-from-tool-call.md +0 -0
  116. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/phase3-theme-system.md +0 -0
  117. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design/sse-consent-off-event-loop.md +0 -0
  118. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/design.md +0 -0
  119. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/index.md +0 -0
  120. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/notes-read.md +0 -0
  121. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/README.md +0 -0
  122. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r01-meeting-notes.md +0 -0
  123. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r02-project-plan.md +0 -0
  124. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r03-spec-prd.md +0 -0
  125. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r04-readme.md +0 -0
  126. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r05-status-report.md +0 -0
  127. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r06-howto.md +0 -0
  128. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r07-comparison.md +0 -0
  129. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r08-research-summary.md +0 -0
  130. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r09-changelog.md +0 -0
  131. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r10-task-plan.md +0 -0
  132. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r11-incident-postmortem.md +0 -0
  133. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r12-interview-guide.md +0 -0
  134. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r13-api-reference.md +0 -0
  135. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r14-study-notes.md +0 -0
  136. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r15-travel-itinerary.md +0 -0
  137. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/realistic/r16-planning-notes.md +0 -0
  138. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/state/dropped-links.md +0 -0
  139. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/state/theme.json +0 -0
  140. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/state/www-scheme.md +0 -0
  141. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-01-starts-bullets.md +0 -0
  142. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-02-starts-numbered.md +0 -0
  143. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-03-starts-table.md +0 -0
  144. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-04-starts-code.md +0 -0
  145. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-05-starts-quote.md +0 -0
  146. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-06-starts-image.md +0 -0
  147. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-07-starts-rule.md +0 -0
  148. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-08-ends-table.md +0 -0
  149. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-09-ends-list.md +0 -0
  150. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-10-empty.md +0 -0
  151. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-11-whitespace-only.md +0 -0
  152. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-12-no-trailing-newline.md +0 -0
  153. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-13-crlf-line-endings.md +0 -0
  154. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-14-front-matter.md +0 -0
  155. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-15-rule-then-setext-heading.md +0 -0
  156. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/docstart/ds-16-front-matter-lists.md +0 -0
  157. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-adjacency-blank.md +0 -0
  158. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-adjacency-noblank.md +0 -0
  159. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-blockquotes.md +0 -0
  160. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-breaks-rules.md +0 -0
  161. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-code-blocks.md +0 -0
  162. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-containment.md +0 -0
  163. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-emphasis.md +0 -0
  164. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-escapes.md +0 -0
  165. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-headings.md +0 -0
  166. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-images.md +0 -0
  167. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-inline-code.md +0 -0
  168. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-inline-html.md +0 -0
  169. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-inline-matrix.md +0 -0
  170. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-links.md +0 -0
  171. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-lists-blocks.md +0 -0
  172. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-lists-boundaries.md +0 -0
  173. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-lists-markers.md +0 -0
  174. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-lists-nesting.md +0 -0
  175. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-long-document.md +0 -0
  176. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-tables.md +0 -0
  177. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-tasks.md +0 -0
  178. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/syntax/md-unicode.md +0 -0
  179. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/md/tc-doc245/relative-image.md +0 -0
  180. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/qa fixture pixel /303/251.png" +0 -0
  181. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
  182. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  183. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  184. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  185. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  186. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
  187. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
  188. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
  189. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
  190. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
  191. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
  192. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
  193. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
  194. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
  195. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
  196. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
  197. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
  198. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
  199. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc240-bullet.md +0 -0
  200. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc240-heading.md +0 -0
  201. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc240-normal.md +0 -0
  202. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc240-quote.md +0 -0
  203. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc240-seed.md +0 -0
  204. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc241-links.html +0 -0
  205. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc241-links.md +0 -0
  206. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc241-write.md +0 -0
  207. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc242-loose.html +0 -0
  208. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures/tc-doc242-loose.md +0 -0
  209. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/fixtures.template.md +0 -0
  210. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/gmail-test-plan.md +0 -0
  211. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/operations.yaml +0 -0
  212. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/playwright_oauth.md +0 -0
  213. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/.gitkeep +0 -0
  214. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-06-02.md +0 -0
  215. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-06-14.md +0 -0
  216. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-06-17.md +0 -0
  217. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-06-18.md +0 -0
  218. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
  219. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
  220. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-06-28.md +0 -0
  221. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-07-04-docs.md +0 -0
  222. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
  223. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-07-04-infra.md +0 -0
  224. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
  225. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-07-04.md +0 -0
  226. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-09-04.md +0 -0
  227. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-10-17-pass-audit.md +0 -0
  228. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-10-17-spec.txt +0 -0
  229. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-10-17.md +0 -0
  230. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-10-18-markdown-phase2.md +0 -0
  231. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-10-18-markdown.md +0 -0
  232. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-10-22-markdown-boundaries-state.md +0 -0
  233. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-10-25-markdown.md +0 -0
  234. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-10-28-markdown-pr1286.md +0 -0
  235. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-10-29-markdown-pr1295.md +0 -0
  236. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-10-30-markdown-pr1301.md +0 -0
  237. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/results/2026-10-30-markdown-pr1303.md +0 -0
  238. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/retro-v0.8.0.md +0 -0
  239. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/retro-v0.9.0.md +0 -0
  240. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/retro-v0.9.2.md +0 -0
  241. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/run.md +0 -0
  242. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/runs/README.md +0 -0
  243. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/runs/v0.8.0.md +0 -0
  244. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/runs/v0.8.1.md +0 -0
  245. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/runs/v0.9.0.md +0 -0
  246. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/runs/v0.9.1.md +0 -0
  247. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/runs/v0.9.2.md +0 -0
  248. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/runs/v0.9.3.md +0 -0
  249. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/setup.md +0 -0
  250. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/calendar.md +0 -0
  251. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/docs_layout.md +0 -0
  252. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/docs_markdown.md +0 -0
  253. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/docs_style.md +0 -0
  254. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/docs_tables.md +0 -0
  255. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/drive_activity.md +0 -0
  256. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/drive_files.md +0 -0
  257. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/drive_sharing.md +0 -0
  258. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/drive_transfer.md +0 -0
  259. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/gmail.md +0 -0
  260. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/infra.md +0 -0
  261. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/sheets_charts.md +0 -0
  262. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/sheets_mgmt.md +0 -0
  263. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/sheets_read.md +0 -0
  264. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/qa/tests/sheets_write.md +0 -0
  265. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/style-guide.md +0 -0
  266. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/docs/tools.md +0 -0
  267. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/manifest.json +0 -0
  268. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/mkdocs.yml +0 -0
  269. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/pyproject.toml +0 -0
  270. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/pyrightconfig.json +0 -0
  271. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/build_mcpb.py +0 -0
  272. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/check_supports_all_drives.py +0 -0
  273. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/gen_tool_docs.py +0 -0
  274. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/kai_status.py +0 -0
  275. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/lane_context_hook.py +0 -0
  276. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/oauth_setup.py +0 -0
  277. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/qa_cancel_inflight_write.py +0 -0
  278. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/qa_cancel_insert_local_images.py +0 -0
  279. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/qa_compile_results.py +0 -0
  280. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/qa_gen_md_fixtures.py +0 -0
  281. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/qa_gmail_fixtures.py +0 -0
  282. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/qa_launch_mcpb.py +0 -0
  283. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/qa_revision_temp_doc_parent.py +0 -0
  284. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/setup_team.sh +0 -0
  285. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/sync_slot_config.sh +0 -0
  286. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/scripts/write_team_mcp_configs.sh +0 -0
  287. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/server.json +0 -0
  288. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/.env.template +0 -0
  289. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/__init__.py +0 -0
  290. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/__main__.py +0 -0
  291. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/auth.py +0 -0
  292. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/cache.py +0 -0
  293. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/http_auth.py +0 -0
  294. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/http_transport.py +0 -0
  295. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/local_files.py +0 -0
  296. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/reauth.py +0 -0
  297. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/server.py +0 -0
  298. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/sse_shutdown.py +0 -0
  299. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  300. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/atomic_write.py +0 -0
  301. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/cache.py +0 -0
  302. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/calendar.py +0 -0
  303. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
  304. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  305. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
  306. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/batch_retry.py +0 -0
  307. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
  308. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
  309. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
  310. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/images.py +0 -0
  311. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
  312. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/invalidation.py +0 -0
  313. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  314. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
  315. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/md_to_html.py +0 -0
  316. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
  317. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  318. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  319. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  320. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  321. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
  322. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  323. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  324. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/front_matter.py +0 -0
  325. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/gmail.py +0 -0
  326. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/http_errors.py +0 -0
  327. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/mime_types.py +0 -0
  328. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/pagination.py +0 -0
  329. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  330. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  331. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
  332. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
  333. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
  334. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/__init__.py +0 -0
  335. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/conftest.py +0 -0
  336. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/drive/__init__.py +0 -0
  337. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/drive/test_activity.py +0 -0
  338. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/drive/test_files.py +0 -0
  339. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/drive/test_sharing.py +0 -0
  340. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/drive/test_transfer.py +0 -0
  341. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/drive/test_transfer_front_matter.py +0 -0
  342. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/drive/test_transfer_pagination.py +0 -0
  343. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/drive/test_transfer_query_escaping.py +0 -0
  344. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/fakes.py +0 -0
  345. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/integration/__init__.py +0 -0
  346. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/integration/conftest.py +0 -0
  347. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/integration/test_local_fs_harness.py +0 -0
  348. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/sheets/__init__.py +0 -0
  349. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/sheets/test_data.py +0 -0
  350. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/sheets/test_helpers.py +0 -0
  351. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/sheets/test_structure.py +0 -0
  352. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_atomic_write.py +0 -0
  353. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_auth.py +0 -0
  354. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_cache.py +0 -0
  355. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_cache_isolation.py +0 -0
  356. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_calendar.py +0 -0
  357. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_cancelled_write_invalidation.py +0 -0
  358. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_check_supports_all_drives.py +0 -0
  359. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_concurrency.py +0 -0
  360. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_dependency_floors.py +0 -0
  361. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_anchors.py +0 -0
  362. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_batch_retry.py +0 -0
  363. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_comments.py +0 -0
  364. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_content.py +0 -0
  365. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_dropped_links.py +0 -0
  366. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_editing.py +0 -0
  367. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_images.py +0 -0
  368. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_indices.py +0 -0
  369. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_layout.py +0 -0
  370. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_md_to_html.py +0 -0
  371. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_named_ranges.py +0 -0
  372. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_relative_link.py +0 -0
  373. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_revision_export.py +0 -0
  374. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_style.py +0 -0
  375. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_docs_tables.py +0 -0
  376. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_front_matter.py +0 -0
  377. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_gen_tool_docs.py +0 -0
  378. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_get_doc_structure_inline_objects.py +0 -0
  379. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_gmail.py +0 -0
  380. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_gmail_send_confirm.py +0 -0
  381. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_http_auth.py +0 -0
  382. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_http_errors.py +0 -0
  383. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_http_transport.py +0 -0
  384. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_kai_status.py +0 -0
  385. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_lane_context_hook.py +0 -0
  386. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_local_file_sites.py +0 -0
  387. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_local_files.py +0 -0
  388. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_mcpb_manifest.py +0 -0
  389. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_mime_types.py +0 -0
  390. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_python_versions.py +0 -0
  391. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_qa_gen_md_fixtures.py +0 -0
  392. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_qa_gmail_fixtures.py +0 -0
  393. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_reauth.py +0 -0
  394. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_response_limits.py +0 -0
  395. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_server.py +0 -0
  396. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_server_json.py +0 -0
  397. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_sse_shutdown.py +0 -0
  398. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_sync_slot_config.py +0 -0
  399. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/tests/test_team_mcp_configs.py +0 -0
  400. {mcp_gee_sweet-0.9.6.dev134 → mcp_gee_sweet-0.9.6.dev136}/uv.lock +0 -0
@@ -76,17 +76,17 @@ Each entry below says what a module owns and the invariants a future edit could
76
76
  - `docs/layout.py` — tools (`create_header`, `create_footer`)
77
77
  - `docs/comments.py` — tools (`list_doc_comments`, `add_doc_comment`, `resolve_doc_comment`) via the Drive `comments`/`replies` resource, not the Docs API
78
78
  - `docs/named_ranges.py` — helper (`_create_named_range`) and tools (`create_named_range`, `create_bookmark`), extracted out of `content.py` (#371) since they're fully self-contained. The Docs API has no bookmark-creation endpoint — `create_bookmark` is a thin wrapper over `createNamedRange` spanning a single character; neither tool's output is a link target (only UI-created bookmarks/headings are).
79
- - `docs/doc_to_ast.py` / `docs/ast_to_markdown.py` / `docs/markdown_export.py` — the read side (`get_doc_as_markdown`, opt-in `include_comments` for non-resolved comments via the Drive `comments` resource). It walks `documents().get()` into the same AST the write side uses and serializes that to Markdown, instead of cleaning up Google's styled HTML export (`docs/design/doc-to-markdown.md`). A paragraph is a fenced code block only when *every* run is code-styled (Courier New), so a paragraph holding only one inline code span renders as a block (documented in `docs/known-limitations.md`). Exception (#951): a fence can't hold a link, so a code paragraph with a linked run renders as linked inline code spans instead. A linked run that is only line breaks doesn't count, and neither does a linked image. Every image renders through `_render_image`, which keeps its link, and one inside a code paragraph renders outside the fence (`_render_code_block`). Links come from `_link_url`, shared by text runs and images, which rebuilds a heading or bookmark link (reported by ID, with no `url`) as a same-document URL. Empty-runs paragraphs in table cells are kept as spacers, and `_render_cell` converts `"\n"` to `<br>` *before* stripping. Emphasis markers go inside surrounding whitespace (CommonMark flanking). `_text_run_to_run` turns every `"\v"` (soft break) into `"\n"`, and a plain run's `"\n"` renders as `<br>` with edge breaks outside its link (#1057); `get_doc_structure`/`find_in_doc` still decode only Courier New runs (`decode_code_run_text`). A nested table renders a placeholder note. An ordered item renders its ordinal from `BulletItem.number`, which `document_to_ast` counts per `listId` and nesting level over body paragraphs, never per contiguous run, since a table or paragraph between two items doesn't restart a Docs list (#1208). `startNumber` 0 means 1 only for lettered and Roman levels. A sublist whose first number isn't 1 writes `1.` for that item, since CommonMark won't let it interrupt the parent's text. A block inside a list item (#1302, the inverse of #1094) is found by indent in `doc_to_ast._ListItemContext`: a non-bullet paragraph after an item with `indentStart == indentFirstLine` at that item's level's text indent (or a shallower level's), plus 36pt per quote level, gets that level as `list_depth`, with its quote depth counted from the text indent, not the margin. It tracks the `listId` open at each depth, since a sublist of the other type has its own (#1072). A table (no indent in the API) and a quote whose indent also reads as a deeper margin quote wait: they're inside an item only when the list goes on after them (an item of a list open at that depth, a sublist nested under the last item, or a plainly in-item block), else outside as read. `ast_to_markdown._OpenItems` indents such a block to its item's content column (`_item_prefix`'s width), never a fixed width, with the item's quote levels outside the indent whatever depth the block reads with. A bulleted `HEADING_N` paragraph that isn't a checkbox item is a `BulletItem` with `heading_level`, rendered `- # Title`; every other item's text goes through `_escape_leading_block_marker`. [History](docs/design/module-history.md#toolsdocsdoc_to_astpy--docsast_to_markdownpy--docsmarkdown_exportpy).
79
+ - `docs/doc_to_ast.py` / `docs/ast_to_markdown.py` / `docs/markdown_export.py` — the read side (`get_doc_as_markdown`, opt-in `include_comments` for non-resolved comments via the Drive `comments` resource). It walks `documents().get()` into the same AST the write side uses and serializes that to Markdown, instead of cleaning up Google's styled HTML export (`docs/design/doc-to-markdown.md`). A paragraph is a fenced code block only when *every* run is code-styled (Courier New), so a paragraph holding only one inline code span renders as a block (documented in `docs/known-limitations.md`). Exception (#951): a fence can't hold a link, so a code paragraph with a linked run renders as linked inline code spans instead. A linked run that is only line breaks doesn't count, and neither does a linked image. Every image renders through `_render_image`, which keeps its link, and one inside a code paragraph renders outside the fence (`_render_code_block`). Links come from `_link_url`, shared by text runs and images, which rebuilds a heading or bookmark link (reported by ID, with no `url`) as a same-document URL. Empty-runs paragraphs in table cells are kept as spacers, and `_render_cell` converts `"\n"` to `<br>` *before* stripping. Emphasis markers go inside surrounding whitespace (CommonMark flanking). Two adjacent quoted blocks are one Docs quote (one bar; separate quotes have the emitter's unquoted separator between them), so `_OpenItems.separator` makes the blank line between two nodes carry the quote levels both write at the margin (a block in a quoted list's item writes its item's), or the export reads as two quotes. An empty list item and its first block are joined by a newline, not a blank line: CommonMark ends an item that starts with a blank line (`_starts_empty_item`). `_text_run_to_run` turns every `"\v"` (soft break) into `"\n"`, and a plain run's `"\n"` renders as `<br>` with edge breaks outside its link (#1057); `get_doc_structure`/`find_in_doc` still decode only Courier New runs (`decode_code_run_text`). A nested table renders a placeholder note. An ordered item renders its ordinal from `BulletItem.number`, which `document_to_ast` counts per `listId` and nesting level over body paragraphs, never per contiguous run, since a table or paragraph between two items doesn't restart a Docs list (#1208). `startNumber` 0 means 1 only for lettered and Roman levels. A sublist whose first number isn't 1 writes `1.` for that item, since CommonMark won't let it interrupt the parent's text. A block inside a list item (#1302, the inverse of #1094) is found by indent in `doc_to_ast._ListItemContext`: a non-bullet paragraph after an item with `indentStart == indentFirstLine` at that item's level's text indent (or a shallower level's), plus 36pt per quote level, gets that level as `list_depth`, with its quote depth counted from the text indent, not the margin. It tracks the `listId` open at each depth, since a sublist of the other type has its own (#1072). A table (no indent in the API) and a quote whose indent also reads as a deeper margin quote wait: they're inside an item only when the list goes on after them (an item of a list open at that depth, a sublist nested under the last item, or a plainly in-item block), else outside as read. `ast_to_markdown._OpenItems` indents such a block to its item's content column (`_item_prefix`'s width), never a fixed width, with the item's quote levels outside the indent whatever depth the block reads with. A bulleted `HEADING_N` paragraph that isn't a checkbox item is a `BulletItem` with `heading_level`, rendered `- # Title`; every other item's text goes through `_escape_leading_block_marker`. [History](docs/design/module-history.md#toolsdocsdoc_to_astpy--docsast_to_markdownpy--docsmarkdown_exportpy).
80
80
  - `docs/ast.py` — dataclass schema
81
81
  - `docs/html_parser.py` — HTML→AST. [History](docs/design/module-history.md#toolsdocshtml_parserpy). Invariants:
82
82
  - Bare top-level text gets an implicit paragraph. `_tag_depth` counts open non-block tags, excluding `_VOID_TAGS` (stdlib `HTMLParser` never closes a void tag written without a slash), so text inside only an inline tag stays a no-op. `_finalize()` flushes the block still open at end of input.
83
- - `_emit_block_node(preserve_if_empty)`: a block's own first close keeps an empty node (`runs=[]`) as a paragraph boundary; a resumed frame's re-close (`_block_resumed`) still drops an empty result. A fresh `<p>` (only `<p>`, not `<li>` or headings, and not resumed) keeps whitespace-only runs, since `&nbsp;` lines are deliberate spacers. A fresh `<li>` always emits its `BulletItem`, with `runs=[]` when it has no text (#1075): a Markdown bare `-`, `<li>&nbsp;</li>`, and an `<li>` wrapping only a nested list or block are empty bullets, as a browser shows their markers. A bare `<hr>` appends an empty `Paragraph`. `<pre>` builds its node directly and applies the same fresh-vs-resumed rule itself, keeping a fresh `<pre>`'s whitespace.
83
+ - `_emit_block_node(preserve_if_empty)`: a block's own first close keeps an empty node (`runs=[]`) as a paragraph boundary; a resumed frame's re-close (`_block_resumed`) still drops an empty result. A fresh `<p>` (only `<p>`, not `<li>` or headings, and not resumed) keeps whitespace-only runs, since `&nbsp;` lines are deliberate spacers. A fresh `<li>` always emits its `BulletItem`, with `runs=[]` when it has no text (#1075): a Markdown bare `-`, `<li>&nbsp;</li>`, and an `<li>` wrapping only a nested list or block are empty bullets, as a browser shows their markers. A bare `<hr>` appends a rule, an empty `Paragraph` with `rule=True` (#1096). `<pre>` builds its node directly and applies the same fresh-vs-resumed rule itself, keeping a fresh `<pre>`'s whitespace.
84
84
  - Any `</ol>` or `</ul>` pops `_list_ordered` by count, whatever type opened it, and must run *before* `_resume_interrupted_block`. List frames match by `list_depth`, not tag: the frame is popped once its depth is reached, but the outer block is resumed only on an exact tag match.
85
85
  - `_resume_interrupted_block` flushes whatever block is open, unconditionally, before anything else.
86
86
  - Every leaf-node construction (`Heading`, `Paragraph`, `BulletItem`, `NamedBlock`) must pass `**self._blockquote_fields()`, which sets `blockquote_depth` and `blockquote_path` from the open-`<blockquote>` id stack (`docs/design/blockquote-representation.md`). Without the path, two adjacent quotes are indistinguishable (#1098). A top-level `Table` gets `blockquote_depth` only, at `</table>`, for its indent (#1097). `_make_bullet_item` clamps depth to `>= 0` for an orphan `<li>`.
87
87
  - A block inside a list item (#1094) is that block's own node with `list_depth`/`list_path` naming the item, from `_list_item_fields`, which every non-item leaf construction (and a top-level `Table`, at `</table>`) must pass. The item is the one whose later segment is open, else the innermost `<li>` frame on `_block_stack` whose list ids are exactly the ones open now (each `_BlockFrame` records the interrupted block's `_block_lists`), so a block loose in a list nested in the item isn't the item's. "Open now" is the open block's own `_block_lists`, not the live `_list_ids`, since a block left open until its list's `</ul>` is flushed after the pop (PR #1301 QA round 1). The walk skips a non-matching `<li>` frame instead of stopping at it: an unclosed item in a nested list leaves its frame behind after the `</ul>` (PR #1301 QA round 2). Match by ids, never by depth: a stale frame from a closed list has the depth of a later list (PR #1301 QA round 3). A resumed `<li>` segment (text after a nested list, quote, code block, paragraph or rule) is therefore a `Paragraph` inside the item, never a new `BulletItem`. A heading that opens an `<li>` (`- # Title`) is the item's own text: `BulletItem.heading_level`, the item emitted at the heading's close and its later segment resumed. Its tag stays on `_li_heading_tags` until that `</hN>`, so the end tag is consumed even when a nested block interrupted the item first; never let it reach the inline end-tag path, which would decrement `_tag_depth` for a start tag that never incremented it.
88
88
  - In a table cell, a whitespace run holding a line break is dropped at a segment's start (the cell's start, or just after a nested table). At the end of the text so far it's held as runs with their own style and link (`_cell_held_runs`), and written only if content follows in the same segment, which ends at a nested `<table>` or `</td>` (#1073). Nothing inside a cell's `<pre>` is trimmed (`_cell_pre_depth`). Everything else in a cell parses as it did before; collapsing every source line break merged a cell's `<p>`s and `<pre>` lines (PR #1215 QA round 2), and whether a newline in cell text should collapse is #1221 (`docs/design/image-conversion.md`).
89
- - `_interrupt_open_block` drops trailing whitespace holding a line break from the outgoing segment's unflushed source text: markdown-it writes `<li>a\n<ul>` for every nested block, and the `\n` would become an empty paragraph (#1116). A `<br>` goes into the run buffer as `_LineBreak`, never a bare `"\n"`, so that trim leaves it. A resumed segment likewise drops the line break at its start (`handle_data`), keeping an authored `&nbsp;`. Neither trim runs in `<pre>` (`_in_pre`), where the break and indentation are the code's. An `<hr>` in an open block that has text, is a resumed segment, or is a fresh `<li>` (an empty bullet, #1075) interrupts and resumes it; in any other fresh block with no text (`<p><hr>`) it stays a dropped construct that keeps the boundary (#401).
89
+ - `_interrupt_open_block` drops trailing whitespace holding a line break from the outgoing segment's unflushed source text: markdown-it writes `<li>a\n<ul>` for every nested block, and the `\n` would become an empty paragraph (#1116). A `<br>` goes into the run buffer as `_LineBreak`, never a bare `"\n"`, so that trim leaves it. A resumed segment likewise drops the line break at its start (`handle_data`), keeping an authored `&nbsp;`. Neither trim runs in `<pre>` (`_in_pre`), where the break and indentation are the code's. An `<hr>` in any open block, outside `<pre>` and inline tags, interrupts and resumes it: after text, in a resumed segment, in a fresh `<li>` (an empty bullet, #1075), and in a fresh block with no text, whose empty first segment emits nothing, so `<p><hr></p>` is the rule alone (#1096).
90
90
  - Known gap: a resumed block's genuine trailing `&nbsp;` is dropped (#444).
91
91
  - `docs/emitter.py` — AST→Docs API requests, plus the multi-phase table fill (nested tables, cell styling). [History](docs/design/module-history.md#toolsdocsemitterpy). Invariants:
92
92
  - Bullet depth is encoded as leading `"\t"` on each paragraph's text, and `createParagraphBullets` computes levels only within one call. So each top-level list gets exactly one call over its whole range, nested lists of either type included; one call per paragraph creates separate lists and breaks numbering. `_bullet_lists` (#1072) returns each top-level list with the lists nested in it that aren't merged into it: every list of the other type, and a same-type list that follows another sublist under the same item (which restarts). A top-level list's range spans every item nested in it, a trailing nested list's too, or that list's tabs go unconsumed. Each of those is then taken back out, parent before child (`_top_list_requests`: delete its bullets, zero its indent, re-insert its tabs, anchor-wrap it with its own preset), so the outer list keeps counting past it. A `createParagraphBullets` range containing an already-listed paragraph joins the unlisted ones to that paragraph's list (confirmed live), so the outer call can't come last. A range whose preceding paragraph is in a same-preset list joins that list too (confirmed live, #1241), so a nested list's anchor wrap puts a barrier paragraph before its anchor (`barrier=True`). Each `BulletItem` carries `list_path`, the ids of its enclosing `<ul>`/`<ol>` lists (`html_parser._push_list`), captured when its block segment opens (`_block_lists`), since an unclosed `<li>` is flushed after `</ul>` pops. An open list that doesn't enclose an item is closed first (`_encloses`), and a shallower item rejoins the block's first list only from a list enclosing it (`_rejoins_root`), so two adjacent same-type lists stay two Docs lists at any depth; an item without ids groups by depth and type alone. A nested list re-inserts and re-consumes its own tabs, so each item's tabs are consumed once, net.
@@ -96,6 +96,7 @@ Each entry below says what a module owns and the invariants a future edit could
96
96
  - `insertTable` gives the table its indent, which no API field can reset afterward, and every empty cell its text style, from the paragraph at the insertion point. So each table gets a placeholder paragraph in the phase-1 text, reset to plain after the bullet pass by `_table_placeholder_requests` (a table between two same-preset list items sits inside their one bullet range; a quoted table keeps its quote's indent via `Table.blockquote_depth`, which `_quoted` still ignores). The table is inserted there, and the placeholder, pushed past the table, is deleted by the `deleteContentRange` right after the `insertTable` (`_inserted_table_length`). Never insert a table at the start of the next block's text: it takes that block's indent and first-character style (#1097). `_positional_insert_tail_start` keeps that delete in the positional tail by its place after an `insertTable`.
97
97
  - `_strip_bullet_leading_tabs` removes a literal leading tab from a bullet's own text (it would read as depth) and returns new `Run`/`Image` objects; it must never mutate caller-owned AST nodes. Dropping an authored leading tab is a deliberate, documented loss.
98
98
  - Blockquote nodes get a gray `borderLeft` and `indentStart` = `indentFirstLine` = 36pt × depth (`indentFirstLine` is absolute, not relative to `indentStart`). `createParagraphBullets` overwrites both indents, so a quoted `BulletItem`'s request runs after the whole bullet pass and before the table/image inserts, at the list level's own indents (`list_level_indents`, level capped at 8 as Docs caps `nestingLevel`) plus the quote's. Every position used after the bullet pass maps through `_after_bullet_pass`. `doc_to_ast.py` subtracts the level's indent when inferring depth.
99
+ - A rule (`Paragraph.rule`, #1096) is an empty 6pt paragraph with a gray `borderBottom`, since the API can't insert a `horizontalRule`. It also sets `borderBetween`: Docs joins adjacent paragraphs with the same borders and draws `borderBottom` only under the last, so two rules in a row drew one line (confirmed live). A quoted rule gets no `spaceBelow` and a 4pt line instead: a paragraph's spacing isn't covered by the quote bar when its neighbours' borders differ, so any gap split one quote into two on screen (PR #1326 QA round 1). `_rule_requests` must come before a quote's edge spacing for the same range, and a rule inside a list item is drawn after the bullet pass (`deferred`), like its indent. The read side recognizes the stand-in by its whole signature (`is_rule_style`: bottom and matching between-border, no top or right), never a bottom border alone, which an empty line in a bordered box also has; `_build_blank_para_before_table_collapses` uses the same test to skip a rule before a table. `doc_to_ast` splits a paragraph at each native `horizontalRule` (`_split_at_horizontal_rules`; Drive's import puts one in the same paragraph as text, a list item's included), keeping the text's own type and putting a line after a list item's text inside that item. `ast_to_markdown` writes a rule `---`, or `***` as the first line, where `---` opens front matter.
99
100
  - A code-styled paragraph's internal `"\n"` breaks become `"\v"` (`_is_code_paragraph`) so a fenced block stays one Docs paragraph; `doc_to_ast.py` reverses it.
100
101
  - Table-cell images (#1073) go in during `fill_tables`, not phase 1, from the URIs in `CellImages`. `fill_tables` requires one, and every cell-fill `batchUpdate` goes through `CellImages.execute`. `_run_group_fill_requests` writes a cell's text and styles as if no image were there, then each image (through `CellImages.insert_requests`) at the offset of the text before it, latest first. `execute` replaces an unfetchable image with its fallback text and resends through `batch_retry.send_dropping_failed_images`, the loop the body's retry also uses, and splits a batch only at a failed image, never elsewhere: an earlier cell's text moves every later position. Its request bookkeeping holds the request dicts and is cleared after each batch, so an `id()` key can't outlive its request. `_text_offset_since_last_table` counts what each image took (`CellImages.doc_length`): one index embedded, its fallback text's UTF-16 length when a never-resolved or dropped image was written as text (#1099). `CellImages.embedded` grows as each batch lands, and `_apply_doc_content` gives every resolved cell image missing from it an `error` after the fill and on any `Exception` out of the edit, phase 1 included, but not on a cancellation, whose last batch can still land (`docs/design/image-conversion.md`).
101
102
  - Docs joins the left borders of adjacent paragraphs with the same border and indents into one bar, whatever their spacing. So `_with_quote_separators` puts an unbordered empty paragraph, collapsed by `_collapse_paragraph_requests`, between two adjacent quoted nodes whose `blockquote_path`s aren't one a prefix of the other (#1098). `_block_spacing` gives a code block 8pt above and below, and a quote 8pt where it meets content outside it. A separator is spaced on one side only (below the quote before it), and a `Heading` or TITLE/SUBTITLE `NamedBlock` gets no quote-edge space, since an explicit value replaces its named style's own. `_spacing_requests` puts `spaceAbove` on a node's first Docs paragraph and `spaceBelow` on its last, since a node with a `"\n"` in its text spans several. A quoted `BulletItem`'s spacing is emitted after the bullet pass, like its quote styling. `_build_blank_para_before_table_collapses` resets every empty paragraph before a table to plain, a backstop since the table placeholder (above). It deletes the bullet first, because `deleteParagraphBullets` adds indent, then clears indents and border. It leaves the paragraph between two tables at full height, so they don't touch.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcp-gee-sweet
3
- Version: 0.9.6.dev134
3
+ Version: 0.9.6.dev136
4
4
  Summary: MCP server for Google Workspace — Sheets, Drive, Docs (HTML or Markdown), Calendar, and Gmail.
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
@@ -91,15 +91,19 @@ needs Markdown inside it as HTML instead.
91
91
  **What:** A table inside a list's last item (`1. a\n\n | x |\n |---|\n`) is written to the
92
92
  Doc indented under the item, but `get_doc_as_markdown` exports it at the margin, after the list.
93
93
  A quote that ends a list's last item (`- a\n\n > q\n`) exports as a deeper quote at the margin
94
- (`> > q`). Either one exports inside its item when the list goes on after it: a later item of the
95
- list, a sublist nested under the item (of either type), or a paragraph indented in the item. A
96
- paragraph, code block, or heading inside any item always exports inside it.
94
+ (`> > q`). So does a paragraph, code block, heading, or rule inside the last item of a *quoted*
95
+ list (`> - a\n>\n> b\n` exports `b` as `> > b`). Any of these exports inside its item when the
96
+ list goes on after it: a later item of the list, a sublist nested under the item (of either
97
+ type), or a paragraph indented in the item. A paragraph, code block, or heading inside an item of
98
+ an unquoted list always exports inside it.
97
99
 
98
100
  **Why:** The export finds a block inside an item from its indent (#1302). A table's indent isn't
99
101
  reported anywhere in the Docs API: the JSON of a table inside an item matches one at the margin
100
102
  (checked live). A quote's is, but a quote inside an item sits exactly where a deeper quote at the
101
103
  margin would (a quote in a top-level item is at 72pt, like a margin `> >`), so the indent can't
102
- tell them apart. Such a block counts as inside an item only when the list goes on after it, which
104
+ tell them apart. A block inside a quoted list's item is written the same way, at the item's text
105
+ indent plus the quote's and with the quote's border, which is exactly where a deeper quote at the
106
+ margin goes. Such a block counts as inside an item only when the list goes on after it, which
103
107
  it never does after a list's last item. Reading an ambiguous quote as the margin quote keeps every
104
108
  quote level; reading it into the item would drop one.
105
109
 
@@ -134,6 +138,27 @@ lines join into one paragraph. Decided in #1242.
134
138
 
135
139
  ---
136
140
 
141
+ ### A horizontal rule is a bordered paragraph, not a Docs horizontal line
142
+
143
+ **What:** A Markdown thematic break (`---`, `***`, `___`) or an HTML `<hr>` written by `create_doc`,
144
+ `write_doc_content`, `create_doc_from_file`, `update_doc_from_file`, or `upload_file` (Markdown
145
+ source) appears as a thin gray line, but in Docs it's an empty paragraph with a bottom border
146
+ (6pt, or 4pt inside a quote), not the element Insert > Horizontal line adds, so editing it in Docs
147
+ behaves like editing a paragraph. Inside a quote the line sits closer to the text below it than
148
+ to the text above: space below it would break the quote bar. `get_doc_as_markdown` reads both
149
+ kinds of line back as `---` (`***` when it's the document's first line, where `---` would open
150
+ front matter). A native line that shares a list item's paragraph but comes before its text
151
+ exports at the margin, ending the list there. An `<hr>` inside a table cell is dropped: a cell
152
+ holds no paragraph styling the converter writes (#1327).
153
+
154
+ **Why:** The Docs API has no request that inserts a horizontal line (#1096).
155
+
156
+ **Workaround:** For a native line, use Insert > Horizontal line in the Docs UI, or convert the
157
+ file with `upload_local_file(convert=True)`, whose Drive import draws native lines (with the other
158
+ differences listed under Google Drive below).
159
+
160
+ ---
161
+
137
162
  ### `create_bookmark` doesn't create a bookmark Docs lists
138
163
 
139
164
  **What:** `create_bookmark` creates a one-character named range. It doesn't appear under
@@ -0,0 +1,4 @@
1
+ <p>text before<hr>text after</p>
2
+ <ul><li>item one<hr></li><li>item two</li></ul>
3
+ <h2>heading<hr></h2>
4
+ <p>end</p>
@@ -0,0 +1,11 @@
1
+ Para before
2
+
3
+ ---
4
+
5
+ Para after
6
+
7
+ > Quoted before
8
+ >
9
+ > ***
10
+ >
11
+ > Quoted after
@@ -2466,7 +2466,7 @@ Known gap: 2/3-space indented ordered children flatten (documented bug-3 behavio
2466
2466
  **Checks**
2467
2467
  - Same nesting expectations as TC-DOC104 — 4-space indentation is the one width where `_md_to_html` produces genuinely nested `<ul>`/`<ol>` HTML, so this TC should behave identically to TC-DOC104 once bugs 1+2 are fixed
2468
2468
  - Case 2's bare `-` is an empty bullet at level 0, with "Bare-nested child C1"/"C2" one level under it; no literal `-` text (#1075)
2469
- - Case 4: an empty bullet between "Bullet before" and "Bullet after" (three bullets, one list), and an empty numbered item between "Number one" and "Number three", which show 1., 2. (empty) and 3.; no literal `-` or `2.` text (#1075). Then "Rule item before", an empty bullet with the rule's paragraph under it, unbulleted (the item's only content is the rule; a rule renders as an empty paragraph, not a drawn line, until #1096), and "Rule item after", three bullets in one list (PR #1322 QA round 1)
2469
+ - Case 4: an empty bullet between "Bullet before" and "Bullet after" (three bullets, one list), and an empty numbered item between "Number one" and "Number three", which show 1., 2. (empty) and 3.; no literal `-` or `2.` text (#1075). Then "Rule item before", an empty bullet with the rule under it, unbulleted and drawn as a thin gray line at the item's text indent (the item's only content is the rule; #1096), and "Rule item after", three bullets in one list (PR #1322 QA round 1)
2470
2470
  - Confirms whether a "use 4 spaces" workaround note in tool docstrings would be sufficient today (it would not — bugs 1+2 still apply at this indentation)
2471
2471
 
2472
2472
  **Cleanup:** write fixture content back
@@ -2495,6 +2495,8 @@ Run 2 (ash).
2495
2495
 
2496
2496
  **Result (2026-10-31, PR #1322 @ fca630bb, round 2) ✅ PASS — rule-only item added.** `create_doc_from_file` on the updated fixture into a scratch doc (trashed after), `mcp-gee-sweet-sky`, graded from `get_doc_structure` and Playwright on screen. Cases 1–4 as in round 1 (Case 2: empty bullet at level 0, C1/C2 at level 1; Case 4: empty bullet and empty "2." in place). New list: "Rule item before", an empty bullet, the rule's unbulleted empty paragraph, then "Rule item after", the three bullets sharing one `listId`. On screen the rule is a blank gap with no line; a control doc's top-level `---` renders the same way, so that's #1096, not this PR. Check wording updated to match.
2497
2497
 
2498
+ **Result (2026-11-01, PR #1326 @ 28c464a4, round 2) ✅ PASS — Case 4 only (#1096).** Case 4 of the fixture via `create_doc` into a scratch doc (trashed after), `mcp-gee-sweet-sky`, Playwright on screen. Empty bullet between "Bullet before" and "Bullet after"; 1., 2. (empty), 3.; then "Rule item before", an empty bullet, the rule drawn as a thin gray line at the item's text indent, and "Rule item after", one list. Cases 1–3 not re-run: no code they reach changed in this PR's emitter beyond the rule paragraph.
2499
+
2498
2500
  ---
2499
2501
 
2500
2502
  **Tooling gap note (not a fix, just a QA-visibility limitation):** `get_doc_structure` doesn't currently expose paragraph indentation or the Docs API's bullet `nestingLevel` field, so confirming bug 2's live-rendering half (or a future fix for it) requires a Playwright visual check every time rather than a structural assertion. Worth a follow-up ticket if this bug is fixed — the same way `font_family` is already a known `get_doc_structure` gap (TC-DOC42/47).
@@ -6664,12 +6666,12 @@ asyncio.run(main())
6664
6666
  - LB-06: ` > Quoted text inside the item.` (2 spaces, one `>`), between the two bullets. No `> >` anywhere in the output except where the source has a nested quote
6665
6667
  - LB-07: every table line indented 3 spaces, between `1. Compare the options:` and `2. Pick one.`
6666
6668
  - LB-08: `- # Heading inside an item`, then `- Plain item after it`
6667
- - LB-10: ` After the rule` (3 spaces) before `2. Next item`
6669
+ - LB-10: ` ---` (3 spaces), a blank line, then ` After the rule` (3 spaces) before `2. Next item` (the rule exported since #1096)
6668
6670
  - LB-11: ` > First quote.` and ` > Second quote.`, separated by at least two blank lines, before `2. Next item`
6669
6671
  - LB-12: ` Inner second paragraph` (4 spaces), then `- Outer two`
6670
6672
  - LB-13: `> 1. Quoted one`, `>`, `> More of quoted one`, `>`, `> 2. Quoted two`
6671
6673
  - LB-14: the table at the margin (no leading spaces), then `1. Step two`: the write side makes two Docs lists here, so the table is outside the list. The second list starts at 1, not 2, because the write side ignores `start` (#1118)
6672
- - Step 3's export equals step 2's, except that LB-10 has two fewer blank lines: the rule exports as an empty paragraph, which the re-import drops (#1096)
6674
+ - Step 3's export equals step 2's. (Until #1096, LB-10's rule exported as an empty paragraph, which the re-import dropped, so step 3 had two fewer blank lines there.)
6673
6675
  - Step 4, W-1: every line from `> - a` through `> - c` starts with `>`; the table lines are `> | …` (the quote, then 2 spaces)
6674
6676
  - W-2: the table lines indented 3 spaces between `1. a` and `- b`, then `2. c`. (`- b` itself is indented 2 spaces, which is #1128)
6675
6677
  - W-3: the table lines indented 2 spaces (in a), not 4 (in b), before `- c`
@@ -6684,6 +6686,9 @@ asyncio.run(main())
6684
6686
  **Result (2026-10-31) ✅ PASS — PR #1311 round 2 (Sky)**
6685
6687
  `mcp-gee-sweet-sky` at b5e90da6, reconnected after the reset, on scratch docs in `{FOLDER_ID}`. Steps 1–3 as in round 1: every LB check holds, and step 3's export equals step 2's except LB-10's two fewer blank lines (#1096). Step 4: W-1 keeps `>` on every line from `> - a` to `> - c`, with the table at `> | …`; W-2 has the table at 3 spaces between `1. a` and ` - b` (#1128), then `2. c`; W-3 has the table at 2 spaces, then `- c`; W-4 exports `> > b` at the margin, then `After.`. Extra probes, all exported as written: a quote then a paragraph in a last item (` > q`, ` tail`), a quote under a nested item before a parent item (` > q`), a quote inside a quoted list's item (`> > inner`), a table then a quote in one item before the next item. Scratch docs trashed.
6686
6688
 
6689
+ **Result (2026-11-01) ✅ PASS — PR #1326 round 2 (Sky)**
6690
+ `mcp-gee-sweet-sky` at 28c464a4, reconnected, on scratch docs in `{FOLDER_ID}`. Run because the PR rewrote the export's block separator (`_OpenItems.separator`). Step 2: every LB check holds, LB-10 now ` ---`, a blank line, ` After the rule`, then `2. Next item`. Step 3's export is byte-identical to step 2's (diffed). Step 4: W-1 keeps `>` on every line, with the table at `> | …`; W-2 has the table at 3 spaces, ` - b` (#1128), then `2. c`; W-3 has the table at 2 spaces, then `- c`; W-4 is `> > b` at the margin, then `After.`. Scratch docs trashed.
6691
+
6687
6692
  ### TC-DOC245: A file's relative image path resolves against the file, and a replace uploads its local images to folder_id, the server default, or the doc's own folder (#1121, #1139) ⚠️ requires-oauth ⚠️ destructive
6688
6693
  **Purpose:** `create_doc_from_file`/`update_doc_from_file` resolved a relative image path against the server's working directory, so a README's images never embedded (#1121). `update_doc_from_file` had no `folder_id`, so on a server with no default folder every local image failed after the doc was cleared, leaving it without its images (#1139). A relative path is now joined to the file's own folder; `write_doc_content`/`update_doc_from_file` take `folder_id`, fall back to the server's default folder and then to the doc's own folder, and refuse before touching the doc when none is known. The doc's-folder fallback only runs on a server with no `DRIVE_FOLDER_ID`, so parts B and C use a scratch script against the checkout under test with `folder_id=None`, the way TC-DOC243 part B does.
6689
6694
 
@@ -6746,3 +6751,36 @@ Fresh interpreter against eb6ce267, `folder_id=None`, with an existing local ima
6746
6751
 
6747
6752
  **Result (2026-10-31) ✅ PASS (step 6 only) — PR #1310 round 4 (Kit)**
6748
6753
  Fresh interpreter at 78f5536a (the `develop` merge that renumbered this case from TC-DOC244). Step 6 as first written stubbed `_doc_parents` to return `None`, but it returns `(parents, error)` since round 2, so the stub would raise on unpacking; corrected to `result=(None, None)`. With it, step 6 printed the documented refusal ending "Pass folder_id. The document was left unchanged." on a fresh scratch doc, which was then trashed. The other steps' code paths are unchanged since round 3.
6754
+
6755
+ ### TC-DOC246: A thematic break is drawn as a line, survives a replace without leaking, and exports as a rule, native lines included (#1096) ⚠️ requires-oauth
6756
+ **Purpose:** A Markdown `---`/`***`/`___` or an HTML `<hr>` rendered as an empty paragraph, a blank gap with no line (#1096). The Docs API can't insert a horizontal line, so a rule is now an empty 6pt paragraph with a gray bottom border (plus a between-border, so two rules in a row draw two lines). `<p><hr></p>` is the rule alone, and text after `<p><hr>` is no longer lost. `get_doc_as_markdown` reads both that stand-in and a native horizontal line (Drive's Markdown import, Insert > Horizontal line) back as `---`, or `***` when the rule is the doc's first line, where `---` would open front matter. TC-MD003 (C8), TC-MD114 and the TC-MD2xx rule cases grade the rendering across every entry point; this case covers the export, the HTML spellings and the replace path.
6757
+
6758
+ **Playwright: required**
6759
+
6760
+ **Setup:** `F` = `{REPO}/docs/qa/fixtures/md/tc-doc246/rules.md`, `H` = `{REPO}/docs/qa/fixtures/md/tc-doc246/native-lines.html`.
6761
+
6762
+ **Steps**
6763
+ 1. `create_doc(title="TC-DOC246 md", folder_id={FOLDER_ID}, content_format="markdown", content="***\n\nPara before\n\n---\n\nPara between\n\n***\n___\n\nTwo rules above\n\n---\n\n## Heading after a rule\n\n> Quoted one\n>\n> ---\n>\n> Quoted two\n\n1. Item one\n\n ---\n\n2. Item two\n\n- - -\n\n| a | b |\n|---|---|\n| 1 | 2 |\n\nTail\n\n---\n")`; call its `docId` `{MD}`
6764
+ 2. `get_doc_as_markdown(doc_id={MD})`
6765
+ 3. `create_doc(title="TC-DOC246 html", folder_id={FOLDER_ID}, content="<p>A</p><hr><p>B</p><p><hr></p><p>C</p><p><hr>D</p>")`, then `get_doc_as_markdown` on its `docId`
6766
+ 4. `write_doc_content(doc_id={MD}, content_format="markdown", content="Plain one\n\nPlain two\n")`, then `get_doc_as_markdown(doc_id={MD})`
6767
+ 5. `upload_local_file(local_path=F, parent_folder_id={FOLDER_ID}, name="TC-DOC246 native.md", convert=True)`, then `get_doc_as_markdown` on its `fileId` (the `.md` in `name` is required: `convert` reads the extension from `name`, and without it the call returns "Conversion not supported for extension ''")
6768
+ 6. `upload_local_file(local_path=H, parent_folder_id={FOLDER_ID}, name="TC-DOC246 native.html", convert=True)`, then `get_doc_as_markdown` on its `fileId`
6769
+ 7. `create_doc(title="TC-DOC246 lists", folder_id={FOLDER_ID}, content_format="markdown", content="- a\n-\n ---\n- b\n\n> - qa\n>\n> qb\n>\n> - qc\n>\n> after\n")`, then `get_doc_as_markdown` on its `docId`
6770
+
6771
+ **Checks**
6772
+ - Step 1 returns `docId` with no `error`. On screen, every rule is a thin gray line across the text column with space above and below it: the first line of the doc; between "Para before" and "Para between"; two separate lines (not one, not touching) above "Two rules above"; above the heading; between the quoted paragraphs, starting at the quote bar, with the bar running unbroken from "Quoted one" through the rule to "Quoted two" (one quote, not two; the line sits closer to "Quoted two" than to "Quoted one"); in the list, indented to "Item one"'s text, with the next item numbered **2.**; above the table; and after "Tail". No blank gap stands in for any rule
6773
+ - Step 2 is `***`, then each rule as `---` in the same places: one quote, `> Quoted one`, `>`, `> ---`, `>`, `> Quoted two` (a `>` on the blank lines, so it isn't three quotes), ` ---` (3 spaces) between `1. Item one` and `2. Item two`, and two `---` blocks in a row above "Two rules above"
6774
+ - Step 3, on screen: A, a line, B, a line, C, a line, D. The export is `A`, `---`, `B`, `---`, `C`, `---`, `D`, a blank line between each: `<p><hr></p>` gives one rule and no extra empty paragraph, and "D" is kept
6775
+ - Step 4's export is `Plain one` and `Plain two` with no `---`, and on screen neither paragraph has a line under it: the replaced rules' borders didn't carry over
6776
+ - Step 5's Doc draws Drive's own horizontal lines; its export has a `---` between "Para before" and "Para after" and another between "Quoted before" and "Quoted after". Extra blank lines and the quote's missing `>` are Drive's import, not this case (`docs/known-limitations.md`)
6777
+ - Step 6: Drive's import puts each line in the same paragraph as the text next to it. The export keeps every line and every piece of text, in order: `text before`, `---`, `text after`; the "item one" item, then `---` indented under it (inside the item), then the "item two" item, one list; `## heading`, then `---`; `end`. How the list itself exports (Drive imports it indented, which reads as a quoted numbered list) is Drive's import, not this case
6778
+ - Step 7, on screen: three bullets in one list with the line under the empty middle one, at the item's text indent. The quoted list's bars are offset from the quote's own, as `docs/known-limitations.md` describes. The export has `- a`, `- `, ` ---` on the line right after the empty item's marker (no blank line between, which would end the item), then a blank line and `- b`; then a blank line, and `> - qa`, `>`, `> qb`, `>`, `> - qc`, `>`, `> after`, a `>` on every blank line (one quote)
6779
+
6780
+ **Cleanup:** `delete_file` the five docs
6781
+
6782
+ **Result (2026-11-01) ⚠️ PARTIAL — PR #1326 round 1 (Sky)**
6783
+ Live on `mcp-gee-sweet-sky` at 42e755df (OAuth, Shared Drive folder). Steps 2–5 pass. Step 2's export matches every listed line, including `***` first and `> ---` with `>` on the blank lines. Step 3 draws A, line, B, line, C, line, D and exports the same, with "D" kept. Step 4 exports `Plain one`/`Plain two` with no lines left on screen. Step 5's Drive import draws native lines and exports both `---`. Step 5 as first written passed `name="TC-DOC246 native"`, and `convert` reads the extension from `name`, so the call errored ("Conversion not supported for extension ''"); the step now uses `TC-DOC246 native.md`. Step 1: every rule draws (first line, between paragraphs, two separate lines in a row, above the heading, in the list at "Item one"'s text indent with **2.** after it, above the table, after "Tail"). One defect: the quoted rule's line starts at the bar, but the bar breaks below it. "Quoted one" plus the rule carry one bar, then the rule's 6pt space below opens a gap, and "Quoted two" gets a bar of its own, so on screen it reads as two quotes while the export says one. Sent back. Cleanup: the three docs plus a probe doc trashed.
6784
+
6785
+ **Result (2026-11-01) ✅ PASS — PR #1326 round 2 (Sky)**
6786
+ Live on `mcp-gee-sweet-sky` at 28c464a4, after `/mcp reconnect` (OAuth, Shared Drive folder), all seven steps. Step 1: every rule draws as listed. The quote bar now runs unbroken from "Quoted one" through the rule to "Quoted two", with the line closer to "Quoted two". Step 2's export is unchanged from round 1 and matches every check. Steps 3 and 4 pass as in round 1, re-run on this head. Step 5 exports both `---`. Step 6 exports `text before`, `---`, `text after`; then `> 1. item one`, `>`, `> ---`, `>`, `> 2. item two` (the line inside "item one", one list; Drive imports the list indented, which reads as a quote); then `## heading`, `---`, `end`. Step 7 draws three bullets in one list with the line under the empty middle one at the item's text indent. The quoted list is a separate Docs list (its own `listId`), with offset bars. Its export is `- a`, `- `, ` ---` (no blank line after the empty marker), a blank line, `- b`, a blank line, then `> - qa`, `>`, `> qb`, `>`, `> - qc`, `>`, `> after`. Cleanup: the five docs trashed.
@@ -399,8 +399,8 @@ Feature set and fill set by the maintainer 2026-10-18, under the release-scoping
399
399
  - [x] A numbered list restarts its numbering after a nested bullet list. #1241, the reverse case in the same run grouping, was folded in (maintainer, 2026-10-28) but PR #1286 didn't fix it, so #1241 stays open below — lane A ([#1072](https://github.com/khuisman/mcp-gee-sweet/issues/1072)) (PR #1286)
400
400
  - [x] Two adjacent Markdown lists render as one Docs list. `wrong/narrow`, observed in TC-MD105/108. Folded into #1072 (maintainer, 2026-10-28), but PR #1286 didn't fix it: it needs a list boundary in the AST — lane A ([#1241](https://github.com/khuisman/mcp-gee-sweet/issues/1241)) (PR #1295)
401
401
  - [x] Images inside table cells are dropped silently ([#1073](https://github.com/khuisman/mcp-gee-sweet/issues/1073)) (PR #1215)
402
- - [ ] HTML input: whitespace between list tags becomes stray bullet paragraphs — lane A ([#1074](https://github.com/khuisman/mcp-gee-sweet/issues/1074))
403
- - [ ] A bare '-' (empty list item) isn't recognized in Markdown ([#1075](https://github.com/khuisman/mcp-gee-sweet/issues/1075))
402
+ - [x] HTML input: whitespace between list tags becomes stray bullet paragraphs — lane A ([#1074](https://github.com/khuisman/mcp-gee-sweet/issues/1074)) (PR #1315)
403
+ - [x] A bare '-' (empty list item) isn't recognized in Markdown — lane A ([#1075](https://github.com/khuisman/mcp-gee-sweet/issues/1075)) (PR #1322)
404
404
  - [ ] **Bundle:** Markdown/HTML inline styles — one PR closing [#1077](https://github.com/khuisman/mcp-gee-sweet/issues/1077) (strikethrough, `<del>`) and [#1119](https://github.com/khuisman/mcp-gee-sweet/issues/1119) (`<sub>`, `<sup>`, `<mark>`); after PR #1228, check how much of #1077 the GFM strikethrough rule already fixed. Bundled 2026-10-25 (maintainer) — lane B ([#1231](https://github.com/khuisman/mcp-gee-sweet/issues/1231))
405
405
  - [x] get_doc_as_markdown indents a numbered sub-list under a numbered item by only 2 spaces, so it re-imports flat. Found in the 0.9.3 release-QA pass ([#1128](https://github.com/khuisman/mcp-gee-sweet/issues/1128)) (PR #1234)
406
406
  - [x] get_doc_as_markdown renumbers a numbered list from 1 after a table (or any block) interrupts it, so it re-imports as two lists. `wrong/narrow`, observed in PR #1201 QA ([#1208](https://github.com/khuisman/mcp-gee-sweet/issues/1208)) (PR #1234)
@@ -413,7 +413,7 @@ Feature set and fill set by the maintainer 2026-10-18, under the release-scoping
413
413
  - [ ] **Bundle:** Markdown/HTML line breaks — one PR closing [#1093](https://github.com/khuisman/mcp-gee-sweet/issues/1093) (soft break), [#1117](https://github.com/khuisman/mcp-gee-sweet/issues/1117) (hard break) and [#1091](https://github.com/khuisman/mcp-gee-sweet/issues/1091) (`<br>` in prose), html_parser cluster; after PR #1228. Bundled 2026-10-25 (maintainer) ([#1230](https://github.com/khuisman/mcp-gee-sweet/issues/1230))
414
414
  - [x] A fenced code block or second paragraph inside a list item breaks the list — html_parser cluster; fence half may fold into #1116 ([#1094](https://github.com/khuisman/mcp-gee-sweet/issues/1094)) (PR #1301)
415
415
  - [x] Adjacent blocks run together: separate quotes merge, consecutive code blocks merge, quotes have no spacing — lane B, after #1056 ([#1098](https://github.com/khuisman/mcp-gee-sweet/issues/1098)) (PR #1199)
416
- - [ ] A thematic break (`---`) renders as an empty paragraph ([#1096](https://github.com/khuisman/mcp-gee-sweet/issues/1096))
416
+ - [ ] A thematic break (`---`) renders as an empty paragraph — lane A ([#1096](https://github.com/khuisman/mcp-gee-sweet/issues/1096))
417
417
  - [ ] GFM table column alignment is ignored ([#1095](https://github.com/khuisman/mcp-gee-sweet/issues/1095))
418
418
  - [x] A table takes on the style of the paragraph that follows it ([#1097](https://github.com/khuisman/mcp-gee-sweet/issues/1097)) (PR #1201)
419
419
  - [x] Drive's native Markdown import (EP-5 to EP-7) renders differently from our converter — ornithomimus scope: document EP-5 to EP-7 as Drive's engine; routing is #1113 ([#1101](https://github.com/khuisman/mcp-gee-sweet/issues/1101)) (PR #1270)
@@ -573,7 +573,7 @@ Fill candidates moved from v0.9.3 and the decision-needed backlog on 2026-10-18.
573
573
 
574
574
  - [ ] `style_doc_range`: a `font_family`-only change turns off bold inherited from a named style (Docs writes `bold: false`). `wrong/narrow`, observed in #1153's live check; the second follow-up in #1273's family, after #1289 ([#1300](https://github.com/khuisman/mcp-gee-sweet/issues/1300))
575
575
  - [ ] `upload_local_file(convert=True)`: a `name=` without an extension errors instead of falling back to `local_path`'s extension. `loud/narrow`; the drop-`.md`-from-the-Doc-name half split out to #1309 ([#1307](https://github.com/khuisman/mcp-gee-sweet/issues/1307))
576
- - [ ] `get_doc_as_markdown` indents an item nested under a numbered item by 2 spaces, short of the parent's content column, so other CommonMark/GFM readers flatten or split the list (our own import nests it via the D1 pre-pass). `wrong/narrow`, observed; found in #1144's live check ([#1314](https://github.com/khuisman/mcp-gee-sweet/issues/1314))
576
+ - [ ] **Bundle:** `get_doc_as_markdown` list-item export — one PR closing [#1314](https://github.com/khuisman/mcp-gee-sweet/issues/1314) (item under a numbered item indented 2 spaces, short of the parent's content column; found in #1144's live check), [#1323](https://github.com/khuisman/mcp-gee-sweet/issues/1323) (empty first sub-item reads back as a setext underline) and [#1324](https://github.com/khuisman/mcp-gee-sweet/issues/1324) (empty item holding only a block exports with a blank line, splitting the list); the last two from PR #1322's review. All `wrong/narrow`, observed. Bundled 2026-10-31 (maintainer) ([#1325](https://github.com/khuisman/mcp-gee-sweet/issues/1325))
577
577
 
578
578
  ### velociraptor — Parity 1 _(queued, label [`planned: velociraptor`](https://github.com/khuisman/mcp-gee-sweet/issues?q=is%3Aissue+label%3A%22planned%3A+velociraptor%22); gets a number when it goes in flight)_
579
579
 
@@ -163,6 +163,11 @@ class Paragraph:
163
163
  blockquote_path: tuple[int, ...] = field(default=(), compare=False) # #1098, see above Heading
164
164
  list_depth: int | None = None # #1094, see above Heading
165
165
  list_path: tuple[int, ...] = field(default=(), compare=False) # #1094, see above Heading
166
+ # A thematic break (`---`, `<hr>`, #1096): an empty paragraph (runs=[]) the emitter
167
+ # draws as a horizontal line, since the Docs API can't insert a horizontalRule
168
+ # element. doc_to_ast.py reads both that stand-in and a native horizontal line back
169
+ # as one, and ast_to_markdown.py writes it as a rule.
170
+ rule: bool = False
166
171
 
167
172
 
168
173
  @dataclass
@@ -65,6 +65,13 @@ _LEADING_ORDERED_MARKER = re.compile(r"^(\d{1,9})([.)])(?=[ \t]|$)")
65
65
  # rules can't occur: _escape has already escaped both (#993).
66
66
  _LEADING_RULE_LINE = re.compile(r"^(?:=+[ \t]*|-[- \t]*)$")
67
67
 
68
+ # A thematic break (#1096). Every block is separated by a blank line, so "---"
69
+ # never sits under a paragraph line, where it would be a setext underline. As the
70
+ # document's first line it would open YAML front matter, which the Markdown import
71
+ # drops (D3, #1173) when what follows reads as a mapping, so there it's "***".
72
+ _RULE = "---"
73
+ _FIRST_LINE_RULE = "***"
74
+
68
75
  _NESTED_TABLE_PLACEHOLDER = (
69
76
  "*(nested table omitted — Markdown tables can't contain a table; "
70
77
  "use get_doc_structure for full fidelity)*"
@@ -78,18 +85,10 @@ def ast_to_markdown(nodes: list[DocNode], comments: list[dict] | None = None) ->
78
85
  open_items = _OpenItems()
79
86
  for node in nodes:
80
87
  rendered = open_items.render(node, prev_node)
88
+ if prev_node is None and rendered == _RULE:
89
+ rendered = _FIRST_LINE_RULE
81
90
  if prev_node is not None:
82
- # Consecutive list items get a single newline, not a blank line, so
83
- # they render as one "tight" list (CommonMark still parses a blank-
84
- # line-separated run as the same list, just needlessly "loose" —
85
- # no correctness difference, only readability of the output).
86
- same_list_run = isinstance(node, BulletItem) and isinstance(prev_node, BulletItem)
87
- if same_list_run:
88
- parts.append("\n")
89
- elif open_items.shared_quote:
90
- parts.append(f"\n{'>' * open_items.shared_quote}\n")
91
- else:
92
- parts.append("\n\n")
91
+ parts.append(open_items.separator)
93
92
  parts.append(rendered)
94
93
  prev_node = node
95
94
 
@@ -106,7 +105,7 @@ def ast_to_markdown(nodes: list[DocNode], comments: list[dict] | None = None) ->
106
105
 
107
106
  class _OpenItems:
108
107
  """Renders each node, writing a block inside a list item (its `list_depth`,
109
- #1302) under that item instead of at the margin.
108
+ #1302) under that item instead of at the margin, and picks the separator before it.
110
109
 
111
110
  CommonMark keeps a block in an item when it's indented to the item's content
112
111
  column, the width of the item's own indent and marker. So every list item
@@ -117,32 +116,57 @@ class _OpenItems:
117
116
  A quoted item's quote markers come before its indent ("> - a"). A block inside
118
117
  it is inside that quote too, whatever depth it reads with (a table reads none),
119
118
  so the item's quote levels go outside the block's indent and only its own extra
120
- levels inside ("> - a" then "> > b"). The blank line
121
- before such a block, and before the item after it, keeps those shared quote
122
- levels (">"), or it would end the quote the list is in.
123
-
124
- After `render`, `shared_quote` is how many quote levels the node shares with
125
- the one before it, which the blank line between them must carry."""
119
+ levels inside ("> - a" then "> > b").
120
+
121
+ After `render`, `separator` is what goes between the node and the one before it:
122
+ a newline between two items at the same quote depth, so a list stays tight (items at
123
+ different depths are different lists, "- b" then "> - qa"); a newline between an
124
+ empty item and its first block, since CommonMark ends an item that starts with a
125
+ blank line there (PR #1326 QA round 1); otherwise a blank line carrying the quote
126
+ levels both nodes write at the margin, so it doesn't end a quote they share. Two
127
+ adjacent quoted blocks in Docs are one quote: Docs draws their left borders as one
128
+ bar, and emitter.py puts an unquoted separator paragraph between two separate
129
+ quotes (#1098), which reads back as a node of its own with no quote."""
126
130
 
127
131
  def __init__(self) -> None:
128
132
  # Per open item depth: (content column, blockquote_depth).
129
133
  self._items: list[tuple[int, int]] = []
130
- self.shared_quote = 0
134
+ # Quote levels the last node wrote at the margin, before any indent.
135
+ self._margin_quote = 0
136
+ self.separator = "\n\n"
131
137
 
132
138
  def render(self, node: DocNode, prev_node: DocNode | None) -> str:
133
- self.shared_quote = 0
139
+ prev_quote = self._margin_quote
140
+ rendered = self._render(node, prev_node)
141
+ same_list = (
142
+ isinstance(node, BulletItem)
143
+ and isinstance(prev_node, BulletItem)
144
+ and node.blockquote_depth == prev_node.blockquote_depth
145
+ )
146
+ if same_list or _starts_empty_item(prev_node, node):
147
+ self.separator = "\n"
148
+ else:
149
+ shared = min(prev_quote, self._margin_quote)
150
+ self.separator = f"\n{'>' * shared}\n" if shared else "\n\n"
151
+ return rendered
152
+
153
+ def _render(self, node: DocNode, prev_node: DocNode | None) -> str:
134
154
  if isinstance(node, BulletItem):
135
155
  opens_sublist = _opens_sublist(node, prev_node)
136
- if self._items:
137
- self.shared_quote = min(node.blockquote_depth, self._items[0][1])
138
156
  del self._items[node.depth :]
139
157
  self._items.extend([(0, 0)] * (node.depth - len(self._items)))
140
158
  self._items.append((len(_item_prefix(node, opens_sublist)), node.blockquote_depth))
159
+ self._margin_quote = node.blockquote_depth
141
160
  return _render_block(node, opens_sublist=opens_sublist)
142
161
  list_depth = node.list_depth
143
162
  if list_depth is None or list_depth >= len(self._items):
144
163
  self._items.clear()
145
- return _render_table(node) if isinstance(node, Table) else _render_block(node)
164
+ if isinstance(node, Table):
165
+ # A table renders unquoted (_render_table writes no quote markers).
166
+ self._margin_quote = 0
167
+ return _render_table(node)
168
+ self._margin_quote = node.blockquote_depth
169
+ return _render_block(node)
146
170
  del self._items[list_depth + 1 :]
147
171
  column, item_quote = self._items[list_depth]
148
172
  # A block inside a quoted list's item is inside that list's quote too. Its
@@ -151,7 +175,7 @@ class _OpenItems:
151
175
  quote = 0 if isinstance(node, Table) else node.blockquote_depth
152
176
  quote = max(quote, item_quote)
153
177
  outer = item_quote
154
- self.shared_quote = outer
178
+ self._margin_quote = outer
155
179
  if isinstance(node, Table):
156
180
  body = _render_table(node)
157
181
  else:
@@ -160,6 +184,20 @@ class _OpenItems:
160
184
  return _blockquote(body, outer) if outer else body
161
185
 
162
186
 
187
+ def _starts_empty_item(prev_node: DocNode | None, node: DocNode) -> bool:
188
+ """Whether `node` is the first block inside `prev_node`, a list item with no text.
189
+ CommonMark lets an item start with at most one blank line, its marker's own, so
190
+ "-", a blank line, then " ---" is an empty item ended by a rule at the margin."""
191
+ return (
192
+ isinstance(prev_node, BulletItem)
193
+ and not isinstance(node, BulletItem)
194
+ and not prev_node.runs
195
+ and prev_node.checked is None
196
+ and prev_node.heading_level is None
197
+ and node.list_depth == prev_node.depth
198
+ )
199
+
200
+
163
201
  def _indent_lines(body: str, column: int) -> str:
164
202
  # Blank lines stay empty: CommonMark keeps them in the item either way.
165
203
  pad = " " * column
@@ -255,7 +293,9 @@ def _render_block(
255
293
  ) -> str:
256
294
  """`blockquote_depth` overrides the node's own, for a block whose outer quote
257
295
  levels its caller writes (see `_OpenItems`)."""
258
- if isinstance(node, Paragraph) and _is_code_block(node):
296
+ if isinstance(node, Paragraph) and node.rule:
297
+ body = _RULE
298
+ elif isinstance(node, Paragraph) and _is_code_block(node):
259
299
  body = _render_code_block(node)
260
300
  elif isinstance(node, Heading):
261
301
  body = f"{'#' * node.level} {_render_inline(node.runs)}".rstrip()
@@ -25,7 +25,7 @@ from .ast import (
25
25
  Run,
26
26
  Table,
27
27
  )
28
- from .emitter import _BLOCKQUOTE_INDENT_PT_PER_LEVEL, list_level_indents
28
+ from .emitter import _BLOCKQUOTE_INDENT_PT_PER_LEVEL, is_rule_style, list_level_indents
29
29
 
30
30
  _HEADING_LEVELS = {f"HEADING_{i}": i for i in range(1, 7)}
31
31
 
@@ -45,6 +45,10 @@ def document_to_ast(document: dict) -> list[DocNode]:
45
45
  for elem in document.get("body", {}).get("content", []):
46
46
  if "paragraph" in elem:
47
47
  para = elem["paragraph"]
48
+ pieces = _split_at_horizontal_rules(para)
49
+ if len(pieces) > 1:
50
+ nodes.extend(_paragraph_with_rules(para, pieces, document, list_counter, items))
51
+ continue
48
52
  node = _paragraph_to_node(para, document, list_counter)
49
53
  if isinstance(node, BulletItem):
50
54
  items.item(para["bullet"])
@@ -280,7 +284,90 @@ def _paragraph_to_node(
280
284
  )
281
285
  if named_style in ("TITLE", "SUBTITLE"):
282
286
  return NamedBlock(style_type=named_style, runs=runs, blockquote_depth=blockquote_depth)
283
- return Paragraph(runs=runs, blockquote_depth=blockquote_depth)
287
+ return Paragraph(
288
+ runs=runs, blockquote_depth=blockquote_depth, rule=_is_rule_paragraph(para, runs)
289
+ )
290
+
291
+
292
+ def _is_rule_paragraph(para: dict, runs: list[Run | Image]) -> bool:
293
+ """Whether a paragraph with no text is emitter.py's rule stand-in (#1096): an
294
+ empty paragraph with the rule's borders. A native horizontal line is split out
295
+ before this (_split_at_horizontal_rules)."""
296
+ return not runs and is_rule_style(para.get("paragraphStyle", {}))
297
+
298
+
299
+ def _split_at_horizontal_rules(para: dict) -> list[list[dict] | None]:
300
+ """The paragraph's elements split at each native horizontalRule (#1096), with
301
+ None standing for each rule; a single piece when it has none."""
302
+ pieces: list[list[dict] | None] = []
303
+ current: list[dict] = []
304
+ for pe in para.get("elements", []):
305
+ if "horizontalRule" in pe:
306
+ pieces.extend([current, None])
307
+ current = []
308
+ else:
309
+ current.append(pe)
310
+ pieces.append(current)
311
+ return pieces
312
+
313
+
314
+ def _paragraph_with_rules(
315
+ para: dict,
316
+ pieces: list[list[dict] | None],
317
+ document: dict,
318
+ list_counter: _ListCounter,
319
+ items: _ListItemContext,
320
+ ) -> list[DocNode]:
321
+ """A paragraph holding native horizontal lines (#1096), as a rule node for each
322
+ line and a node for the text between them, in order. Drive's import puts a line in
323
+ the same paragraph as text before or after it, a list item's included (checked
324
+ live: `[hr, "text after"]`, `["item one", hr]`).
325
+
326
+ The text keeps the paragraph's own type: a heading's is a heading, and a list
327
+ item's first text is the item, with a rule or text after it a block inside the
328
+ item (#1302). A list item with only a line is an empty item with the rule in it."""
329
+ bullet = para.get("bullet")
330
+ texts = [
331
+ i
332
+ for i, piece in enumerate(pieces)
333
+ if piece is not None and _paragraph_runs({**para, "elements": piece}, document)
334
+ ]
335
+ item: BulletItem | None = None
336
+ item_at = -1
337
+ if bullet:
338
+ item_at = texts[0] if texts else 0
339
+ node = _paragraph_to_node(
340
+ {**para, "elements": pieces[item_at] or []}, document, list_counter
341
+ )
342
+ assert isinstance(node, BulletItem)
343
+ item = node
344
+ out: list[DocNode] = []
345
+ for i, piece in enumerate(pieces):
346
+ if item is not None and i == item_at:
347
+ items.item(para["bullet"])
348
+ out.append(item)
349
+ continue
350
+ node: Heading | Paragraph | BulletItem | NamedBlock
351
+ if piece is None:
352
+ node = Paragraph(runs=[], rule=True)
353
+ elif i not in texts:
354
+ continue
355
+ elif item is not None:
356
+ node = Paragraph(runs=_paragraph_runs({**para, "elements": piece}, document))
357
+ else:
358
+ node = _paragraph_to_node({**para, "elements": piece}, document, list_counter)
359
+ assert not isinstance(node, BulletItem)
360
+ if item is not None and i > item_at:
361
+ # Inside the item: its depth and quote, not read from the paragraph's
362
+ # indents, which are the item's own.
363
+ node.list_depth = item.depth
364
+ node.blockquote_depth = item.blockquote_depth
365
+ else:
366
+ if isinstance(node, Paragraph) and node.rule:
367
+ node.blockquote_depth = _infer_blockquote_depth(para.get("paragraphStyle", {}))
368
+ items.block(node, para)
369
+ out.append(node)
370
+ return out
284
371
 
285
372
 
286
373
  def _nesting_level(bullet: dict, document: dict) -> dict | None:
@@ -350,8 +437,10 @@ def _paragraph_runs(para: dict, document: dict) -> list[Run | Image]:
350
437
  image = _resolve_inline_image(ioe.get("inlineObjectId"), document, link_url)
351
438
  if image is not None:
352
439
  items.append(image)
353
- # footnoteReference, horizontalRule, pageBreak, columnBreak, autoText,
354
- # richLink, person, equation: no Markdown equivalent, silently skipped.
440
+ # footnoteReference, pageBreak, columnBreak, autoText, richLink, person,
441
+ # equation: no Markdown equivalent, silently skipped. A horizontalRule
442
+ # never gets here: document_to_ast splits it out as a rule node first
443
+ # (_split_at_horizontal_rules, #1096).
355
444
 
356
445
  # Every paragraph's content ends with a trailing "\n" (Docs API convention)
357
446
  # — strip exactly one from the end of the last Run, dropping it entirely if