mcp-gee-sweet 0.9.2.dev283__tar.gz → 0.9.2.dev286__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/amy.md +3 -1
  2. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/PKG-INFO +1 -1
  3. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/docs_content.md +56 -1
  4. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/roadmap.md +1 -1
  5. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/content.py +73 -64
  6. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/emitter.py +8 -4
  7. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/html_parser.py +6 -3
  8. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/images.py +28 -11
  9. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_content.py +80 -0
  10. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_core.py +76 -42
  11. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_images.py +39 -0
  12. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_tables.py +13 -0
  13. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/cleanup-worktrees.md +0 -0
  14. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/merge-pr.md +0 -0
  15. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/notify-kai.md +0 -0
  16. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/notify-partner.md +0 -0
  17. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/orchestrator.md +0 -0
  18. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/prep-for-pr.md +0 -0
  19. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/qa-kickoff.md +0 -0
  20. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/release.md +0 -0
  21. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/retro.md +0 -0
  22. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/roadmap-status.md +0 -0
  23. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/start-worktree.md +0 -0
  24. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/team-member.md +0 -0
  25. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/commands/verify-pr.md +0 -0
  26. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/settings.json +0 -0
  27. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/aziz.md +0 -0
  28. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/bob.md +0 -0
  29. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/dev.md +0 -0
  30. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/joy.md +0 -0
  31. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/kai.md +0 -0
  32. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.claude/team-roles/qa.md +0 -0
  33. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.dockerignore +0 -0
  34. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  35. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  36. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  37. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/dependabot.yml +0 -0
  38. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/workflows/ci.yml +0 -0
  39. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/workflows/docs.yml +0 -0
  40. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/workflows/publish-dev.yml +0 -0
  41. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.github/workflows/release.yml +0 -0
  42. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.gitignore +0 -0
  43. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.pre-commit-config.yaml +0 -0
  44. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/.python-version +0 -0
  45. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/CHANGELOG.md +0 -0
  46. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/CLAUDE.md +0 -0
  47. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/CODE_OF_CONDUCT.md +0 -0
  48. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/CONTRIBUTING.md +0 -0
  49. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/Dockerfile +0 -0
  50. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/LICENSE +0 -0
  51. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/Makefile +0 -0
  52. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/README.md +0 -0
  53. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/SECURITY.md +0 -0
  54. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docker-compose.yml +0 -0
  55. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/assets/logo.png +0 -0
  56. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/auth.md +0 -0
  57. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/client-setup.md +0 -0
  58. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/configuration.md +0 -0
  59. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-async-tool-execution.md +0 -0
  60. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
  61. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-cache-invalidation.md +0 -0
  62. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-chart-theming.md +0 -0
  63. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-comments-first-class.md +0 -0
  64. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-composite-tools.md +0 -0
  65. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
  66. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
  67. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-dev-team.md +0 -0
  68. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-docs-formatting.md +0 -0
  69. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-fork.md +0 -0
  70. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-gmail-domain.md +0 -0
  71. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  72. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-local-fs-test-harness.md +0 -0
  73. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
  74. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-pillow-image-dependency.md +0 -0
  75. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-prompt-qa-role.md +0 -0
  76. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-publishing.md +0 -0
  77. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-release-docs-roles.md +0 -0
  78. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-repositioning.md +0 -0
  79. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  80. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
  81. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-review-finding-ticket-bar.md +0 -0
  82. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
  83. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-sync-folder-recursion.md +0 -0
  84. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-testing.md +0 -0
  85. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/decision-tool-error-visibility.md +0 -0
  86. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/index.md +0 -0
  87. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/decisions/role-file-history.md +0 -0
  88. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/blockquote-representation.md +0 -0
  89. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/borderless-table-columns.md +0 -0
  90. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/doc-to-markdown.md +0 -0
  91. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/docs-ast-pipeline.md +0 -0
  92. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/effectiveformat-spike.md +0 -0
  93. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/heading-anchor-resolution.md +0 -0
  94. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/image-conversion.md +0 -0
  95. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/index.md +0 -0
  96. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/markdown-support.md +0 -0
  97. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/module-history.md +0 -0
  98. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/oauth-reauthorize-from-tool-call.md +0 -0
  99. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/phase3-theme-system.md +0 -0
  100. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design/sse-consent-off-event-loop.md +0 -0
  101. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/design.md +0 -0
  102. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/index.md +0 -0
  103. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/known-limitations.md +0 -0
  104. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/notes-read.md +0 -0
  105. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/README.md +0 -0
  106. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
  107. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  108. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  109. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  110. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  111. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
  112. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
  113. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
  114. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
  115. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
  116. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
  117. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
  118. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
  119. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
  120. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
  121. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
  122. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
  123. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
  124. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/fixtures.template.md +0 -0
  125. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/gmail-test-plan.md +0 -0
  126. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/operations.yaml +0 -0
  127. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/playwright_oauth.md +0 -0
  128. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/.gitkeep +0 -0
  129. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-02.md +0 -0
  130. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-14.md +0 -0
  131. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-17.md +0 -0
  132. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-18.md +0 -0
  133. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
  134. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
  135. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-06-28.md +0 -0
  136. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-07-04-docs.md +0 -0
  137. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
  138. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-07-04-infra.md +0 -0
  139. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
  140. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-07-04.md +0 -0
  141. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/results/2026-09-04.md +0 -0
  142. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/retro-v0.8.0.md +0 -0
  143. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/retro-v0.9.0.md +0 -0
  144. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/run.md +0 -0
  145. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/runs/README.md +0 -0
  146. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/runs/v0.8.0.md +0 -0
  147. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/runs/v0.8.1.md +0 -0
  148. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/runs/v0.9.0.md +0 -0
  149. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/setup.md +0 -0
  150. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/calendar.md +0 -0
  151. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/docs_layout.md +0 -0
  152. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/docs_style.md +0 -0
  153. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/docs_tables.md +0 -0
  154. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/drive_activity.md +0 -0
  155. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/drive_files.md +0 -0
  156. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/drive_sharing.md +0 -0
  157. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/drive_transfer.md +0 -0
  158. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/gmail.md +0 -0
  159. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/infra.md +0 -0
  160. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/sheets_charts.md +0 -0
  161. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/sheets_mgmt.md +0 -0
  162. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/sheets_read.md +0 -0
  163. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/qa/tests/sheets_write.md +0 -0
  164. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/style-guide.md +0 -0
  165. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/docs/tools.md +0 -0
  166. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/mkdocs.yml +0 -0
  167. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/pyproject.toml +0 -0
  168. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/pyrightconfig.json +0 -0
  169. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/check_supports_all_drives.py +0 -0
  170. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/gen_tool_docs.py +0 -0
  171. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/lane_context_hook.py +0 -0
  172. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/oauth_setup.py +0 -0
  173. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/qa_cancel_insert_local_images.py +0 -0
  174. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/qa_gmail_fixtures.py +0 -0
  175. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/setup_team.sh +0 -0
  176. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/scripts/write_team_mcp_configs.sh +0 -0
  177. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/server.json +0 -0
  178. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/.env.template +0 -0
  179. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/__init__.py +0 -0
  180. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/auth.py +0 -0
  181. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/cache.py +0 -0
  182. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/http_transport.py +0 -0
  183. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/reauth.py +0 -0
  184. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/server.py +0 -0
  185. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/sse_shutdown.py +0 -0
  186. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  187. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/cache.py +0 -0
  188. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/calendar.py +0 -0
  189. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
  190. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  191. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
  192. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
  193. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +0 -0
  194. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
  195. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
  196. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
  197. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
  198. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  199. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
  200. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
  201. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  202. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  203. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  204. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  205. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
  206. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  207. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  208. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/gmail.py +0 -0
  209. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/pagination.py +0 -0
  210. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  211. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  212. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
  213. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
  214. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
  215. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/__init__.py +0 -0
  216. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/conftest.py +0 -0
  217. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/drive/__init__.py +0 -0
  218. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/drive/test_activity.py +0 -0
  219. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/drive/test_files.py +0 -0
  220. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/drive/test_sharing.py +0 -0
  221. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/drive/test_transfer.py +0 -0
  222. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/fakes.py +0 -0
  223. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/integration/__init__.py +0 -0
  224. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/integration/conftest.py +0 -0
  225. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/integration/test_local_fs_harness.py +0 -0
  226. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/sheets/__init__.py +0 -0
  227. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/sheets/test_data.py +0 -0
  228. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/sheets/test_helpers.py +0 -0
  229. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/sheets/test_structure.py +0 -0
  230. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_auth.py +0 -0
  231. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_cache.py +0 -0
  232. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_calendar.py +0 -0
  233. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_check_supports_all_drives.py +0 -0
  234. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_concurrency.py +0 -0
  235. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_anchors.py +0 -0
  236. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_comments.py +0 -0
  237. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_editing.py +0 -0
  238. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_indices.py +0 -0
  239. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_layout.py +0 -0
  240. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_markdown_export.py +0 -0
  241. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_named_ranges.py +0 -0
  242. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_docs_style.py +0 -0
  243. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_gen_tool_docs.py +0 -0
  244. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_gmail.py +0 -0
  245. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_lane_context_hook.py +0 -0
  246. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_qa_gmail_fixtures.py +0 -0
  247. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_reauth.py +0 -0
  248. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_response_limits.py +0 -0
  249. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_server.py +0 -0
  250. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_server_json.py +0 -0
  251. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_sse_shutdown.py +0 -0
  252. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/tests/test_team_mcp_configs.py +0 -0
  253. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev286}/uv.lock +0 -0
@@ -2,7 +2,9 @@ Tech Writer (Amy). Reached via `/team-member Amy` after that command's shared is
2
2
 
3
3
  Amy keeps documentation accurate, friendly, comprehensive, and fun — README, `docs/`, tool docstrings surfaced to users, and QA test docs' clarity (not their results).
4
4
 
5
- - **On `team/amy` (idle):** claim work the same way Ash/Jay do, but from the `documentation` label instead of `ready-for-development`: `gh issue list --repo khuisman/mcp-gee-sweet --label documentation --state open --json number,title,body --jq 'sort_by(.number)'`. Filter out issues already claimed by an open PR the same way Dev roles do. Before picking one, check whether any open issues describe the same underlying problem from different angles (e.g. several tickets all pointing at one stale doc file) — if so, consolidate into a single new issue, close the redundant ones as `not planned` with a comment linking to the survivor, and work the consolidated issue instead of the originals (see #303 for precedent). Once duplicates are consolidated, pick the highest-priority remaining issue by this order, fixed so the choice doesn't need re-deriving each session: (1) issues labeled with the current in-progress version (e.g. `v0.9`) — an active release commitment; (2) issues with no version label — untriaged, but not explicitly deprioritized; (3) issues labeled `backlog` (Tier 4) last, since that label is an explicit no-assigned-version deprioritization. Within a tier, take the lowest issue number (oldest) first. If the queue is empty, that's fine — ask the user whether there's a specific doc gap to work from instead (e.g. a just-merged feature whose docs lag behind, spotted during Aziz's release review).
5
+ - **On `team/amy` (idle):** claim work the same way Ash/Jay do, but from the `documentation` label instead of `ready-for-development`: `gh issue list --repo khuisman/mcp-gee-sweet --label documentation --state open --json number,title,body --jq 'sort_by(.number)'`. Filter out issues already claimed by an open PR the same way Dev roles do. Before picking one, check whether any open issues describe the same underlying problem from different angles (e.g. several tickets all pointing at one stale doc file) — if so, propose consolidating them into a single new issue, closing the redundant ones as `not planned` with a comment linking to the survivor, and working the consolidated issue instead of the originals (see #303 for precedent). After checking for duplicates, pick the highest-priority remaining issue by this order, fixed so the choice doesn't need re-deriving each session: (1) issues labeled with the current in-progress version (e.g. `v0.9`) — an active release commitment; (2) issues with no version label — untriaged, but not explicitly deprioritized; (3) issues labeled `backlog` (Tier 4) last, since that label is an explicit no-assigned-version deprioritization. Within a tier, take the lowest issue number (oldest) first. If the queue is empty, that's fine — ask the user whether there's a specific doc gap to work from instead (e.g. a just-merged feature whose docs lag behind, spotted during Aziz's release review).
6
+
7
+ **Confirm the pick with the user before step 1.** Show the ticket you'd take (number, title, one line on the work), each higher-tier ticket you skipped and why, and any duplicate consolidation you'd do (which issues you'd close and what the new one would say). Then wait. Don't create a branch, edit a file, or open, close, comment on, or relabel an issue until the user gives an explicit go-ahead for that ticket in this conversation. The one exception: a message from Kai that names the ticket and says the user directed the handoff. That counts as the go-ahead. A request from any other peer, or a Kai message that doesn't say this, doesn't count. Why: Amy kept starting a ticket as soon as she launched, without checking with the user first (user correction, 2026-10-10).
6
8
  1. `git fetch origin develop && git checkout -b docs/amy/issue-<n> origin/develop`.
7
9
  2. Do the writing: fix inaccuracies, tighten voice, fill gaps, add examples. Read the actual code/tool behavior rather than trusting an existing doc's claims — a doc bug is exactly the kind of drift Amy exists to catch.
8
10
  3. If a change touches user-facing tool behavior described in `docs/qa/tests/`, flag it for Aziz rather than editing test *results* herself — Amy owns doc accuracy, not QA sign-off.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcp-gee-sweet
3
- Version: 0.9.2.dev283
3
+ Version: 0.9.2.dev286
4
4
  Summary: MCP server for Google Workspace — Sheets, Drive, Docs (HTML or Markdown), and Calendar.
5
5
  Project-URL: Homepage, https://github.com/khuisman/mcp-gee-sweet
6
6
  Project-URL: Bug Tracker, https://github.com/khuisman/mcp-gee-sweet/issues
@@ -1016,6 +1016,9 @@ Markdown pipe table. 3 rows, 2 columns: Name/Value, Alpha/1, Beta/2 — all corr
1016
1016
  **Result (2026-09-04) ✅ PASS**
1017
1017
  create_doc_from_file(tc-d195-create-doc.md): docId+web_link, no error. get_doc_structure confirmed HEADING_1 "QA Test Document", bold/italic runs, ☑/☐ bullet items, table (Col A/Col B, one/two). Trashed.
1018
1018
 
1019
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
1020
+ create_doc_from_file(tc-d195-create-doc.md): docId+web_link, no error. get_doc_structure: HEADING_1 "QA Test Document", bold/italic runs, ☑/☐/Plain item bullets, Col A/Col B table. Playwright (account check passed): heading, bold/italic, checkbox glyphs, and table render correctly. Trashed.
1021
+
1019
1022
  ---
1020
1023
 
1021
1024
  ### TC-DOC45: `create_doc_from_file` with a local .html file ⚠️ requires-oauth ⚠️ destructive
@@ -1036,6 +1039,9 @@ create_doc_from_file(tc-d195-create-doc.md): docId+web_link, no error. get_doc_s
1036
1039
  **Result (2026-09-04) ✅ PASS**
1037
1040
  create_doc_from_file(tc-d196-create-doc.html): docId+web_link, no error. get_doc_structure confirmed HEADING_2 "From HTML file", paragraph "Content paragraph." Trashed.
1038
1041
 
1042
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
1043
+ create_doc_from_file(tc-d196-create-doc.html): docId+web_link, no error. get_doc_structure: HEADING_2 "From HTML file", paragraph "Content paragraph." Trashed.
1044
+
1039
1045
  ---
1040
1046
 
1041
1047
  ### TC-DOC46: `create_doc_from_file` file not found
@@ -1051,6 +1057,9 @@ create_doc_from_file(tc-d196-create-doc.html): docId+web_link, no error. get_doc
1051
1057
  **Result (2026-09-04) ✅ PASS**
1052
1058
  create_doc_from_file('~/does-not-exist.md') returned {"error": "File not found: ~/does-not-exist.md"}, no exception.
1053
1059
 
1060
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
1061
+ create_doc_from_file('~/does-not-exist.md') returned {"error": "File not found: ~/does-not-exist.md"}, no exception. Unsupported .txt also still returns the no-hint message "Use .md or .html/.htm" (shared helper, format_overridable=False).
1062
+
1054
1063
  ---
1055
1064
 
1056
1065
  ### TC-DOC47: `write_doc_content` inline code monospace ⚠️ destructive
@@ -2804,6 +2813,9 @@ insert_local_images (default revoke_sharing) on scratch doc with "Marker: IMGMAR
2804
2813
  **Result (2026-09-04) ✅ PASS**
2805
2814
  create_doc HTML table with image in a cell: no error. get_doc_structure: cell 0 "Before", cell 2 "After", cell 1 (image) empty (""). No images key in response (image dropped at parse time, table-cell images still out of scope). No timeout/hang. Trashed.
2806
2815
 
2816
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
2817
+ create_doc HTML table with an image in a cell: returned at once, no error, no `images` key. get_doc_structure: cell 0 "Before", cell 1 "", cell 2 "After". No hang. Trashed.
2818
+
2807
2819
  ---
2808
2820
 
2809
2821
  ### TC-DOC154: A `<pre>` block resumed after a nested table doesn't leave a spurious trailing whitespace-only paragraph ⚠️ requires-oauth ⚠️ destructive
@@ -3107,6 +3119,9 @@ Generated real ~76MB 4800x5000 (24.0MP, under 25MP pre-check) noise PNG. Main ca
3107
3119
  **Result (2026-09-04) ✅ PASS**
3108
3120
  update_doc_from_file(tc-d195-create-doc.md) on DOC_ID: returned docId matching DOC_ID, no error. get_doc_structure matches TC-DOC44's create_doc_from_file output exactly (HEADING_1, bold/italic runs, ☑/☐ items, Col A/Col B table). get_file_metadata before/after identical name/parents/webViewLink — doc identity/location preserved.
3109
3121
 
3122
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
3123
+ update_doc_from_file(tc-d195-create-doc.md) on DOC_ID: docId matched, web_link present, no error. get_doc_structure matches TC-DOC44's output (HEADING_1, bold/italic, ☑/☐ items, Col A/Col B table). get_file_metadata before/after: same name/parents/webViewLink.
3124
+
3110
3125
  ### TC-DOC169: `update_doc_from_file` with a local .html file ⚠️ destructive
3111
3126
  **Prompt**
3112
3127
  > "Update Google Doc {DOC_ID} from the file <repo-root>/docs/qa/fixtures/tc-d196-create-doc.html"
@@ -3123,6 +3138,9 @@ update_doc_from_file(tc-d195-create-doc.md) on DOC_ID: returned docId matching D
3123
3138
  **Result (2026-09-04) ✅ PASS**
3124
3139
  update_doc_from_file(tc-d196-create-doc.html): docId matched, no error. get_doc_structure: HEADING_2 "From HTML file", paragraph "Content paragraph." — matches TC-DOC45.
3125
3140
 
3141
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
3142
+ update_doc_from_file(tc-d196-create-doc.html): docId matched, no error. get_doc_structure: HEADING_2 "From HTML file", paragraph "Content paragraph."
3143
+
3126
3144
  ### TC-DOC170: `update_doc_from_file` file not found
3127
3145
  **Prompt**
3128
3146
  > "Update Google Doc {DOC_ID} from the file ~/does-not-exist.md"
@@ -3137,6 +3155,9 @@ update_doc_from_file(tc-d196-create-doc.html): docId matched, no error. get_doc_
3137
3155
  **Result (2026-09-04) ✅ PASS**
3138
3156
  update_doc_from_file('~/does-not-exist.md') returned {"error":"File not found: ~/does-not-exist.md"}, no exception. get_doc_content confirmed doc unchanged (still HEADING_2/paragraph from DOC169).
3139
3157
 
3158
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
3159
+ update_doc_from_file('~/does-not-exist.md') returned {"error": "File not found: ~/does-not-exist.md"}, no exception. get_doc_content unchanged.
3160
+
3140
3161
  ### TC-DOC171: `update_doc_from_file` unsupported extension with no override returns an error, doc untouched
3141
3162
  **Setup:** create a local file `qa-update.txt` with arbitrary plain text
3142
3163
 
@@ -3155,6 +3176,9 @@ update_doc_from_file('~/does-not-exist.md') returned {"error":"File not found: ~
3155
3176
  **Result (2026-09-04) ✅ PASS**
3156
3177
  update_doc_from_file(qa-update.txt, no override) returned {"error":"Unsupported file extension '.txt'. Use .md or .html/.htm, or pass content_format explicitly."}. get_doc_content confirmed doc untouched — no Docs API call made.
3157
3178
 
3179
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
3180
+ update_doc_from_file(qa-update.txt) returned {"error": "Unsupported file extension '.txt'. Use .md or .html/.htm, or pass content_format explicitly."}. get_doc_content unchanged.
3181
+
3158
3182
  ### TC-DOC172: `update_doc_from_file` `content_format` explicitly overrides an unrecognized extension ⚠️ destructive
3159
3183
  **Setup:** create a local file `qa-update-override.txt` containing `# Overridden Heading\n\nParagraph text.\n`
3160
3184
 
@@ -3171,6 +3195,9 @@ update_doc_from_file(qa-update.txt, no override) returned {"error":"Unsupported
3171
3195
  **Result (2026-09-04) ✅ PASS**
3172
3196
  update_doc_from_file(qa-update-override.txt, content_format='markdown') succeeded, no error. get_doc_structure: HEADING_1 "Overridden Heading", paragraph "Paragraph text." Fixture restored after.
3173
3197
 
3198
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
3199
+ update_doc_from_file(qa-update-override.txt, content_format='markdown'): no error. get_doc_structure: HEADING_1 "Overridden Heading", paragraph "Paragraph text." Fixture restored to setup.md's seed HTML via write_doc_content afterward.
3200
+
3174
3201
  ---
3175
3202
 
3176
3203
  ## `get_doc_as_markdown`
@@ -3747,12 +3774,15 @@ Tool call: `insert_local_images(doc_id={DOC_ID}, images=[{"marker": "IMGMARKERBO
3747
3774
  **Checks**
3748
3775
  - `results[0]` has no `fileId` and an `error` containing "14000x13000 (182.0 megapixels)" and "25 megapixels"
3749
3776
  - No `qa-bomb.png` appears in `{FOLDER_ID}`, and the marker text is unchanged
3750
- - Re-run with `auto_downscale=True`: `results[0]` still has no `fileId`, and its `error` ends with "Could not auto-downscale it (unreadable or corrupt image, animated, or too large to decode safely)."
3777
+ - Re-run with `auto_downscale=True`: `results[0]` still has no `fileId`, and its `error` ends with "Could not auto-downscale it (unreadable or corrupt image, animated, too large to decode safely, or still over the file-size limit after shrinking)." (the last reason was added by #894/#581)
3751
3778
 
3752
3779
  **Cleanup:** trash the doc; `rm /tmp/qa-bomb.png`
3753
3780
 
3754
3781
  **Result (2026-09-24) ✅ PASS — Kit, PR #801 round 2 (fix `4d5f842`, issue #560).** Both checks matched exactly, and nothing was uploaded. (Round 1 reproduced the pre-fix behavior locally: `check_image_file` returned `None` for this file.) Doc trashed.
3755
3782
 
3783
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
3784
+ insert_local_images with the 14000x13000 PNG: results[0] no fileId, error names "14000x13000 (182.0 megapixels)" and "25 megapixels". With auto_downscale=True: error ends with "Could not auto-downscale it (unreadable or corrupt image, animated, too large to decode safely, or still over the file-size limit after shrinking)." No qa-bomb.png in FOLDER_ID; marker text unchanged. Doc trashed.
3785
+
3756
3786
  ---
3757
3787
 
3758
3788
  ### TC-DOC198: `insert_local_images` — an unreadable local file fails with "failed to read local file", not a raw upload error (PR #801)
@@ -3935,3 +3965,28 @@ Two extra probes ran from a scratch copy of the script (not committed):
3935
3965
  All files from both probes and the three steps, and the doc, were trashed.
3936
3966
 
3937
3967
  **Result (2026-10-04) ✅ PASS — Kit, PR #903 round 2 (`4e392c3`).** All six steps ran from the kit worktree with its OAuth token, on a fresh scratch doc in `{FOLDER_ID}`. Each step printed `cancelled at '<point>'; the tool returned nothing` and the WARNING `Doc edit for {DOC_ID} failed or was cancelled after its images were resolved; files created by this call: <one file ID>; pre-existing files: none; still link-shared by this call: ...`. That last field listed `{KEPT_FILE_ID}` for step 4 and was `none` for every other step. Steps 2 (`share-fail`, the orphan) and 6 (`revoke`), which logged nothing in round 1's probe, now log. `list_permissions` showed no `anyone` grant on `{SHARE_FILE_ID}`, `{ORPHAN_FILE_ID}`, `{EDIT_FILE_ID}`, `{INFLIGHT_FILE_ID}`, or `{REVOKE_FILE_ID}`, and showed `anyoneWithLink`/`reader` on `{KEPT_FILE_ID}`. After `refresh_cache`, `get_doc_as_markdown` read `Before`, `IMGMARKER883`, an image, an image, `After`. So steps 1–4 left `IMGMARKER883` untouched, step 6's edit landed before its cancel, and step 5's in-flight edit also landed (an image replaced `INFLIGHT883`). All six files and the doc were trashed.
3968
+
3969
+ ## Unreadable source file returns an error — `create_doc_from_file` / `update_doc_from_file` (PR #961)
3970
+
3971
+ Both tools read the local file through the shared `_read_doc_source_file` helper. Before PR #961's round 2, a path that existed but wasn't a readable UTF-8 regular file raised out of the tool (`[Errno 21] Is a directory`, `'utf-8' codec can't decode byte 0xe9 ...`) instead of returning `{"error": ...}`.
3972
+
3973
+ ### TC-DOC208: A directory or non-UTF-8 source file returns `{"error": ...}`, and nothing is created or edited
3974
+ **Setup:**
3975
+ - `mkdir -p /tmp/tc-doc208/dir.md`
3976
+ - `python3 -c "open('/tmp/tc-doc208/latin1.html','wb').write('<p>caf\xe9</p>'.encode('latin-1'))"`
3977
+ - `get_doc_content(file_id={DOC_ID})`, note the content for comparison afterward
3978
+
3979
+ **Steps**
3980
+ 1. `create_doc_from_file(local_path="/tmp/tc-doc208/dir.md", folder_id={FOLDER_ID})`
3981
+ 2. `create_doc_from_file(local_path="/tmp/tc-doc208/latin1.html", folder_id={FOLDER_ID})`
3982
+ 3. `update_doc_from_file(doc_id={DOC_ID}, local_path="/tmp/tc-doc208/dir.md")`
3983
+ 4. `update_doc_from_file(doc_id={DOC_ID}, local_path="/tmp/tc-doc208/latin1.html")`
3984
+
3985
+ **Checks**
3986
+ - Each call returns a dict with an `error` key naming the path (any wording). None comes back as `Error executing tool ...`
3987
+ - `list_files(folder_id={FOLDER_ID})` shows no new Doc named `dir` or `latin1` (the filename stem `create_doc_from_file` uses as the default title; steps 1–2 fail before the Drive `create`)
3988
+ - `get_doc_content(file_id={DOC_ID})` matches the Setup snapshot (steps 3–4 fail before any Docs API call)
3989
+
3990
+ **Cleanup:** `rm -rf /tmp/tc-doc208`
3991
+
3992
+ **Result (2026-10-10) ✅ PASS — Sky, PR #961 round 2 (`849f0bb`).** Ran with the fixtures under this job's scratch dir instead of `/tmp/tc-doc208`. Steps 1 and 3 returned `{"error": "Not a regular file: <path>/dir.md"}`; steps 2 and 4 returned `{"error": "File is not valid UTF-8: <path>/latin1.html (invalid continuation byte at byte 6)"}`. No call surfaced as `Error executing tool`. `list_files({FOLDER_ID})` showed only the three standing fixtures. After `refresh_cache`, `get_doc_content({DOC_ID})` matched the Setup snapshot. Round 1 reproduced both raises on the pre-fix code (`075ce99`).
@@ -351,7 +351,7 @@ Requires `gmail/v1` client and `https://www.googleapis.com/auth/gmail.modify` sc
351
351
  - [ ] Define the correctness rubric and own execution for the composite-vs-primitive efficiency benchmark — Aziz ([#584](https://github.com/khuisman/mcp-gee-sweet/issues/584))
352
352
  - [ ] QA `.env` has no single source of truth — `setup_team.sh` only copies `.env` into a worktree when absent, so fixture-ID changes never propagate (had to hand-sync 9 copies for #305); also `run.md` vs `setup.md` disagree on `docs/qa/.env` vs repo-root `.env`. Surfaced by PR #677, relates to [#648](https://github.com/khuisman/mcp-gee-sweet/issues/648) ([#679](https://github.com/khuisman/mcp-gee-sweet/issues/679))
353
353
  - [x] `list_files`/`get_file_metadata` docstrings cite a stale `upload_local_file` `modifiedTime` caveat (fixed at the root by PR #472) and carry a broken self-referential contrast — from Bob's `docs/tools.md` docstring sweep; Dev/QA track, batches with #682/#683 ([#681](https://github.com/khuisman/mcp-gee-sweet/issues/681)) (PR 898)
354
- - [ ] `create_doc` family docstrings name only the ~25MP inline-image limit; the ~50MB byte-size limit (#562) is equally enforced, and 3 deferring docstrings inherit the gap — from Bob's `docs/tools.md` docstring sweep. Routed to Amy (`documentation`); closed community PR #932 has a diff ([#682](https://github.com/khuisman/mcp-gee-sweet/issues/682))
354
+ - [x] `create_doc` family docstrings name only the ~25MP inline-image limit; the ~50MB byte-size limit (#562) is equally enforced, and 3 deferring docstrings inherit the gap — from Bob's `docs/tools.md` docstring sweep. Routed to Amy (`documentation`); closed community PR #932 has a diff ([#682](https://github.com/khuisman/mcp-gee-sweet/issues/682)) (PR 970)
355
355
  - [ ] `qa(docs)`: no comprehensive fixture exercising every Docs formatting construct in one pass — Aziz, triaged out of `backlog` 2026-09-11 ([#477](https://github.com/khuisman/mcp-gee-sweet/issues/477))
356
356
  - [ ] Fact-check and word competitive/benchmark claims once real benchmark numbers exist — routed to Amy, triaged out of `backlog` 2026-09-11 ([#585](https://github.com/khuisman/mcp-gee-sweet/issues/585))
357
357
  - [x] QA test-doc cross-reference cleanup after the `drive.md`/`docs.md` split (PR #582) — triaged out of `backlog` 2026-09-11 ([#590](https://github.com/khuisman/mcp-gee-sweet/issues/590)) (PR #851)
@@ -13,7 +13,7 @@ from markdown.inlinepatterns import InlineProcessor
13
13
  from mcp.server.mcpserver import Context
14
14
  from mcp.types import ToolAnnotations
15
15
 
16
- from ...auth import execute_in_thread
16
+ from ...auth import SpreadsheetContext, execute_in_thread
17
17
  from ...cache import CACHE_VALIDATE_MODIFIED_TIME
18
18
  from ..drive import _SA_QUOTA_ERROR
19
19
  from ..drive.transfer import _GOOGLE_DOC_MIME
@@ -266,14 +266,45 @@ async def _resolve_image_source(
266
266
  )
267
267
 
268
268
 
269
+ _SOURCE_FILE_FORMATS = {".md": "markdown", ".html": "html", ".htm": "html"}
270
+
271
+
272
+ def _read_doc_source_file(
273
+ local_path: str, content_format: str | None, *, format_overridable: bool
274
+ ) -> tuple[str, str] | dict[str, Any]:
275
+ """Shared file reading for create_doc_from_file / update_doc_from_file (#567):
276
+ returns (content, content_format), or {"error": ...} for a missing file, a path
277
+ that isn't a regular file, an unreadable or non-UTF-8 file, or an extension with
278
+ no inferable format. content_format=None infers it from the extension.
279
+ format_overridable says whether the calling tool takes a content_format
280
+ parameter, so the error only suggests one when it exists."""
281
+ path = Path(local_path)
282
+ if not path.exists():
283
+ return {"error": f"File not found: {local_path}"}
284
+ if not path.is_file():
285
+ return {"error": f"Not a regular file: {local_path}"}
286
+ if content_format is None:
287
+ ext = path.suffix.lower()
288
+ inferred = _SOURCE_FILE_FORMATS.get(ext)
289
+ if inferred is None:
290
+ hint = ", or pass content_format explicitly." if format_overridable else ""
291
+ return {"error": f"Unsupported file extension '{ext}'. Use .md or .html/.htm{hint}"}
292
+ content_format = inferred
293
+ try:
294
+ content = path.read_text(encoding="utf-8")
295
+ except UnicodeDecodeError as e:
296
+ return {"error": f"File is not valid UTF-8: {local_path} ({e.reason} at byte {e.start})"}
297
+ except OSError as e:
298
+ return {"error": f"Could not read file {local_path}: {e.strerror or e}"}
299
+ return content, content_format
300
+
301
+
269
302
  async def _replace_doc_content(
270
- docs_service,
271
- drive_service,
272
- doc_cache,
273
- folder_id: str | None,
303
+ lc: SpreadsheetContext,
274
304
  doc_id: str,
275
305
  content: str,
276
306
  content_format: str,
307
+ *,
277
308
  autolink_urls: bool,
278
309
  revoke_sharing: bool,
279
310
  auto_downscale: bool,
@@ -283,7 +314,14 @@ async def _replace_doc_content(
283
314
  the clear-then-insert mechanism (including the same-batch insertText
284
315
  style-inheritance quirk below) has exactly one implementation instead of two
285
316
  copies that could drift out of sync.
317
+
318
+ The three flags are keyword-only (#567), here and in _apply_doc_content: as
319
+ positional bools, swapping two at a call site would type-check and silently do
320
+ the wrong thing.
286
321
  """
322
+ docs_service = lc.docs_service
323
+ drive_service = lc.drive_service
324
+ doc_cache = lc.doc_cache
287
325
  doc = await execute_in_thread(
288
326
  docs_service.documents().get(documentId=doc_id).execute,
289
327
  docs_service,
@@ -330,10 +368,10 @@ async def _replace_doc_content(
330
368
  doc_id,
331
369
  content,
332
370
  content_format,
333
- autolink_urls,
334
- folder_id,
335
- revoke_sharing,
336
- auto_downscale,
371
+ autolink_urls=autolink_urls,
372
+ target_folder_id=lc.folder_id,
373
+ revoke_sharing=revoke_sharing,
374
+ auto_downscale=auto_downscale,
337
375
  )
338
376
  except DocEditError as e:
339
377
  # The clear above already committed, so the doc changed either way.
@@ -382,6 +420,7 @@ async def _apply_doc_content(
382
420
  doc_id: str,
383
421
  content: str,
384
422
  content_format: str,
423
+ *,
385
424
  autolink_urls: bool,
386
425
  target_folder_id: str | None,
387
426
  revoke_sharing: bool,
@@ -932,10 +971,10 @@ def register(tool):
932
971
  doc_id,
933
972
  content,
934
973
  content_format,
935
- autolink_urls,
936
- target_folder_id,
937
- revoke_sharing,
938
- auto_downscale,
974
+ autolink_urls=autolink_urls,
975
+ target_folder_id=target_folder_id,
976
+ revoke_sharing=revoke_sharing,
977
+ auto_downscale=auto_downscale,
939
978
  )
940
979
  except DocEditError as e:
941
980
  if target_folder_id:
@@ -1010,20 +1049,11 @@ def register(tool):
1010
1049
  Requires OAuth or ADC auth. Service accounts cannot create files in personal
1011
1050
  Drive (no storage quota). Check server://auth-status for your current auth method.
1012
1051
  """
1013
- path = Path(local_path)
1014
- if not path.exists():
1015
- return {"error": f"File not found: {local_path}"}
1016
-
1017
- ext = path.suffix.lower()
1018
- if ext == ".md":
1019
- content_format = "markdown"
1020
- elif ext in (".html", ".htm"):
1021
- content_format = "html"
1022
- else:
1023
- return {"error": f"Unsupported file extension '{ext}'. Use .md or .html/.htm"}
1024
-
1025
- content = path.read_text(encoding="utf-8")
1026
- doc_title = title or path.stem
1052
+ source = _read_doc_source_file(local_path, None, format_overridable=False)
1053
+ if isinstance(source, dict):
1054
+ return source
1055
+ content, content_format = source
1056
+ doc_title = title or Path(local_path).stem
1027
1057
 
1028
1058
  lc = ctx.request_context.lifespan_context
1029
1059
  drive_service = lc.drive_service
@@ -1066,10 +1096,10 @@ def register(tool):
1066
1096
  doc_id,
1067
1097
  content,
1068
1098
  content_format,
1069
- autolink_urls,
1070
- target_folder_id,
1071
- revoke_sharing,
1072
- auto_downscale,
1099
+ autolink_urls=autolink_urls,
1100
+ target_folder_id=target_folder_id,
1101
+ revoke_sharing=revoke_sharing,
1102
+ auto_downscale=auto_downscale,
1073
1103
  )
1074
1104
  except DocEditError as e:
1075
1105
  if target_folder_id:
@@ -1222,16 +1252,13 @@ def register(tool):
1222
1252
  """
1223
1253
  lc = ctx.request_context.lifespan_context
1224
1254
  return await _replace_doc_content(
1225
- lc.docs_service,
1226
- lc.drive_service,
1227
- lc.doc_cache,
1228
- lc.folder_id,
1255
+ lc,
1229
1256
  doc_id,
1230
1257
  content,
1231
1258
  content_format,
1232
- autolink_urls,
1233
- revoke_sharing,
1234
- auto_downscale,
1259
+ autolink_urls=autolink_urls,
1260
+ revoke_sharing=revoke_sharing,
1261
+ auto_downscale=auto_downscale,
1235
1262
  )
1236
1263
 
1237
1264
  @tool(annotations=ToolAnnotations(title="Update Document from File", destructiveHint=True))
@@ -1284,38 +1311,20 @@ def register(tool):
1284
1311
  instead, with any temporary image share already revoked. The old content
1285
1312
  was cleared first, so the doc may be empty or partly written.
1286
1313
  """
1287
- path = Path(local_path)
1288
- if not path.exists():
1289
- return {"error": f"File not found: {local_path}"}
1290
-
1291
- if content_format is None:
1292
- ext = path.suffix.lower()
1293
- if ext == ".md":
1294
- content_format = "markdown"
1295
- elif ext in (".html", ".htm"):
1296
- content_format = "html"
1297
- else:
1298
- return {
1299
- "error": (
1300
- f"Unsupported file extension '{ext}'. Use .md or .html/.htm, "
1301
- "or pass content_format explicitly."
1302
- )
1303
- }
1304
-
1305
- content = path.read_text(encoding="utf-8")
1314
+ source = _read_doc_source_file(local_path, content_format, format_overridable=True)
1315
+ if isinstance(source, dict):
1316
+ return source
1317
+ content, content_format = source
1306
1318
 
1307
1319
  lc = ctx.request_context.lifespan_context
1308
1320
  return await _replace_doc_content(
1309
- lc.docs_service,
1310
- lc.drive_service,
1311
- lc.doc_cache,
1312
- lc.folder_id,
1321
+ lc,
1313
1322
  doc_id,
1314
1323
  content,
1315
1324
  content_format,
1316
- autolink_urls,
1317
- revoke_sharing,
1318
- auto_downscale,
1325
+ autolink_urls=autolink_urls,
1326
+ revoke_sharing=revoke_sharing,
1327
+ auto_downscale=auto_downscale,
1319
1328
  )
1320
1329
 
1321
1330
  @tool(annotations=ToolAnnotations(title="Get Document Structure", readOnlyHint=True))
@@ -910,12 +910,16 @@ async def _fill_children_recursive(
910
910
  table_inserts.append((t, r, c, occurrence, child))
911
911
  new_cursor = cursor + 1
912
912
  else:
913
+ # Consume every non-Table child up to the next Table. An Image is
914
+ # skipped, as _build_fill_requests does for a cell with no nested
915
+ # table: table-cell images aren't supported (#509). Stopping only at
916
+ # a Table guarantees j > cursor, so the cell can't stay pending forever.
913
917
  j = cursor
914
918
  run_group: list[Run] = []
915
- while j < len(children) and isinstance(children[j], Run):
916
- next_run = children[j]
917
- assert isinstance(next_run, Run)
918
- run_group.append(next_run)
919
+ while j < len(children) and not isinstance(children[j], Table):
920
+ next_child = children[j]
921
+ if isinstance(next_child, Run):
922
+ run_group.append(next_child)
919
923
  j += 1
920
924
  reqs = _run_group_fill_requests(run_group, para_start)
921
925
  if reqs:
@@ -917,9 +917,12 @@ class _AstParser(HTMLParser):
917
917
  self._code_depth = max(0, self._code_depth - 1)
918
918
 
919
919
  def handle_data(self, data):
920
- if (
921
- self._table_depth >= 1 and self._table_stack and self._table_stack[-1].in_cell
922
- ) or self._block_tag:
920
+ # Table-cell text and open-block text are independent triggers that happen
921
+ # to be buffered the same way today; kept as separate branches so either
922
+ # can diverge without first untangling a merged condition (#638).
923
+ if self._table_depth >= 1 and self._table_stack and self._table_stack[-1].in_cell: # noqa: SIM114
924
+ self._run_buf.append(data)
925
+ elif self._block_tag:
923
926
  self._run_buf.append(data)
924
927
  elif self._table_depth == 0 and self._tag_depth == 0 and data.strip():
925
928
  # Genuinely bare text — no wrapping block tag, no enclosing tag of
@@ -70,8 +70,12 @@ _FORMAT_MIME = {"PNG": "image/png", "JPEG": "image/jpeg", "GIF": "image/gif"}
70
70
 
71
71
  # Every reason downscale_image_bytes can decline for an image already known to be
72
72
  # oversized. "Too large to decode safely": Pillow's decompression-bomb guard refuses a
73
- # full decode above ~179 megapixels, and downscaling needs one.
74
- _CANNOT_DOWNSCALE_REASONS = "unreadable or corrupt image, animated, or too large to decode safely"
73
+ # full decode above ~179 megapixels, and downscaling needs one. "Still over the size
74
+ # limit": the bounded shrink loop ran out of attempts (#581).
75
+ _CANNOT_DOWNSCALE_REASONS = (
76
+ "unreadable or corrupt image, animated, too large to decode safely, "
77
+ "or still over the file-size limit after shrinking"
78
+ )
75
79
 
76
80
 
77
81
  def _decode(data: bytes) -> Image.Image | None:
@@ -187,9 +191,11 @@ def downscale_image_bytes(data: bytes) -> tuple[bytes, str] | None:
187
191
  """Resize image `data` to fit both the megapixel and file-size limits, preserving
188
192
  aspect ratio and format. Returns (resized_bytes, mime_type), or None if: the image
189
193
  can't be decoded; it's already within both limits (nothing to do — caller should
190
- keep using the original bytes/upload path); or it's animated (Pillow's resize only
194
+ keep using the original bytes/upload path); it's animated (Pillow's resize only
191
195
  touches the current frame, which would silently drop every other frame of an
192
- animated GIF — declining is safer than corrupting it).
196
+ animated GIF — declining is safer than corrupting it); or the bounded shrink loop
197
+ below still can't get it under the file-size limit (#581), so a caller never
198
+ embeds still-oversized bytes as if the resize had worked.
193
199
 
194
200
  The byte-size limit can't be hit exactly on the first resize the way the
195
201
  megapixel one can: encoded size depends on compression, not just pixel count, so a
@@ -236,6 +242,8 @@ def downscale_image_bytes(data: bytes) -> tuple[bytes, str] | None:
236
242
  encoded = _encode(resized)
237
243
  attempts += 1
238
244
 
245
+ if len(encoded) > MAX_INLINE_IMAGE_BYTES:
246
+ return None
239
247
  return encoded, _FORMAT_MIME.get(fmt, "application/octet-stream")
240
248
 
241
249
 
@@ -249,17 +257,26 @@ def check_drive_image_metadata(metadata: dict[str, Any]) -> dict[str, Any] | Non
249
257
  _resolve_image_source). Dimensions are checked first, matching
250
258
  downscale_image_bytes's own resize-by-pixels-first behavior. Returns the first
251
259
  violation found, or None if within both limits or the metadata doesn't report
252
- enough to check (e.g. a non-image binary with no imageMediaMetadata)."""
260
+ enough to check (e.g. a non-image binary with no imageMediaMetadata, or a
261
+ width/height/`size` that isn't a number). Never raises: both call sites call it
262
+ outside their try/except (#581)."""
253
263
  img_meta = metadata.get("imageMediaMetadata") or {}
254
- width, height = img_meta.get("width"), img_meta.get("height")
255
- if width and height:
264
+ try:
265
+ width, height = int(img_meta["width"]), int(img_meta["height"])
266
+ except (KeyError, TypeError, ValueError):
267
+ # Missing or malformed dimensions mean unknown; fall through to the size check.
268
+ width = height = 0
269
+ if width > 0 and height > 0:
256
270
  error = check_dimensions(width, height)
257
271
  if error is not None:
258
272
  return error
259
- size = metadata.get("size")
260
- if size is not None:
261
- return check_file_size(int(size))
262
- return None
273
+ try:
274
+ size_bytes = int(metadata["size"])
275
+ except (KeyError, TypeError, ValueError):
276
+ # Drive sends `size` as a decimal string. Missing or malformed means unknown,
277
+ # the same as missing dimensions: let the embed call be the judge.
278
+ return None
279
+ return check_file_size(size_bytes)
263
280
 
264
281
 
265
282
  def rewrite_too_large_error(message: str) -> str:
@@ -1,5 +1,6 @@
1
1
  """Tests for docs content tools and HTML/Markdown pipeline."""
2
2
 
3
+ import inspect
3
4
  import io
4
5
  import json
5
6
  import os
@@ -2400,6 +2401,25 @@ class TestCreateDocFromFile:
2400
2401
  result = await _docs_tools["create_doc_from_file"](local_path=str(txt_file), ctx=ctx)
2401
2402
  assert "error" in result
2402
2403
  assert ".txt" in result["error"]
2404
+ # create_doc_from_file has no content_format parameter to suggest (#567).
2405
+ assert "content_format" not in result["error"]
2406
+
2407
+ @pytest.mark.parametrize("kind", ["directory", "latin1"])
2408
+ async def test_unreadable_source_returns_error(self, tmp_path, kind):
2409
+ # PR #961 QA round 1: a path that exists but isn't a readable UTF-8 file
2410
+ # returns {"error": ...} instead of raising, and creates nothing.
2411
+ if kind == "directory":
2412
+ src = tmp_path / "dir.md"
2413
+ src.mkdir()
2414
+ else:
2415
+ src = tmp_path / "latin1.html"
2416
+ src.write_bytes("<p>caf\xe9</p>".encode("latin-1"))
2417
+ drive_svc, docs_svc = self._make_services()
2418
+ ctx = self._ctx(drive_svc, docs_svc)
2419
+ result = await _docs_tools["create_doc_from_file"](local_path=str(src), ctx=ctx)
2420
+ assert "error" in result
2421
+ assert str(src) in result["error"]
2422
+ drive_svc.files.return_value.create.assert_not_called()
2403
2423
 
2404
2424
 
2405
2425
  class TestUpdateDocFromFile:
@@ -2510,8 +2530,68 @@ class TestUpdateDocFromFile:
2510
2530
  )
2511
2531
  assert "error" in result
2512
2532
  assert ".txt" in result["error"]
2533
+ assert "pass content_format explicitly" in result["error"]
2534
+ docs_svc.documents.return_value.get.assert_not_called()
2535
+
2536
+ @pytest.mark.parametrize("kind", ["directory", "latin1"])
2537
+ async def test_unreadable_source_returns_error(self, tmp_path, kind):
2538
+ # PR #961 QA round 1: same as create_doc_from_file; the doc is never touched.
2539
+ if kind == "directory":
2540
+ src = tmp_path / "dir.md"
2541
+ src.mkdir()
2542
+ else:
2543
+ src = tmp_path / "latin1.html"
2544
+ src.write_bytes("<p>caf\xe9</p>".encode("latin-1"))
2545
+ drive_svc, docs_svc = self._make_services()
2546
+ ctx = self._ctx(drive_svc, docs_svc)
2547
+ result = await _docs_tools["update_doc_from_file"](
2548
+ doc_id="doc1", local_path=str(src), ctx=ctx
2549
+ )
2550
+ assert "error" in result
2551
+ assert str(src) in result["error"]
2513
2552
  docs_svc.documents.return_value.get.assert_not_called()
2514
2553
 
2554
+ def test_doc_content_flags_are_keyword_only(self):
2555
+ # #567 / PR #961 QA round 1: positional bools could be swapped at a call site
2556
+ # and still type-check, in both shared helpers.
2557
+ for fn, names in (
2558
+ (
2559
+ content_module._replace_doc_content,
2560
+ ("autolink_urls", "revoke_sharing", "auto_downscale"),
2561
+ ),
2562
+ (
2563
+ content_module._apply_doc_content,
2564
+ ("autolink_urls", "target_folder_id", "revoke_sharing", "auto_downscale"),
2565
+ ),
2566
+ ):
2567
+ params = inspect.signature(fn).parameters
2568
+ for name in names:
2569
+ assert params[name].kind is inspect.Parameter.KEYWORD_ONLY, (fn.__name__, name)
2570
+
2571
+ @pytest.mark.parametrize(
2572
+ "flags",
2573
+ [
2574
+ {"autolink_urls": False, "revoke_sharing": True, "auto_downscale": True},
2575
+ {"autolink_urls": True, "revoke_sharing": False, "auto_downscale": False},
2576
+ ],
2577
+ )
2578
+ async def test_flags_reach_apply_doc_content_unswapped(self, tmp_path, flags):
2579
+ # #567: _replace_doc_content's three bools are keyword-only, so each tool
2580
+ # flag must land in its own _apply_doc_content slot.
2581
+ md_file = tmp_path / "notes.md"
2582
+ md_file.write_text("text\n")
2583
+ drive_svc, docs_svc = self._make_services(end_index=2)
2584
+ ctx = self._ctx(drive_svc, docs_svc)
2585
+ real_apply = content_module._apply_doc_content
2586
+ with patch.object(content_module, "_apply_doc_content", return_value=None) as apply:
2587
+ await _docs_tools["update_doc_from_file"](
2588
+ doc_id="doc1", local_path=str(md_file), ctx=ctx, **flags
2589
+ )
2590
+ # Bind against the real signature so the assertion holds however the call
2591
+ # site passes them, and fails if the call wouldn't bind at all.
2592
+ bound = inspect.signature(real_apply).bind(*apply.call_args.args, **apply.call_args.kwargs)
2593
+ assert {k: bound.arguments[k] for k in flags} == flags
2594
+
2515
2595
  async def test_clears_trailing_mark_style_before_deleting_in_its_own_batch(self, tmp_path):
2516
2596
  # Same clear-then-insert mechanism as write_doc_content (#255) — verified here
2517
2597
  # too since it's now shared via _replace_doc_content.