mammoth-cli 2.0.29__tar.gz → 2.0.30__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 (221) hide show
  1. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/PKG-INFO +1 -1
  2. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/__init__.py +1 -1
  3. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/app.py +34 -1
  4. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/SKILL.md +3 -1
  5. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/view.md +2 -2
  6. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/input.md +2 -0
  7. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/operations.md +8 -0
  8. mammoth_cli-2.0.30/mammoth_cli/bundled_skill/mammoth-cli/references/safety.md +66 -0
  9. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/view.py +7 -3
  10. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/view_ops.py +61 -1
  11. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/runtime/confirm.py +4 -0
  12. mammoth_cli-2.0.30/mammoth_cli/runtime/dryrun.py +133 -0
  13. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/runtime/invocation.py +32 -1
  14. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/runtime/session.py +7 -0
  15. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/command_contract.py +10 -0
  16. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/input_fields.py +12 -0
  17. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/sdk_service.py +12 -0
  18. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/testing.py +11 -0
  19. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/pyproject.toml +1 -1
  20. mammoth_cli-2.0.29/mammoth_cli/bundled_skill/mammoth-cli/references/safety.md +0 -33
  21. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/.gitignore +0 -0
  22. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/LICENSE +0 -0
  23. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/README.md +0 -0
  24. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/__main__.py +0 -0
  25. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/auth.md +0 -0
  26. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/capabilities-misc.md +0 -0
  27. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/capabilities.md +0 -0
  28. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/command-index.md +0 -0
  29. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/activity.md +0 -0
  30. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/addon.md +0 -0
  31. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/agent.md +0 -0
  32. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/ai.md +0 -0
  33. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/annotation.md +0 -0
  34. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/auth.md +0 -0
  35. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/automation.md +0 -0
  36. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/batch.md +0 -0
  37. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/billing.md +0 -0
  38. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/browse.md +0 -0
  39. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/capability.md +0 -0
  40. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/client-app.md +0 -0
  41. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/completion.md +0 -0
  42. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/config.md +0 -0
  43. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/connector.md +0 -0
  44. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/context.md +0 -0
  45. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard.md +0 -0
  46. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/data-app.md +0 -0
  47. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dataset.md +0 -0
  48. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/doctor.md +0 -0
  49. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/external-key.md +0 -0
  50. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/file.md +0 -0
  51. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/folder.md +0 -0
  52. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/job.md +0 -0
  53. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/log.md +0 -0
  54. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/notification.md +0 -0
  55. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/parameter.md +0 -0
  56. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/project.md +0 -0
  57. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/report.md +0 -0
  58. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/schedule.md +0 -0
  59. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/schema.md +0 -0
  60. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/skill.md +0 -0
  61. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/snippet.md +0 -0
  62. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/support.md +0 -0
  63. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/template.md +0 -0
  64. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/trash.md +0 -0
  65. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/upgrade.md +0 -0
  66. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/user.md +0 -0
  67. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/version.md +0 -0
  68. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/webhook.md +0 -0
  69. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/workflow.md +0 -0
  70. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/workspace.md +0 -0
  71. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/handoff.md +0 -0
  72. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/jobs-drafts.md +0 -0
  73. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/machine-output.md +0 -0
  74. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/auth-scope.md +0 -0
  75. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/cleanup.md +0 -0
  76. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/dashboards.md +0 -0
  77. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/end-to-end.md +0 -0
  78. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/exports.md +0 -0
  79. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/index.md +0 -0
  80. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/need-action.md +0 -0
  81. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/resources.md +0 -0
  82. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/transforms.md +0 -0
  83. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/typed-etl-discovery.md +0 -0
  84. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/recovery.md +0 -0
  85. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/report-checklist.md +0 -0
  86. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/retention.md +0 -0
  87. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/bundled_skill/mammoth-cli/references/task-start.md +0 -0
  88. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/__init__.py +0 -0
  89. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/activity.py +0 -0
  90. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/addon.py +0 -0
  91. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/agent.py +0 -0
  92. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/ai.py +0 -0
  93. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/annotation.py +0 -0
  94. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/auth.py +0 -0
  95. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/automation.py +0 -0
  96. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/batch.py +0 -0
  97. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/billing.py +0 -0
  98. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/browse.py +0 -0
  99. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/capability.py +0 -0
  100. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/client_app.py +0 -0
  101. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/completion.py +0 -0
  102. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/config.py +0 -0
  103. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/connector.py +0 -0
  104. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/context.py +0 -0
  105. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/dashboard.py +0 -0
  106. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/data_app.py +0 -0
  107. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/dataset.py +0 -0
  108. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/doctor.py +0 -0
  109. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/external_key.py +0 -0
  110. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/file.py +0 -0
  111. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/folder.py +0 -0
  112. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/job.py +0 -0
  113. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/notification.py +0 -0
  114. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/parameter.py +0 -0
  115. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/project.py +0 -0
  116. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/registry.py +0 -0
  117. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/report.py +0 -0
  118. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/schedule.py +0 -0
  119. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/schema.py +0 -0
  120. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/skill.py +0 -0
  121. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/snippet.py +0 -0
  122. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/support.py +0 -0
  123. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/template.py +0 -0
  124. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/trash.py +0 -0
  125. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/upgrade.py +0 -0
  126. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/user.py +0 -0
  127. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/webhook.py +0 -0
  128. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/workflow.py +0 -0
  129. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/commands/workspace.py +0 -0
  130. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/context/__init__.py +0 -0
  131. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/context/credentials.py +0 -0
  132. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/context/endpoint.py +0 -0
  133. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/context/profiles.py +0 -0
  134. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/context/resolver.py +0 -0
  135. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/contracts/__init__.py +0 -0
  136. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/contracts/auth.py +0 -0
  137. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/errors/__init__.py +0 -0
  138. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/errors/envelope.py +0 -0
  139. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/manifest/__init__.py +0 -0
  140. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/manifest/loader.py +0 -0
  141. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/messages/__init__.py +0 -0
  142. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/output/__init__.py +0 -0
  143. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/output/envelope.py +0 -0
  144. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/output/normalize.py +0 -0
  145. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/output/policy.py +0 -0
  146. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/output/progress.py +0 -0
  147. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/output/render.py +0 -0
  148. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/py.typed +0 -0
  149. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/runtime/__init__.py +0 -0
  150. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/runtime/executor.py +0 -0
  151. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/runtime/input_loader.py +0 -0
  152. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/runtime/options.py +0 -0
  153. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/runtime/parents.py +0 -0
  154. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/runtime/runlog.py +0 -0
  155. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/runtime/strict.py +0 -0
  156. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/runtime/updates.py +0 -0
  157. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/runtime/validate.py +0 -0
  158. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/__init__.py +0 -0
  159. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/argspec.py +0 -0
  160. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/coerce.py +0 -0
  161. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/conditions.py +0 -0
  162. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/dispatch.py +0 -0
  163. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/factory.py +0 -0
  164. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/mapping.py +0 -0
  165. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/openapi_types.py +0 -0
  166. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/positionals.py +0 -0
  167. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/protocol.py +0 -0
  168. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/services/type_system.py +0 -0
  169. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/skills/__init__.py +0 -0
  170. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/skills/installer.py +0 -0
  171. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/skills/steering.py +0 -0
  172. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/mammoth_cli/testing.py +0 -0
  173. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/activity.yaml +0 -0
  174. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/addon.yaml +0 -0
  175. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/agent.yaml +0 -0
  176. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/ai.yaml +0 -0
  177. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/annotation.yaml +0 -0
  178. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/auth.yaml +0 -0
  179. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/automation.yaml +0 -0
  180. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/batch.yaml +0 -0
  181. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/billing.yaml +0 -0
  182. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/browse.yaml +0 -0
  183. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/capability.yaml +0 -0
  184. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/client-app.yaml +0 -0
  185. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/completion.yaml +0 -0
  186. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/config.yaml +0 -0
  187. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/connector.yaml +0 -0
  188. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/context.yaml +0 -0
  189. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/dashboard.yaml +0 -0
  190. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/data-app.yaml +0 -0
  191. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/dataset.yaml +0 -0
  192. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/doctor.yaml +0 -0
  193. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/external-key.yaml +0 -0
  194. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/file.yaml +0 -0
  195. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/folder.yaml +0 -0
  196. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/job.yaml +0 -0
  197. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/log.yaml +0 -0
  198. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/notification.yaml +0 -0
  199. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/parameter.yaml +0 -0
  200. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/project.yaml +0 -0
  201. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/report.yaml +0 -0
  202. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/schedule.yaml +0 -0
  203. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/schema.yaml +0 -0
  204. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/skill.yaml +0 -0
  205. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/snippet.yaml +0 -0
  206. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/support.yaml +0 -0
  207. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/template.yaml +0 -0
  208. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/trash.yaml +0 -0
  209. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/upgrade.yaml +0 -0
  210. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/user.yaml +0 -0
  211. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/version.yaml +0 -0
  212. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/view.yaml +0 -0
  213. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/webhook.yaml +0 -0
  214. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/workflow.yaml +0 -0
  215. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/commands/workspace.yaml +0 -0
  216. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/openapi-operations.yaml +0 -0
  217. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/schema-v1.json +0 -0
  218. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/sdk-catalog.source.yaml +0 -0
  219. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/manifests/sdk-methods.yaml +0 -0
  220. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/openapi/metadata.json +0 -0
  221. {mammoth_cli-2.0.29 → mammoth_cli-2.0.30}/spec/openapi/openapi.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mammoth-cli
3
- Version: 2.0.29
3
+ Version: 2.0.30
4
4
  Summary: Command-line interface for the Mammoth Analytics platform
5
5
  Project-URL: homepage, https://mammoth.io
6
6
  Project-URL: repository, https://github.com/EdgeMetric/mammothsdk
@@ -6,7 +6,7 @@ not implement a second HTTP client and does not call private SDK members.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- __version__ = "2.0.29"
9
+ __version__ = "2.0.30"
10
10
 
11
11
  # The versioned machine output envelope contract.
12
12
  SCHEMA_VERSION = 1
@@ -29,7 +29,7 @@ from typer.core import TyperGroup
29
29
 
30
30
  from mammoth_cli import __version__
31
31
  from mammoth_cli.commands import BESPOKE
32
- from mammoth_cli.commands.registry import HANDLERS
32
+ from mammoth_cli.commands.registry import HANDLERS, Handler
33
33
  from mammoth_cli.context import profiles
34
34
  from mammoth_cli.errors.envelope import EXIT_USAGE, CliError, not_implemented_error
35
35
  from mammoth_cli.manifest.loader import command_by_id, load_commands
@@ -629,6 +629,18 @@ def _shared_option_params() -> list[inspect.Parameter]:
629
629
  ),
630
630
  ],
631
631
  ),
632
+ opt(
633
+ "dry_run",
634
+ False,
635
+ Annotated[
636
+ bool,
637
+ typer.Option(
638
+ "--dry-run",
639
+ help="Resolve and validate, then report the request instead of sending it.",
640
+ rich_help_panel="Safety",
641
+ ),
642
+ ],
643
+ ),
632
644
  opt(
633
645
  "input_file",
634
646
  None,
@@ -777,6 +789,8 @@ def _execute(invocation: Invocation) -> None:
777
789
  record = command_by_id(invocation.command_id)
778
790
  sdk_symbol = record.get("sdk_symbol", "") if record else ""
779
791
  raise not_implemented_error(invocation.command_id, sdk_symbol)
792
+ if invocation.dry_run:
793
+ return _dry_run(handler, invocation)
780
794
  return handler(invocation)
781
795
 
782
796
  executor.run(
@@ -789,6 +803,25 @@ def _execute(invocation: Invocation) -> None:
789
803
  )
790
804
 
791
805
 
806
+ def _dry_run(handler: Handler, invocation: Invocation) -> tuple[Any, dict[str, Any]]:
807
+ """Run ``handler`` until its request would leave; report that request.
808
+
809
+ Local commands never open a service, so a dry run of one is the real run.
810
+ An API-backed handler that returns normally under ``--dry-run`` made no
811
+ gated SDK call at all (a pure read path); its result is returned as is.
812
+ """
813
+ from mammoth_cli.runtime.dryrun import DryRunStop
814
+ from mammoth_cli.runtime.session import resolved_project
815
+
816
+ try:
817
+ return handler(invocation)
818
+ except DryRunStop as stop:
819
+ return stop.record, {
820
+ "profile": invocation.profile,
821
+ "project_id": resolved_project(invocation),
822
+ }
823
+
824
+
792
825
  def _command_help(command_id: str, record: dict[str, Any] | None) -> str | None:
793
826
  """Build a command's user-facing ``--help`` summary.
794
827
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mammoth-cli
3
- version: 2.0.29
3
+ version: 2.0.30
4
4
  description: "Use Mammoth Analytics from a terminal: install or authenticate the CLI, discover its live command contract, and safely manage projects, data, views, pipelines, dashboards, exports, and handoffs."
5
5
  ---
6
6
 
@@ -60,6 +60,8 @@ before the next step.
60
60
  '{"full": true}'` returns the dependency and display trees too.
61
61
  - Column inputs and expressions use the exact **display names** the view
62
62
  returns, never backend aliases.
63
+ - `--dry-run` on any API-backed command resolves everything and reports the
64
+ request instead of sending it; use it before a write you are not sure of.
63
65
  - Destructive commands need `--yes --confirm ID`. Preserve requested
64
66
  deliverables; cleanup is exact-ID authorized and
65
67
  never means delete-all-owned-resources: delete only ids this run created,
@@ -932,7 +932,7 @@ Example: `mammoth view transform extract-date 123 --input '{"column": "Status",
932
932
 
933
933
  Result: `ViewTransformExtractDateResult`; mutation `reversible_pipeline`, confirmation `none`, wait policy `always_wait`.
934
934
 
935
- Status on release: untried; it submits through `view.task.add`, but this transform was not among those run.
935
+ Status on release: ran once on CLI 2.0.18 through `view.task.add` and was read back with `view data get`; other inputs for this transform are untried.
936
936
 
937
937
  ### `view.transform.fill-missing`
938
938
 
@@ -1112,7 +1112,7 @@ Example: `mammoth view transform window 123 --input '{"function": "ROW_NUMBER",
1112
1112
 
1113
1113
  Result: `ViewTransformWindowResult`; mutation `reversible_pipeline`, confirmation `none`, wait policy `always_wait`.
1114
1114
 
1115
- Status on release: untried; it submits through `view.task.add`, but this transform was not among those run.
1115
+ Status on release: ran once on CLI 2.0.18 through `view.task.add` and was read back with `view data get`; other inputs for this transform are untried.
1116
1116
 
1117
1117
  ### `view.trash`
1118
1118
 
@@ -13,6 +13,8 @@ mammoth view transform filter 1039 --project 180 \
13
13
  - `--input FILE` (or `--input @FILE`) reads a JSON or YAML file; the format
14
14
  is inferred from the extension.
15
15
  - `--input -` reads stdin; then `--input-format json|yaml` is required.
16
+ - `--dry-run` admits and resolves the document, then reports the SDK call
17
+ instead of making it ([safety](safety.md)).
16
18
  - The top level must be a mapping. A bad path, format, or shape fails with exit
17
19
  code 2 and a stable error code.
18
20
  - Column references use display names returned by the exact view schema. Do not
@@ -90,6 +90,14 @@ task/list or pipeline/items result back after a successful write. Without that
90
90
  independent specification, report the route as unsupported/ambiguous and stop
91
91
  safely.
92
92
 
93
+ Typed transforms and `view task add` append at the end of the pipeline and
94
+ never reorder or delete an existing step. On a pipeline other people or
95
+ agents also edit, make that checkable: read `view task list VIEW_ID`, plan,
96
+ then write with `"expected_task_count": N` in the input; the CLI re-reads
97
+ the list just before the write and refuses with `pipeline_changed` when the
98
+ count moved ([safety](safety.md)). `--dry-run` on the same command shows the
99
+ resolved call first.
100
+
93
101
  For a new workflow (a container for automation), discover and then read it
94
102
  back; workflow creation is not the same as adding a view pipeline task:
95
103
 
@@ -0,0 +1,66 @@
1
+ # Safe mutation
2
+
3
+ Each command carries a reviewed confirmation policy:
4
+
5
+ | policy | how to satisfy it noninteractively |
6
+ |---|---|
7
+ | none | nothing required |
8
+ | prompt_or_yes | pass `--yes` |
9
+ | yes_always | pass `--yes` (always, even at a terminal) |
10
+ | confirm_target | pass `--yes` and `--confirm TARGET` (exact match) |
11
+
12
+ Under `--no-input` (and in `json`/`ndjson` modes) there is no prompt: a missing
13
+ `--yes`/`--confirm` fails with exit code 2 and error code
14
+ `confirmation_required` or `confirmation_target_mismatch`.
15
+
16
+ ```bash
17
+ # normal delete
18
+ mammoth folder delete 7 --project 180 --yes
19
+
20
+ # high-impact: target must equal the resource
21
+ mammoth project user remove --project 180 \
22
+ --yes --confirm 180 --input '{"user_ids": ["u_123"]}'
23
+
24
+ # project delete cascades to every dataset, view, export and dashboard in it
25
+ mammoth project delete 180 --yes --confirm 180
26
+ ```
27
+
28
+ Discover a command's policy with `mammoth schema get <command.id>`.
29
+
30
+ ## Rehearse before you write
31
+
32
+ `--dry-run` on any API-backed command does everything but send the request:
33
+ input admission, parent resolution, display-name and condition checks, then
34
+ it stops at the SDK call and reports it as `data.would_call` (`sdk_symbol`,
35
+ resolved `arguments`, `view_id`/`dataset_id`) with `data.dry_run: true`. No
36
+ confirmation flag is needed for a dry run; the real run still needs its
37
+ `--yes`/`--confirm`. Use it before a mutation on a shared or production
38
+ resource, and whenever you are unsure which view or column an input resolves
39
+ to.
40
+
41
+ ```bash
42
+ mammoth view transform filter 144 --dry-run \
43
+ --input '{"filter_type": "REMOVE", "condition": {"column": "Location", "operator": "EQ", "value": "Total"}}'
44
+ # data.would_call.sdk_symbol = ...FilterOpsMixin.filter_rows, view_id 144, dataset_id 122
45
+ ```
46
+
47
+ ## Add-only edits to a pipeline someone else may touch
48
+
49
+ Every `view transform *` and `view task add` accepts `expected_task_count`:
50
+ the number of tasks your last `view task list VIEW_ID` returned. The CLI
51
+ re-reads the task list right before the write and refuses with
52
+ `pipeline_changed` (exit 2, `details.actual_task_count`, `task_ids`) when the
53
+ count differs, so a plan made against a stale read cannot land on a pipeline
54
+ that changed under you. Read → decide → write with the count, one edit at a
55
+ time; on `pipeline_changed`, re-read and re-decide rather than retrying with
56
+ the new number blindly.
57
+
58
+ ```bash
59
+ mammoth view task list 144 # data.tasks has 3 entries
60
+ mammoth view transform filter 144 --input '{"expected_task_count": 3, "filter_type": "REMOVE", "condition": {...}}'
61
+ ```
62
+ Use exact observed resource scope and display names before mutation. A known job
63
+ handle should be inspected or waited, not submitted again. `outcome_unknown`
64
+ means a mutation may have committed without a terminal response: reconcile the
65
+ target before any replay. A retryable read can honor `Retry-After`; exit 7 is
66
+ not blanket permission to retry a mutation.
@@ -1445,11 +1445,15 @@ def view_task_add(invocation: Invocation) -> HandlerResult:
1445
1445
  kwargs: dict[str, Any] = {"dataview_id": dataview_id, "task_spec": task_spec}
1446
1446
  assert document is not None
1447
1447
  _forward_optional(document, kwargs, ("dataset_id",))
1448
+ # Imported here: view_ops imports this module for the brief record helpers.
1449
+ from mammoth_cli.commands.view_ops import (
1450
+ reject_pipeline_reference_errors,
1451
+ require_expected_task_count,
1452
+ )
1453
+
1448
1454
  with open_service(invocation) as (service, auth):
1455
+ require_expected_task_count(service, dataview_id, kwargs.get("dataset_id"), document)
1449
1456
  data = service.call(_symbol(invocation), **kwargs)
1450
- # Imported here: view_ops imports this module for the brief record helpers.
1451
- from mammoth_cli.commands.view_ops import reject_pipeline_reference_errors
1452
-
1453
1457
  reject_pipeline_reference_errors(service, dataview_id, kwargs.get("dataset_id"), data)
1454
1458
  return data, _meta(invocation, auth.workspace_id, None)
1455
1459
 
@@ -43,6 +43,7 @@ from mammoth_cli.runtime.invocation import Invocation
43
43
  from mammoth_cli.runtime.session import open_service, resolved_project
44
44
  from mammoth_cli.services.command_contract import bind_command_inputs
45
45
  from mammoth_cli.services.conditions import CONDITION_KWARG
46
+ from mammoth_cli.services.input_fields import TASK_COUNT_FIELD
46
47
 
47
48
  HandlerResult = tuple[Any, dict[str, Any]]
48
49
 
@@ -251,6 +252,7 @@ def _dispatch_view(
251
252
  with open_service(invocation) as (service, auth):
252
253
  if dataset_id is None:
253
254
  dataset_id = parents.lookup(_profile_name(invocation), auth.workspace_id, view_id)
255
+ require_expected_task_count(service, view_id, dataset_id, document)
254
256
  try:
255
257
  if dataset_id is None:
256
258
  _require_discovery_allowed(invocation, view_id)
@@ -266,6 +268,60 @@ def _dispatch_view(
266
268
  return data, _meta(invocation, auth.workspace_id)
267
269
 
268
270
 
271
+ CODE_PIPELINE_CHANGED = "pipeline_changed"
272
+ _TASK_LIST_SYMBOL = "mammoth.api.pipeline.PipelineAPI.list_tasks"
273
+
274
+
275
+ def require_expected_task_count(
276
+ service: Any, view_id: int, dataset_id: Any, document: dict[str, Any] | None
277
+ ) -> None:
278
+ """Refuse a pipeline write when the view's task count is not the one read.
279
+
280
+ ``expected_task_count`` is the count from the caller's last ``view task
281
+ list``. A different live count means the pipeline changed since that read
282
+ (another agent, a person in the web app), and an append planned against
283
+ the old shape must not land. Without the field nothing is checked.
284
+ """
285
+ if not document or TASK_COUNT_FIELD not in document:
286
+ return
287
+ expected = int(document[TASK_COUNT_FIELD])
288
+ kwargs: dict[str, Any] = {"dataview_id": view_id}
289
+ if dataset_id is not None:
290
+ kwargs["dataset_id"] = int(dataset_id)
291
+ listing = service.call(_TASK_LIST_SYMBOL, **kwargs)
292
+ tasks = listing.get("tasks") if isinstance(listing, dict) else None
293
+ if not isinstance(tasks, list):
294
+ raise CliError(
295
+ code=CODE_PIPELINE_CHANGED,
296
+ message=f"Could not read the task list of view {view_id} to check the precondition.",
297
+ exit_status=EXIT_USAGE,
298
+ details={"view_id": view_id, "expected_task_count": expected, "response": listing},
299
+ )
300
+ actual = len(tasks)
301
+ if actual == expected:
302
+ return
303
+ raise CliError(
304
+ code=CODE_PIPELINE_CHANGED,
305
+ message=(
306
+ f"View {view_id} has {actual} pipeline task(s), not the {expected} expected; "
307
+ "the pipeline changed since it was read, so nothing was written."
308
+ ),
309
+ exit_status=EXIT_USAGE,
310
+ hint=(
311
+ "Re-read the pipeline, decide whether the plan still holds, and retry "
312
+ f"with expected_task_count {actual}."
313
+ ),
314
+ details={
315
+ "view_id": view_id,
316
+ "dataset_id": int(dataset_id) if dataset_id is not None else None,
317
+ "expected_task_count": expected,
318
+ "actual_task_count": actual,
319
+ "task_ids": [t.get("id") for t in tasks if isinstance(t, dict)],
320
+ },
321
+ recovery_commands=[f"mammoth view task list {view_id}"],
322
+ )
323
+
324
+
269
325
  def _compact_reference_error(entry: Any) -> dict[str, Any]:
270
326
  if not isinstance(entry, dict):
271
327
  return {"reason": str(entry)}
@@ -362,7 +418,11 @@ def _without_resource_context(document: dict[str, Any]) -> dict[str, Any]:
362
418
  # mapping. These identifiers select the View receiver and are not method
363
419
  # kwargs; strip both parent and receiver identities before the transform
364
420
  # adapter forwards the document.
365
- return {key: value for key, value in document.items() if key not in {"dataset_id", "view_id"}}
421
+ return {
422
+ key: value
423
+ for key, value in document.items()
424
+ if key not in {"dataset_id", "view_id", TASK_COUNT_FIELD}
425
+ }
366
426
 
367
427
 
368
428
  def _bind_transform_inputs(invocation: Invocation, document: dict[str, Any]) -> dict[str, Any]:
@@ -84,6 +84,10 @@ def enforce_confirmation(
84
84
  """
85
85
  if policy == POLICY_NONE:
86
86
  return
87
+ if invocation.dry_run:
88
+ # Nothing is sent under --dry-run, so there is nothing to confirm; the
89
+ # report names the confirmation the real run will need.
90
+ return
87
91
 
88
92
  tty = sys.stdin.isatty() if is_tty is None else is_tty
89
93
  machine = invocation.output in MACHINE_OUTPUTS
@@ -0,0 +1,133 @@
1
+ """``--dry-run``: resolve a command completely, then stop before its request.
2
+
3
+ A dry run does everything a real run does up to the network call that the
4
+ command exists for — input admission, parent resolution, display-name checks,
5
+ condition compilation, argument coercion — and then raises :class:`DryRunStop`
6
+ instead of invoking the SDK method. The executor turns that into a success
7
+ envelope describing the call that would have been made.
8
+
9
+ The gate is a small callable installed on the production service by
10
+ :func:`mammoth_cli.runtime.session.open_service`. It classifies each SDK call
11
+ the handler makes:
12
+
13
+ - the command's own manifest ``sdk_symbol`` (or, for View-backed commands,
14
+ the View method of that symbol) → stop and report;
15
+ - a symbol some ``read`` command is backed by → let it through, so the
16
+ resolution the report depends on really happened;
17
+ - anything else → stop and report it as an undeclared write, so a dry run
18
+ can never mutate through a side path.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from collections.abc import Callable, Mapping
24
+ from enum import Enum
25
+ from functools import lru_cache
26
+ from typing import Any
27
+
28
+ from mammoth_cli.manifest.loader import command_by_id, load_commands
29
+
30
+ Gate = Callable[..., None]
31
+
32
+ NOTE_OWN = "No request was sent for this operation; the reads needed to resolve it did run."
33
+ NOTE_UNDECLARED = (
34
+ "Stopped before an SDK call the command's manifest does not declare; "
35
+ "nothing was sent for it."
36
+ )
37
+
38
+
39
+ class DryRunStop(Exception):
40
+ """Raised by the gate at the point a real run would send the request."""
41
+
42
+ def __init__(self, record: dict[str, Any]) -> None:
43
+ super().__init__("dry run")
44
+ self.record = record
45
+
46
+
47
+ @lru_cache(maxsize=1)
48
+ def _read_symbols() -> frozenset[str]:
49
+ return frozenset(
50
+ str(record["sdk_symbol"])
51
+ for record in load_commands()
52
+ if record.get("mutation_class") == "read" and record.get("sdk_symbol")
53
+ )
54
+
55
+
56
+ def _read_view_methods() -> frozenset[str]:
57
+ return frozenset(
58
+ symbol.rsplit(".", 1)[1]
59
+ for symbol in _read_symbols()
60
+ if symbol.startswith("mammoth.view.") or symbol.startswith("mammoth._mixins.")
61
+ )
62
+
63
+
64
+ def jsonable(value: Any) -> Any:
65
+ """Render resolved SDK arguments for the report without leaking objects."""
66
+ if isinstance(value, Enum):
67
+ return value.value
68
+ if isinstance(value, Mapping):
69
+ return {str(key): jsonable(item) for key, item in value.items()}
70
+ if isinstance(value, (list, tuple, set, frozenset)):
71
+ return [jsonable(item) for item in value]
72
+ if value is None or isinstance(value, (str, int, float, bool)):
73
+ return value
74
+ to_dict = getattr(value, "to_dict", None)
75
+ if callable(to_dict):
76
+ try:
77
+ return jsonable(to_dict())
78
+ except Exception: # pragma: no cover - defensive; fall through to repr
79
+ return repr(value)
80
+ view_id = getattr(value, "id", None)
81
+ if type(value).__name__ == "View" and isinstance(view_id, int):
82
+ return {"view_id": view_id, "dataset_id": getattr(value, "dataset_id", None)}
83
+ return repr(value)
84
+
85
+
86
+ def make_gate(command_id: str) -> Gate:
87
+ """Return the gate for ``command_id`` (see the module docstring)."""
88
+ record = command_by_id(command_id) or {}
89
+ own_symbol = str(record.get("sdk_symbol") or "")
90
+ own_method = own_symbol.rsplit(".", 1)[-1] if own_symbol else ""
91
+ mutation_class = record.get("mutation_class")
92
+ # A CLI-composed command (project ensure, ...) is backed by a CLI function
93
+ # and reaches the SDK through whichever writes it needs; those are its own.
94
+ composed = own_symbol.startswith("mammoth_cli.")
95
+ reads = _read_symbols()
96
+ view_reads = _read_view_methods()
97
+
98
+ def stop(symbol: str, arguments: dict[str, Any], *, own: bool, **scope: Any) -> None:
99
+ raise DryRunStop(
100
+ {
101
+ "dry_run": True,
102
+ "command": command_id.replace(".", " "),
103
+ "mutation_class": mutation_class,
104
+ "would_call": {
105
+ "sdk_symbol": symbol,
106
+ **{key: value for key, value in scope.items() if value is not None},
107
+ "arguments": jsonable(arguments),
108
+ },
109
+ "note": NOTE_OWN if own else NOTE_UNDECLARED,
110
+ }
111
+ )
112
+
113
+ def gate(
114
+ symbol: str,
115
+ arguments: dict[str, Any],
116
+ *,
117
+ view_id: int | None = None,
118
+ dataset_id: int | None = None,
119
+ ) -> None:
120
+ if view_id is not None:
121
+ # A View-backed call: ``symbol`` is the View method name.
122
+ if symbol == own_method:
123
+ stop(own_symbol, arguments, own=True, view_id=view_id, dataset_id=dataset_id)
124
+ if symbol in view_reads:
125
+ return
126
+ stop(f"View.{symbol}", arguments, own=False, view_id=view_id, dataset_id=dataset_id)
127
+ if symbol == own_symbol:
128
+ stop(symbol, arguments, own=True)
129
+ if symbol in reads:
130
+ return
131
+ stop(symbol, arguments, own=composed)
132
+
133
+ return gate
@@ -16,7 +16,11 @@ from mammoth_cli.errors.envelope import EXIT_USAGE, CliError
16
16
  from mammoth_cli.output.policy import OUTPUT_AUTO, SELECTABLE_OUTPUTS, resolve_output
17
17
  from mammoth_cli.runtime.input_loader import load_input_document
18
18
  from mammoth_cli.runtime.strict import validate_input_fields
19
- from mammoth_cli.services.input_fields import accepts_resource_dataset
19
+ from mammoth_cli.services.input_fields import (
20
+ TASK_COUNT_FIELD,
21
+ accepts_resource_dataset,
22
+ accepts_task_count_precondition,
23
+ )
20
24
 
21
25
 
22
26
  class _UninitializedInput:
@@ -55,6 +59,7 @@ class Invocation:
55
59
  debug: bool = False
56
60
  yes: bool = False
57
61
  confirm: str | None = None
62
+ dry_run: bool = False
58
63
  input_file: str | None = None
59
64
  input_format: str | None = None
60
65
  positionals: dict[str, Any] = field(default_factory=dict)
@@ -231,10 +236,36 @@ class Invocation:
231
236
  hint="Pass the exact parent dataset id for the target view.",
232
237
  details={"field": "dataset_id", "expected_type": "positive integer"},
233
238
  ) from None
239
+ expected_tasks: Any = None
240
+ has_expected_tasks = (
241
+ document is not None
242
+ and accepts_task_count_precondition(self.command_id)
243
+ and TASK_COUNT_FIELD in document
244
+ )
245
+ if has_expected_tasks:
246
+ assert document is not None
247
+ expected_tasks = document.pop(TASK_COUNT_FIELD)
248
+ try:
249
+ if isinstance(expected_tasks, bool):
250
+ raise ValueError
251
+ expected_tasks = int(expected_tasks)
252
+ if expected_tasks < 0:
253
+ raise ValueError
254
+ except (TypeError, ValueError):
255
+ raise CliError(
256
+ code="invalid_resource_context",
257
+ message=f"Input field '{TASK_COUNT_FIELD}' must be a non-negative integer.",
258
+ exit_status=EXIT_USAGE,
259
+ hint="Pass the task count from your last 'view task list' read.",
260
+ details={"field": TASK_COUNT_FIELD, "expected_type": "integer >= 0"},
261
+ ) from None
234
262
  validate_input_fields(self.command_id, document)
235
263
  if has_resource_dataset:
236
264
  assert document is not None
237
265
  document["dataset_id"] = resource_dataset
266
+ if has_expected_tasks:
267
+ assert document is not None
268
+ document[TASK_COUNT_FIELD] = expected_tasks
238
269
  object.__setattr__(self, "_prepared_input", document)
239
270
  return self._prepared_input # type: ignore[return-value]
240
271
 
@@ -79,6 +79,13 @@ def open_service(invocation: Invocation) -> Iterator[tuple[MammothService, Resol
79
79
  profile=invocation.profile or profiles.get_selected(),
80
80
  progress=not policy.progress_disabled,
81
81
  )
82
+ if invocation.dry_run:
83
+ # The production service exposes the gate; a test double may not, and
84
+ # then the dry run is simply the double's own behaviour.
85
+ if hasattr(service, "gate"):
86
+ from mammoth_cli.runtime.dryrun import make_gate
87
+
88
+ service.gate = make_gate(invocation.command_id)
82
89
  try:
83
90
  yield service, auth
84
91
  finally:
@@ -26,7 +26,9 @@ from mammoth.models.batches import (
26
26
  from mammoth_cli.manifest.loader import command_by_id, load_commands
27
27
  from mammoth_cli.services.argspec import ArgSpec, FieldSpec, arg_spec
28
28
  from mammoth_cli.services.input_fields import (
29
+ TASK_COUNT_FIELD,
29
30
  accepts_resource_dataset,
31
+ accepts_task_count_precondition,
30
32
  excluded_input_fields,
31
33
  handler_owned_fields,
32
34
  is_closed_zero_input,
@@ -672,6 +674,12 @@ def resolve_command_contract(command_id: str) -> ResolvedCommandContract | None:
672
674
  field.name for field in fields
673
675
  }:
674
676
  fields += (FieldSpec("dataset_id", required=False, annotation=int | None, default=None),)
677
+ if accepts_task_count_precondition(command_id) and TASK_COUNT_FIELD not in {
678
+ field.name for field in fields
679
+ }:
680
+ fields += (
681
+ FieldSpec(TASK_COUNT_FIELD, required=False, annotation=int | None, default=None),
682
+ )
675
683
  additional_fields = _S7_ADDITIONAL_INPUT_FIELDS.get(command_id, ())
676
684
  declared_names = {field.name for field in fields}
677
685
  fields += tuple(field for field in additional_fields if field.name not in declared_names)
@@ -686,6 +694,8 @@ def resolve_command_contract(command_id: str) -> ResolvedCommandContract | None:
686
694
  # the subsequent shared bind idempotent after that protected preflight.
687
695
  if accepts_resource_dataset(command_id):
688
696
  context_values["dataset_id"] = "dataset_id"
697
+ if accepts_task_count_precondition(command_id):
698
+ context_values[TASK_COUNT_FIELD] = TASK_COUNT_FIELD
689
699
  context = MappingProxyType(context_values)
690
700
  extensibility = (
691
701
  "closed"
@@ -351,6 +351,18 @@ def accepts_resource_dataset(command_id: str) -> bool:
351
351
  return command_id.startswith(_RESOURCE_DATASET_PREFIXES)
352
352
 
353
353
 
354
+ #: Pipeline writes that admit ``expected_task_count``: the number of tasks the
355
+ #: caller last read on the view. The write is refused when the pipeline has a
356
+ #: different count, so an edit planned against a stale read cannot land on a
357
+ #: pipeline someone else changed (the "add-only to a shared pipeline" rule).
358
+ TASK_COUNT_FIELD = "expected_task_count"
359
+
360
+
361
+ def accepts_task_count_precondition(command_id: str) -> bool:
362
+ """Return True when ``expected_task_count`` is admitted for ``command_id``."""
363
+ return command_id.startswith("view.transform.") or command_id == "view.task.add"
364
+
365
+
354
366
  def example_input_hints(command_id: str) -> dict[str, Any]:
355
367
  """Return extra ``--input`` fields to include in the generated example."""
356
368
  hints = dict(_EXAMPLE_INPUT_HINTS.get(command_id, {}))
@@ -9,6 +9,7 @@ a private (``_``-prefixed) SDK member.
9
9
  from __future__ import annotations
10
10
 
11
11
  import re
12
+ from collections.abc import Callable
12
13
  from types import TracebackType
13
14
  from typing import Any
14
15
 
@@ -72,6 +73,9 @@ class SdkMammothService:
72
73
  """
73
74
  self._progress = progress
74
75
  self._profile = profile
76
+ #: ``--dry-run`` hook (:mod:`mammoth_cli.runtime.dryrun`): called with
77
+ #: the resolved symbol and arguments right before each SDK call.
78
+ self.gate: Callable[..., None] | None = None
75
79
  self._project_id = project_id
76
80
  self._workspace_id = auth.workspace_id
77
81
  kwargs: dict[str, Any] = {}
@@ -124,6 +128,8 @@ class SdkMammothService:
124
128
  ):
125
129
  raise missing_project_error()
126
130
  kwargs = self._coerce_call_arguments(method, kwargs)
131
+ if self.gate is not None:
132
+ self.gate(sdk_symbol, kwargs)
127
133
  try:
128
134
  with spinner(self._progress):
129
135
  return method(**kwargs)
@@ -356,6 +362,8 @@ class SdkMammothService:
356
362
  ) from exc
357
363
  if kwargs.get(CONDITION_KWARG) is not None:
358
364
  kwargs[CONDITION_KWARG] = compile_condition(kwargs[CONDITION_KWARG])
365
+ if self.gate is not None:
366
+ self.gate(method, kwargs, view_id=view_id, dataset_id=getattr(view, "dataset_id", None))
359
367
  try:
360
368
  with spinner(self._progress):
361
369
  return attribute(**kwargs)
@@ -717,6 +725,8 @@ class SdkMammothService:
717
725
  Raises:
718
726
  CliError: Mapped from any SDK exception.
719
727
  """
728
+ if self.gate is not None:
729
+ self.gate("mammoth.api.projects.ProjectsAPI.create", {"name": name, **kwargs})
720
730
  try:
721
731
  return self._client.projects.create(name, **kwargs)
722
732
  except Exception as exc:
@@ -734,6 +744,8 @@ class SdkMammothService:
734
744
  Raises:
735
745
  CliError: Mapped from any SDK exception.
736
746
  """
747
+ if self.gate is not None:
748
+ self.gate("mammoth.api.projects.ProjectsAPI.delete", {"project_id": project_id})
737
749
  try:
738
750
  return self._client.projects.delete(project_id)
739
751
  except Exception as exc: