tripl 0.2.0__tar.gz → 0.2.2__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 (199) hide show
  1. {tripl-0.2.0 → tripl-0.2.2}/PKG-INFO +130 -8
  2. {tripl-0.2.0 → tripl-0.2.2}/README.md +128 -6
  3. {tripl-0.2.0 → tripl-0.2.2}/pyproject.toml +6 -5
  4. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/__init__.py +8 -0
  5. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/branches.py +10 -8
  6. tripl-0.2.2/src/tripl_cli/api/chart_annotations.py +73 -0
  7. tripl-0.2.2/src/tripl_cli/api/docs.py +191 -0
  8. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/endpoints.py +8 -0
  9. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/events.py +52 -0
  10. tripl-0.2.2/src/tripl_cli/api/plan_export.py +39 -0
  11. tripl-0.2.2/src/tripl_cli/api/plan_validation.py +78 -0
  12. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/search.py +43 -1
  13. tripl-0.2.2/src/tripl_cli/check/__init__.py +18 -0
  14. tripl-0.2.2/src/tripl_cli/check/calls.py +359 -0
  15. tripl-0.2.2/src/tripl_cli/check/config.py +868 -0
  16. tripl-0.2.2/src/tripl_cli/check/lexer.py +617 -0
  17. tripl-0.2.2/src/tripl_cli/check/model.py +121 -0
  18. tripl-0.2.2/src/tripl_cli/check/payloads.py +231 -0
  19. tripl-0.2.2/src/tripl_cli/check/presets.py +178 -0
  20. tripl-0.2.2/src/tripl_cli/check/render.py +253 -0
  21. tripl-0.2.2/src/tripl_cli/check/scan.py +367 -0
  22. tripl-0.2.2/src/tripl_cli/check/symbols.py +611 -0
  23. tripl-0.2.2/src/tripl_cli/check/validate.py +191 -0
  24. tripl-0.2.2/src/tripl_cli/check/values.py +393 -0
  25. tripl-0.2.2/src/tripl_cli/check/yamlish.py +404 -0
  26. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/cli.py +18 -3
  27. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/client.py +7 -0
  28. tripl-0.2.2/src/tripl_cli/codegen/__init__.py +10 -0
  29. tripl-0.2.2/src/tripl_cli/codegen/calls.py +183 -0
  30. tripl-0.2.2/src/tripl_cli/codegen/context.py +530 -0
  31. tripl-0.2.2/src/tripl_cli/codegen/context_named.py +428 -0
  32. tripl-0.2.2/src/tripl_cli/codegen/context_plan.py +238 -0
  33. tripl-0.2.2/src/tripl_cli/codegen/files.py +103 -0
  34. tripl-0.2.2/src/tripl_cli/codegen/generate.py +351 -0
  35. tripl-0.2.2/src/tripl_cli/codegen/languages.py +185 -0
  36. tripl-0.2.2/src/tripl_cli/codegen/model.py +278 -0
  37. tripl-0.2.2/src/tripl_cli/codegen/naming.py +455 -0
  38. tripl-0.2.2/src/tripl_cli/codegen/template.py +205 -0
  39. tripl-0.2.2/src/tripl_cli/codegen/templates/named.kotlin.mustache +62 -0
  40. tripl-0.2.2/src/tripl_cli/codegen/templates/named.swift.mustache +138 -0
  41. tripl-0.2.2/src/tripl_cli/codegen/templates/named.ts.mustache +72 -0
  42. tripl-0.2.2/src/tripl_cli/codegen/templates/screen_view.kotlin.mustache +45 -0
  43. tripl-0.2.2/src/tripl_cli/codegen/templates/screen_view.swift.mustache +33 -0
  44. tripl-0.2.2/src/tripl_cli/codegen/templates/screen_view.ts.mustache +40 -0
  45. tripl-0.2.2/src/tripl_cli/codegen/templates/self_describing.kotlin.mustache +66 -0
  46. tripl-0.2.2/src/tripl_cli/codegen/templates/self_describing.swift.mustache +59 -0
  47. tripl-0.2.2/src/tripl_cli/codegen/templates/self_describing.ts.mustache +84 -0
  48. tripl-0.2.2/src/tripl_cli/codegen/templates/structured.kotlin.mustache +45 -0
  49. tripl-0.2.2/src/tripl_cli/codegen/templates/structured.swift.mustache +33 -0
  50. tripl-0.2.2/src/tripl_cli/codegen/templates/structured.ts.mustache +40 -0
  51. tripl-0.2.2/src/tripl_cli/codegen/templates/transport.kotlin.mustache +30 -0
  52. tripl-0.2.2/src/tripl_cli/codegen/templates/transport.swift.mustache +33 -0
  53. tripl-0.2.2/src/tripl_cli/codegen/templates/transport.ts.mustache +57 -0
  54. tripl-0.2.2/src/tripl_cli/codegen/value_types.py +145 -0
  55. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/__init__.py +22 -0
  56. tripl-0.2.2/src/tripl_cli/commands/_docs_files.py +215 -0
  57. tripl-0.2.2/src/tripl_cli/commands/annotate.py +225 -0
  58. tripl-0.2.2/src/tripl_cli/commands/check.py +221 -0
  59. tripl-0.2.2/src/tripl_cli/commands/codegen.py +315 -0
  60. tripl-0.2.2/src/tripl_cli/commands/docs.py +537 -0
  61. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/events.py +97 -2
  62. tripl-0.2.2/src/tripl_cli/commands/export.py +183 -0
  63. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/install.py +2 -2
  64. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/plan.py +28 -14
  65. tripl-0.2.2/src/tripl_cli/commands/whoami.py +93 -0
  66. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/diagnostics/collect.py +9 -0
  67. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/diagnostics/endpoints.py +43 -4
  68. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/diagnostics/scan_checks.py +18 -21
  69. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/install/files/compose.yaml +67 -1
  70. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/install/files.py +3 -2
  71. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/model.py +92 -14
  72. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/render.py +70 -0
  73. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/report.py +142 -1
  74. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/watch/collect.py +1 -1
  75. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/watch/model.py +1 -1
  76. tripl-0.2.2/tests/codegen/check.yml +30 -0
  77. tripl-0.2.2/tests/codegen/custom/check.yml +57 -0
  78. tripl-0.2.2/tests/codegen/custom/golden/kotlin/AcmeAction.kt +14 -0
  79. tripl-0.2.2/tests/codegen/custom/golden/kotlin/AcmeAppConstants.kt +21 -0
  80. tripl-0.2.2/tests/codegen/custom/golden/kotlin/AcmeCategory.kt +12 -0
  81. tripl-0.2.2/tests/codegen/custom/golden/kotlin/AcmeLabel.kt +16 -0
  82. tripl-0.2.2/tests/codegen/custom/golden/kotlin/AcmeProperty.kt +10 -0
  83. tripl-0.2.2/tests/codegen/custom/golden/kotlin/AcmeScreens.kt +21 -0
  84. tripl-0.2.2/tests/codegen/custom/golden/kotlin/AcmeStructured.kt +25 -0
  85. tripl-0.2.2/tests/codegen/custom/golden/kotlin/TriplTransport.kt +30 -0
  86. tripl-0.2.2/tests/codegen/custom/golden/swift/AcmeAppConstants.swift +19 -0
  87. tripl-0.2.2/tests/codegen/custom/golden/swift/AcmePlanned.swift +18 -0
  88. tripl-0.2.2/tests/codegen/custom/golden/swift/AcmeScreens.swift +41 -0
  89. tripl-0.2.2/tests/codegen/custom/golden/swift/AcmeStructured.swift +54 -0
  90. tripl-0.2.2/tests/codegen/custom/golden/swift/TriplTransport.swift +35 -0
  91. tripl-0.2.2/tests/codegen/custom/golden/ts/acmeAction.ts +13 -0
  92. tripl-0.2.2/tests/codegen/custom/golden/ts/acmeAppConstants.ts +19 -0
  93. tripl-0.2.2/tests/codegen/custom/golden/ts/acmeCategory.ts +11 -0
  94. tripl-0.2.2/tests/codegen/custom/golden/ts/acmeLabel.ts +15 -0
  95. tripl-0.2.2/tests/codegen/custom/golden/ts/acmeProperty.ts +9 -0
  96. tripl-0.2.2/tests/codegen/custom/golden/ts/acmeScreens.ts +20 -0
  97. tripl-0.2.2/tests/codegen/custom/golden/ts/acmeStructured.ts +20 -0
  98. tripl-0.2.2/tests/codegen/custom/golden/ts/triplTransport.ts +59 -0
  99. tripl-0.2.2/tests/codegen/custom/model.json +65 -0
  100. tripl-0.2.2/tests/codegen/custom/templates/holder.kotlin.mustache +12 -0
  101. tripl-0.2.2/tests/codegen/custom/templates/holder.ts.mustache +9 -0
  102. tripl-0.2.2/tests/codegen/custom/templates/named.kotlin.mustache +27 -0
  103. tripl-0.2.2/tests/codegen/custom/templates/named.swift.mustache +23 -0
  104. tripl-0.2.2/tests/codegen/custom/templates/named.ts.mustache +23 -0
  105. tripl-0.2.2/tests/codegen/custom/templates/planned.swift.mustache +11 -0
  106. tripl-0.2.2/tests/codegen/custom/templates/screen.kotlin.mustache +18 -0
  107. tripl-0.2.2/tests/codegen/custom/templates/screen.swift.mustache +30 -0
  108. tripl-0.2.2/tests/codegen/custom/templates/screen.ts.mustache +17 -0
  109. tripl-0.2.2/tests/codegen/custom/templates/structured.kotlin.mustache +21 -0
  110. tripl-0.2.2/tests/codegen/custom/templates/structured.swift.mustache +27 -0
  111. tripl-0.2.2/tests/codegen/custom/templates/structured.ts.mustache +19 -0
  112. tripl-0.2.2/tests/codegen/golden/kotlin/AppEvents.kt +43 -0
  113. tripl-0.2.2/tests/codegen/golden/kotlin/LegacyTracking.kt +79 -0
  114. tripl-0.2.2/tests/codegen/golden/kotlin/ScreenTracking.kt +36 -0
  115. tripl-0.2.2/tests/codegen/golden/kotlin/SdTracking.kt +57 -0
  116. tripl-0.2.2/tests/codegen/golden/kotlin/TriplTransport.kt +30 -0
  117. tripl-0.2.2/tests/codegen/golden/swift/AppEvents.swift +39 -0
  118. tripl-0.2.2/tests/codegen/golden/swift/LegacyTracking.swift +132 -0
  119. tripl-0.2.2/tests/codegen/golden/swift/ScreenTracking.swift +32 -0
  120. tripl-0.2.2/tests/codegen/golden/swift/SdTracking.swift +79 -0
  121. tripl-0.2.2/tests/codegen/golden/swift/TriplTransport.swift +35 -0
  122. tripl-0.2.2/tests/codegen/golden/ts/appEvents.ts +43 -0
  123. tripl-0.2.2/tests/codegen/golden/ts/legacyTracking.ts +83 -0
  124. tripl-0.2.2/tests/codegen/golden/ts/screenTracking.ts +36 -0
  125. tripl-0.2.2/tests/codegen/golden/ts/sdTracking.ts +69 -0
  126. tripl-0.2.2/tests/codegen/golden/ts/triplTransport.ts +59 -0
  127. tripl-0.2.2/tests/codegen/model.json +93 -0
  128. {tripl-0.2.0 → tripl-0.2.2}/tests/conftest.py +64 -3
  129. tripl-0.2.2/tests/test_annotate_cmd.py +298 -0
  130. {tripl-0.2.0 → tripl-0.2.2}/tests/test_api_requests.py +97 -1
  131. tripl-0.2.2/tests/test_check_cmd.py +640 -0
  132. tripl-0.2.2/tests/test_check_scan.py +639 -0
  133. {tripl-0.2.0 → tripl-0.2.2}/tests/test_checks.py +10 -4
  134. tripl-0.2.2/tests/test_codegen_cmd.py +305 -0
  135. tripl-0.2.2/tests/test_codegen_custom.py +653 -0
  136. tripl-0.2.2/tests/test_codegen_generate.py +617 -0
  137. tripl-0.2.2/tests/test_codegen_naming.py +128 -0
  138. tripl-0.2.2/tests/test_codegen_template.py +83 -0
  139. {tripl-0.2.0 → tripl-0.2.2}/tests/test_contract.py +307 -19
  140. tripl-0.2.2/tests/test_docs_cmd.py +431 -0
  141. {tripl-0.2.0 → tripl-0.2.2}/tests/test_events_cmd.py +112 -0
  142. {tripl-0.2.0 → tripl-0.2.2}/tests/test_output_samples.py +43 -6
  143. {tripl-0.2.0 → tripl-0.2.2}/tests/test_plan_cmd.py +72 -5
  144. {tripl-0.2.0 → tripl-0.2.2}/tests/test_watch.py +2 -2
  145. {tripl-0.2.0 → tripl-0.2.2}/tests/test_watch_diff.py +1 -1
  146. tripl-0.2.2/tests/test_whoami_cmd.py +99 -0
  147. {tripl-0.2.0 → tripl-0.2.2}/uv.lock +1 -1
  148. {tripl-0.2.0 → tripl-0.2.2}/.gitignore +0 -0
  149. {tripl-0.2.0 → tripl-0.2.2}/LICENSE +0 -0
  150. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/__init__.py +0 -0
  151. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/__main__.py +0 -0
  152. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/auth.py +0 -0
  153. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/data_sources.py +0 -0
  154. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/event_types.py +0 -0
  155. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/monitoring.py +0 -0
  156. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/projects.py +0 -0
  157. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/request.py +0 -0
  158. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/scans.py +0 -0
  159. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/api/variables.py +0 -0
  160. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/_plan.py +0 -0
  161. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/_resolve.py +0 -0
  162. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/_write.py +0 -0
  163. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/doctor.py +0 -0
  164. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/drifts.py +0 -0
  165. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/scans.py +0 -0
  166. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/status.py +0 -0
  167. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/upgrade.py +0 -0
  168. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/commands/watch.py +0 -0
  169. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/config.py +0 -0
  170. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/diagnostics/__init__.py +0 -0
  171. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/diagnostics/checks.py +0 -0
  172. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/errors.py +0 -0
  173. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/install/__init__.py +0 -0
  174. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/install/docker.py +0 -0
  175. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/install/files/rabbitmq.conf +0 -0
  176. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/install/health.py +0 -0
  177. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/install/plan.py +0 -0
  178. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/install/render.py +0 -0
  179. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/install/secrets.py +0 -0
  180. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/install/shell.py +0 -0
  181. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/py.typed +0 -0
  182. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/runner.py +0 -0
  183. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/watch/__init__.py +0 -0
  184. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/watch/diff.py +0 -0
  185. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/watch/loop.py +0 -0
  186. {tripl-0.2.0 → tripl-0.2.2}/src/tripl_cli/watch/render.py +0 -0
  187. {tripl-0.2.0 → tripl-0.2.2}/tests/__init__.py +0 -0
  188. {tripl-0.2.0 → tripl-0.2.2}/tests/test_cli.py +0 -0
  189. {tripl-0.2.0 → tripl-0.2.2}/tests/test_client.py +0 -0
  190. {tripl-0.2.0 → tripl-0.2.2}/tests/test_config.py +0 -0
  191. {tripl-0.2.0 → tripl-0.2.2}/tests/test_docker_detect.py +0 -0
  192. {tripl-0.2.0 → tripl-0.2.2}/tests/test_doctor.py +0 -0
  193. {tripl-0.2.0 → tripl-0.2.2}/tests/test_drifts_cmd.py +0 -0
  194. {tripl-0.2.0 → tripl-0.2.2}/tests/test_install_cmd.py +0 -0
  195. {tripl-0.2.0 → tripl-0.2.2}/tests/test_install_files.py +0 -0
  196. {tripl-0.2.0 → tripl-0.2.2}/tests/test_runner.py +0 -0
  197. {tripl-0.2.0 → tripl-0.2.2}/tests/test_scans_cmd.py +0 -0
  198. {tripl-0.2.0 → tripl-0.2.2}/tests/test_status.py +0 -0
  199. {tripl-0.2.0 → tripl-0.2.2}/tests/test_upgrade_cmd.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: tripl
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Operator CLI for a tripl tracking-plan instance: diagnostics, health and live monitoring
5
5
  Project-URL: Homepage, https://vladenisov.github.io/tripl/
6
6
  Project-URL: Documentation, https://vladenisov.github.io/tripl/run/cli
@@ -98,6 +98,13 @@ tripl scans cancel <scan> <job-id> --project SLUG # cancel an active job (WRIT
98
98
  tripl drifts list # schema drifts; untriaged by default
99
99
  tripl drifts dismiss <drift-id> --project SLUG # false_positive or snooze (WRITE)
100
100
  tripl drifts reopen <drift-id> --project SLUG # back to open; drops the note (WRITE)
101
+ tripl annotate "Deployed web 2026.09.25" --project SLUG --url URL # deploy marker on monitoring charts (WRITE)
102
+ tripl check # validate the tracking calls in this checkout against the plan
103
+ tripl check --payloads events.ndjson # validate captured events; a missing required field fails
104
+ tripl check --format sarif > tripl.sarif # SARIF 2.1.0 for code scanning
105
+ tripl codegen # typed tracking code (Swift, Kotlin, TypeScript) from the plan
106
+ tripl codegen --check # CI: exit 1 when the committed generated files are out of date
107
+ tripl export --out plan-schemas # one JSON Schema (2020-12) per event, plus the bundle
101
108
  tripl install --app-url https://tripl.example.com --version 1.5.0 # provision a stack and start it (HOST)
102
109
  tripl install --app-url https://tripl.example.com --dry-run # print the plan, write nothing
103
110
  tripl upgrade --to 1.6.0 # move an installed stack to a new image tag (HOST)
@@ -108,7 +115,7 @@ cannot move you onto an image you have not read the notes for. The command says
108
115
  so on stderr when you leave it at the default.
109
116
 
110
117
  `doctor`, `status`, `watch`, `scans list`, `scans jobs` and `drifts list` are
111
- **read-only** — a `tk_r_` key is enough. The four marked WRITE need a `tk_w_`
118
+ **read-only** — a `tk_r_` key is enough. The five marked WRITE need a `tk_w_`
112
119
  key backed by an editor or owner, and the CLI does not pre-judge that: the key
113
120
  prefix is derived from the scope's first letter server-side and says nothing
114
121
  about the user's role, so the request is sent and the API's own 403 is printed.
@@ -116,12 +123,122 @@ about the user's role, so the request is sent and the API's own 403 is printed.
116
123
  `scans cancel`, `drifts dismiss` and `drifts reopen` prompt on a terminal and
117
124
  take `--yes`; when stdin is **not** a terminal and `--yes` was not given they
118
125
  refuse with exit 2 rather than hanging a cron job or proceeding silently.
119
- `scans run` does not prompt and has no `--yes` at all — passing one is exit 2,
120
- because a no-op flag here is a flag a script author will assume works on the next
121
- command too. All four writes take `--dry-run`, which resolves everything, prints
126
+ `scans run` and `annotate` do not prompt and have no `--yes` at all — passing one
127
+ is exit 2, because a no-op flag here is a flag a script author will assume works
128
+ on the next command too. All five writes take `--dry-run`, which resolves everything, prints
122
129
  the exact request (method, path, params, body — never a credential) and sends
123
130
  nothing.
124
131
 
132
+ `annotate` posts a chart marker with source `api`, for a deploy step in CI. On
133
+ this one command `--url` is the release link the marker opens, not the instance:
134
+ give the instance with `TRIPL_BASE_URL`, `--base-url`, or `--url` before the
135
+ command name. The API de-duplicates the same `api` label within 24 hours (manual
136
+ annotations are never de-duplicated) and answers
137
+ 200 with the existing marker; `annotate` says so and still exits 0, so a retried
138
+ job is harmless.
139
+
140
+ `check` validates code against the plan. `.tripl/check.yml` (found at or above
141
+ the current directory, up to the repository root; `--check-config PATH`
142
+ otherwise) names the project and, **per event type**, how its calls look — your
143
+ own wrapper first, SDK presets as shorthands:
144
+
145
+ ```yaml
146
+ project: my-app
147
+ sources: ["Sources/**", "web/src/**"]
148
+ enums: [{file: "Sources/**/Events.swift", languages: [swift]}]
149
+ event_types:
150
+ se:
151
+ calls:
152
+ - function: "Analytics.shared.log" # or `pattern:` (a regex)
153
+ args: {category: category, action: action, label: label, properties: properties}
154
+ - objc_selector: "trackWithCategory:action:label:"
155
+ page: {preset: snowplow_screen_view}
156
+ legacy: {calls: [{function: "Analytics.shared.logEvent", name_arg: 0}]}
157
+ ```
158
+
159
+ Presets: `segment_track`, `amplitude_log_event`, `snowplow_structured`,
160
+ `snowplow_screen_view`, `snowplow_self_describing`. Swift, Objective-C, Kotlin,
161
+ Java and TypeScript/JavaScript are read; enum shorthand (`.home`) and qualified
162
+ cases resolve through the enum files, interpolated names become plan variables
163
+ (`"promo_sheet_\(id)_shown"` is `promo_sheet_${id}_shown`), Kotlin/Java
164
+ `.name` / `name()` and Swift `.description` read an enum case's own name, and a
165
+ value only known at runtime is sent as unknown, never as an error (`--strict`
166
+ reports it). `--payloads` validates captured events instead, where a missing
167
+ required field IS an error; a value over the validator's size limits (name 500,
168
+ event type 100, field value 2000 characters, 200 fields) is sent as unknown and
169
+ flagged as an `oversize_value` warning on its line.
170
+ `check` reads nothing but the plan: any member's key works, `tk_r_` included.
171
+
172
+ `codegen` turns the plan into typed tracking code, from the same
173
+ `.tripl/check.yml`. It generates **per event-type style, never a function per
174
+ event**, and the generated code calls **your own wrapper** (the `transport`) —
175
+ or, with none configured, the shared `TriplDestination` in
176
+ `TriplTransport.swift` / `.kt` / `triplTransport.ts` that you implement once.
177
+ It never imports an SDK.
178
+
179
+ | `style` | What is generated |
180
+ |---------|-------------------|
181
+ | `structured` | an enum per plan field (its cases are the plan's values; a variable-backed value contributes its allowed values; free text stays `String`), your wrapper's call with its argument types narrowed — `log(category: Category, action: Action, label: String, properties:)` — and a compiler-checked `knownEvents` list |
182
+ | `screen_view` | the same, with `ScreenType` / `ScreenId` enums |
183
+ | `named` | one generic `track(event)`: Swift `enum LegacyEvent { case homeScreenView(HomeScreenView) … }` with `name` and `properties`; Kotlin a sealed interface of data classes/objects; TypeScript `track<K extends LegacyEventName>(name: K, props: LegacyEventProps[K])` |
184
+ | `self_describing` | one data class per schema with its fields, sharing one `track` |
185
+
186
+ ```yaml
187
+ codegen:
188
+ out: {swift: Sources/Tracking/Generated, kotlin: app/src/main/java/tracking, ts: web/src/tracking}
189
+ kotlin_package: com.example.tracking
190
+ event_types:
191
+ se:
192
+ calls: [{function: "Analytics.shared.log", args: {category: category, action: action, label: label, properties: properties}}]
193
+ codegen:
194
+ style: structured # default: from the preset, else structured when the type has a name rule
195
+ transport:
196
+ swift: "Analytics.shared.log" # reuses the `calls` entry's args mapping
197
+ ts: {function: "analytics.log", positional: [category, action, label, properties],
198
+ import: "import { analytics } from './analytics';"}
199
+ type_names: {namespace: AppEvents, category: EventCategory, function: log}
200
+ template: {swift: .tripl/templates/structured.swift.mustache} # optional override
201
+ legacy:
202
+ calls: [{function: "Analytics.shared.logEvent", name_arg: 0, properties_arg: parameters}]
203
+ codegen: {style: named, languages: [swift, kotlin]}
204
+ ```
205
+
206
+ A `transport` given as a bare function reuses the `args` / `positional` /
207
+ `object_arg` mapping of the `calls` entry with the same `function`. Swift passes
208
+ labelled arguments with their labels, Kotlin as named arguments (give
209
+ `positional` for a Java wrapper), TypeScript positionally — or as one object
210
+ literal with `object_arg`. `type_names` renames `namespace`, `event` (the named
211
+ / self-describing event type), `function`, and any field's enum by field name.
212
+
213
+ An event's parameters are the fields it does not fix plus one per `${token}` in
214
+ its name (`promo_sheet_${sheet_id}_shown` takes `sheetId`, typed by the
215
+ variable's allowed values). Plan strings become identifiers by splitting on
216
+ anything that is not a letter or digit and on camelCase: `Home Screen View` ->
217
+ `homeScreenView` (`HOME_SCREEN_VIEW` for Kotlin enum entries), `checkout:start`
218
+ -> `checkoutStart`; a leading digit gets `_` (`_1stRun`), a reserved word a
219
+ trailing `_` (`default_`), a reserved type name `Value` (`TypeValue`), and a
220
+ collision `2`, `3` in sorted order. The raw plan string is always what is sent.
221
+ Deprecated events are marked (`@available(*, deprecated)`, `@Deprecated`,
222
+ `@deprecated`); archived ones are not generated.
223
+
224
+ A custom `template` uses the built-in one's context (copy it from
225
+ `tripl_cli/codegen/templates/`): a Mustache subset — `{{name}}`, `{{a.b}}`,
226
+ `{{#list}}…{{/list}}`, `{{^empty}}…{{/empty}}`, `{{! comment }}` — where every
227
+ plan string arrives already quoted or escaped for the language, and an unknown
228
+ `{{name}}` is an error rather than an empty string.
229
+
230
+ Output is deterministic (sorted, no timestamps) with a `Generated by tripl
231
+ codegen — do not edit` header naming the project, branch and the plan's
232
+ content hash (not its revision, so a new revision that changes nothing is not
233
+ drift), so it can be committed: `tripl codegen --check` writes nothing and exits
234
+ 1 when a file is missing, differs, or is stale (generated earlier, no longer
235
+ produced — a normal run deletes those, and only files carrying the header for
236
+ the same project, in a language the run writes into that directory). `--model FILE`
237
+ generates from a saved `tripl export --format codegen_model` without an
238
+ instance. `export` writes `bundle.json` and `<event type>/<identity>.schema.json`
239
+ per event (file names sanitised to `[A-Za-z0-9._-]`), or prints the export to
240
+ stdout without `--out`. Both read nothing but the plan: any member's key works.
241
+
125
242
  `drifts reopen` is the one whose prompt is worth reading: reopening clears the
126
243
  drift's `resolution_note`, `resolved_by` and `resolved_at`, and dismissing it
127
244
  again does not bring them back.
@@ -238,10 +355,15 @@ not moved).
238
355
 
239
356
  | Exit | Meaning |
240
357
  |------|---------|
241
- | 0 | Every check passed, or only warned and `--strict` was not given. `status`, whenever it completed. `watch`, whenever the run completed — a failed job or a new signal is still 0. The `scans` / `drifts` verbs, whenever every read arrived or the write was accepted (`--dry-run` included). |
358
+ | 0 | Every check passed, or only warned and `--strict` was not given. `status`, whenever it completed. `watch`, whenever the run completed — a failed job or a new signal is still 0. The `scans` / `drifts` verbs and `annotate`, whenever every read arrived or the write was accepted (`--dry-run` included, and a de-duplicated `annotate` too). |
242
359
  | 1 | The tool itself broke (doctor turns every API failure into a finding), or any other command could not complete a request — unreachable, or the API refused it. For `watch` this includes a key revoked mid-run. For `scans list` / `drifts list` it includes **any** failed read in the fan-out; for `scans run`, a job returned already `failed`; for `scans cancel` / `drifts dismiss` / `drifts reopen`, a declined prompt. |
243
- | 2 | Usage or configuration error. For `doctor` and `status` that is resolved before any socket opens; `watch` also refuses after reading the listings, when `--scan` matches nothing or more than 24 scan configs are selected. The `scans` / `drifts` verbs add a bare group, a missing or repeated `--project`, an unresolved or ambiguous `<scan>`, and a prompting write on a non-TTY without `--yes`. Either way **no JSON is emitted** and no write is sent. |
360
+ | 2 | Usage or configuration error. For `doctor` and `status` that is resolved before any socket opens; `watch` also refuses after reading the listings, when `--scan` matches nothing or more than 24 scan configs are selected. The `scans` / `drifts` verbs add a bare group, a missing or repeated `--project`, an unresolved or ambiguous `<scan>`, and a prompting write on a non-TTY without `--yes`; `annotate` adds a `--url` that is not http(s), an `--at` that is not RFC 3339, and half a scope. Either way **no JSON is emitted** and no write is sent. |
244
361
  | 3 | `doctor` only: at least one check failed, or `--strict` and at least one warning. Nothing else ever exits 3. |
362
+
363
+ `check` uses 0, 1 and 2: 1 when any call site or event has an error (or, with
364
+ `--strict`, a warning), and 2 for a bad check config or payload file.
365
+ `codegen` uses them too: 1 for drift under `--check` (or a plan without a
366
+ configured event type), 2 for a bad check config or template.
245
367
  | 130 | Interrupted (SIGINT). For `watch` this is the **normal** ending — a run without `--duration` has no other way to stop. |
246
368
 
247
369
  An unreachable instance therefore exits **3** out of `doctor`, not 1 — it
@@ -70,6 +70,13 @@ tripl scans cancel <scan> <job-id> --project SLUG # cancel an active job (WRIT
70
70
  tripl drifts list # schema drifts; untriaged by default
71
71
  tripl drifts dismiss <drift-id> --project SLUG # false_positive or snooze (WRITE)
72
72
  tripl drifts reopen <drift-id> --project SLUG # back to open; drops the note (WRITE)
73
+ tripl annotate "Deployed web 2026.09.25" --project SLUG --url URL # deploy marker on monitoring charts (WRITE)
74
+ tripl check # validate the tracking calls in this checkout against the plan
75
+ tripl check --payloads events.ndjson # validate captured events; a missing required field fails
76
+ tripl check --format sarif > tripl.sarif # SARIF 2.1.0 for code scanning
77
+ tripl codegen # typed tracking code (Swift, Kotlin, TypeScript) from the plan
78
+ tripl codegen --check # CI: exit 1 when the committed generated files are out of date
79
+ tripl export --out plan-schemas # one JSON Schema (2020-12) per event, plus the bundle
73
80
  tripl install --app-url https://tripl.example.com --version 1.5.0 # provision a stack and start it (HOST)
74
81
  tripl install --app-url https://tripl.example.com --dry-run # print the plan, write nothing
75
82
  tripl upgrade --to 1.6.0 # move an installed stack to a new image tag (HOST)
@@ -80,7 +87,7 @@ cannot move you onto an image you have not read the notes for. The command says
80
87
  so on stderr when you leave it at the default.
81
88
 
82
89
  `doctor`, `status`, `watch`, `scans list`, `scans jobs` and `drifts list` are
83
- **read-only** — a `tk_r_` key is enough. The four marked WRITE need a `tk_w_`
90
+ **read-only** — a `tk_r_` key is enough. The five marked WRITE need a `tk_w_`
84
91
  key backed by an editor or owner, and the CLI does not pre-judge that: the key
85
92
  prefix is derived from the scope's first letter server-side and says nothing
86
93
  about the user's role, so the request is sent and the API's own 403 is printed.
@@ -88,12 +95,122 @@ about the user's role, so the request is sent and the API's own 403 is printed.
88
95
  `scans cancel`, `drifts dismiss` and `drifts reopen` prompt on a terminal and
89
96
  take `--yes`; when stdin is **not** a terminal and `--yes` was not given they
90
97
  refuse with exit 2 rather than hanging a cron job or proceeding silently.
91
- `scans run` does not prompt and has no `--yes` at all — passing one is exit 2,
92
- because a no-op flag here is a flag a script author will assume works on the next
93
- command too. All four writes take `--dry-run`, which resolves everything, prints
98
+ `scans run` and `annotate` do not prompt and have no `--yes` at all — passing one
99
+ is exit 2, because a no-op flag here is a flag a script author will assume works
100
+ on the next command too. All five writes take `--dry-run`, which resolves everything, prints
94
101
  the exact request (method, path, params, body — never a credential) and sends
95
102
  nothing.
96
103
 
104
+ `annotate` posts a chart marker with source `api`, for a deploy step in CI. On
105
+ this one command `--url` is the release link the marker opens, not the instance:
106
+ give the instance with `TRIPL_BASE_URL`, `--base-url`, or `--url` before the
107
+ command name. The API de-duplicates the same `api` label within 24 hours (manual
108
+ annotations are never de-duplicated) and answers
109
+ 200 with the existing marker; `annotate` says so and still exits 0, so a retried
110
+ job is harmless.
111
+
112
+ `check` validates code against the plan. `.tripl/check.yml` (found at or above
113
+ the current directory, up to the repository root; `--check-config PATH`
114
+ otherwise) names the project and, **per event type**, how its calls look — your
115
+ own wrapper first, SDK presets as shorthands:
116
+
117
+ ```yaml
118
+ project: my-app
119
+ sources: ["Sources/**", "web/src/**"]
120
+ enums: [{file: "Sources/**/Events.swift", languages: [swift]}]
121
+ event_types:
122
+ se:
123
+ calls:
124
+ - function: "Analytics.shared.log" # or `pattern:` (a regex)
125
+ args: {category: category, action: action, label: label, properties: properties}
126
+ - objc_selector: "trackWithCategory:action:label:"
127
+ page: {preset: snowplow_screen_view}
128
+ legacy: {calls: [{function: "Analytics.shared.logEvent", name_arg: 0}]}
129
+ ```
130
+
131
+ Presets: `segment_track`, `amplitude_log_event`, `snowplow_structured`,
132
+ `snowplow_screen_view`, `snowplow_self_describing`. Swift, Objective-C, Kotlin,
133
+ Java and TypeScript/JavaScript are read; enum shorthand (`.home`) and qualified
134
+ cases resolve through the enum files, interpolated names become plan variables
135
+ (`"promo_sheet_\(id)_shown"` is `promo_sheet_${id}_shown`), Kotlin/Java
136
+ `.name` / `name()` and Swift `.description` read an enum case's own name, and a
137
+ value only known at runtime is sent as unknown, never as an error (`--strict`
138
+ reports it). `--payloads` validates captured events instead, where a missing
139
+ required field IS an error; a value over the validator's size limits (name 500,
140
+ event type 100, field value 2000 characters, 200 fields) is sent as unknown and
141
+ flagged as an `oversize_value` warning on its line.
142
+ `check` reads nothing but the plan: any member's key works, `tk_r_` included.
143
+
144
+ `codegen` turns the plan into typed tracking code, from the same
145
+ `.tripl/check.yml`. It generates **per event-type style, never a function per
146
+ event**, and the generated code calls **your own wrapper** (the `transport`) —
147
+ or, with none configured, the shared `TriplDestination` in
148
+ `TriplTransport.swift` / `.kt` / `triplTransport.ts` that you implement once.
149
+ It never imports an SDK.
150
+
151
+ | `style` | What is generated |
152
+ |---------|-------------------|
153
+ | `structured` | an enum per plan field (its cases are the plan's values; a variable-backed value contributes its allowed values; free text stays `String`), your wrapper's call with its argument types narrowed — `log(category: Category, action: Action, label: String, properties:)` — and a compiler-checked `knownEvents` list |
154
+ | `screen_view` | the same, with `ScreenType` / `ScreenId` enums |
155
+ | `named` | one generic `track(event)`: Swift `enum LegacyEvent { case homeScreenView(HomeScreenView) … }` with `name` and `properties`; Kotlin a sealed interface of data classes/objects; TypeScript `track<K extends LegacyEventName>(name: K, props: LegacyEventProps[K])` |
156
+ | `self_describing` | one data class per schema with its fields, sharing one `track` |
157
+
158
+ ```yaml
159
+ codegen:
160
+ out: {swift: Sources/Tracking/Generated, kotlin: app/src/main/java/tracking, ts: web/src/tracking}
161
+ kotlin_package: com.example.tracking
162
+ event_types:
163
+ se:
164
+ calls: [{function: "Analytics.shared.log", args: {category: category, action: action, label: label, properties: properties}}]
165
+ codegen:
166
+ style: structured # default: from the preset, else structured when the type has a name rule
167
+ transport:
168
+ swift: "Analytics.shared.log" # reuses the `calls` entry's args mapping
169
+ ts: {function: "analytics.log", positional: [category, action, label, properties],
170
+ import: "import { analytics } from './analytics';"}
171
+ type_names: {namespace: AppEvents, category: EventCategory, function: log}
172
+ template: {swift: .tripl/templates/structured.swift.mustache} # optional override
173
+ legacy:
174
+ calls: [{function: "Analytics.shared.logEvent", name_arg: 0, properties_arg: parameters}]
175
+ codegen: {style: named, languages: [swift, kotlin]}
176
+ ```
177
+
178
+ A `transport` given as a bare function reuses the `args` / `positional` /
179
+ `object_arg` mapping of the `calls` entry with the same `function`. Swift passes
180
+ labelled arguments with their labels, Kotlin as named arguments (give
181
+ `positional` for a Java wrapper), TypeScript positionally — or as one object
182
+ literal with `object_arg`. `type_names` renames `namespace`, `event` (the named
183
+ / self-describing event type), `function`, and any field's enum by field name.
184
+
185
+ An event's parameters are the fields it does not fix plus one per `${token}` in
186
+ its name (`promo_sheet_${sheet_id}_shown` takes `sheetId`, typed by the
187
+ variable's allowed values). Plan strings become identifiers by splitting on
188
+ anything that is not a letter or digit and on camelCase: `Home Screen View` ->
189
+ `homeScreenView` (`HOME_SCREEN_VIEW` for Kotlin enum entries), `checkout:start`
190
+ -> `checkoutStart`; a leading digit gets `_` (`_1stRun`), a reserved word a
191
+ trailing `_` (`default_`), a reserved type name `Value` (`TypeValue`), and a
192
+ collision `2`, `3` in sorted order. The raw plan string is always what is sent.
193
+ Deprecated events are marked (`@available(*, deprecated)`, `@Deprecated`,
194
+ `@deprecated`); archived ones are not generated.
195
+
196
+ A custom `template` uses the built-in one's context (copy it from
197
+ `tripl_cli/codegen/templates/`): a Mustache subset — `{{name}}`, `{{a.b}}`,
198
+ `{{#list}}…{{/list}}`, `{{^empty}}…{{/empty}}`, `{{! comment }}` — where every
199
+ plan string arrives already quoted or escaped for the language, and an unknown
200
+ `{{name}}` is an error rather than an empty string.
201
+
202
+ Output is deterministic (sorted, no timestamps) with a `Generated by tripl
203
+ codegen — do not edit` header naming the project, branch and the plan's
204
+ content hash (not its revision, so a new revision that changes nothing is not
205
+ drift), so it can be committed: `tripl codegen --check` writes nothing and exits
206
+ 1 when a file is missing, differs, or is stale (generated earlier, no longer
207
+ produced — a normal run deletes those, and only files carrying the header for
208
+ the same project, in a language the run writes into that directory). `--model FILE`
209
+ generates from a saved `tripl export --format codegen_model` without an
210
+ instance. `export` writes `bundle.json` and `<event type>/<identity>.schema.json`
211
+ per event (file names sanitised to `[A-Za-z0-9._-]`), or prints the export to
212
+ stdout without `--out`. Both read nothing but the plan: any member's key works.
213
+
97
214
  `drifts reopen` is the one whose prompt is worth reading: reopening clears the
98
215
  drift's `resolution_note`, `resolved_by` and `resolved_at`, and dismissing it
99
216
  again does not bring them back.
@@ -210,10 +327,15 @@ not moved).
210
327
 
211
328
  | Exit | Meaning |
212
329
  |------|---------|
213
- | 0 | Every check passed, or only warned and `--strict` was not given. `status`, whenever it completed. `watch`, whenever the run completed — a failed job or a new signal is still 0. The `scans` / `drifts` verbs, whenever every read arrived or the write was accepted (`--dry-run` included). |
330
+ | 0 | Every check passed, or only warned and `--strict` was not given. `status`, whenever it completed. `watch`, whenever the run completed — a failed job or a new signal is still 0. The `scans` / `drifts` verbs and `annotate`, whenever every read arrived or the write was accepted (`--dry-run` included, and a de-duplicated `annotate` too). |
214
331
  | 1 | The tool itself broke (doctor turns every API failure into a finding), or any other command could not complete a request — unreachable, or the API refused it. For `watch` this includes a key revoked mid-run. For `scans list` / `drifts list` it includes **any** failed read in the fan-out; for `scans run`, a job returned already `failed`; for `scans cancel` / `drifts dismiss` / `drifts reopen`, a declined prompt. |
215
- | 2 | Usage or configuration error. For `doctor` and `status` that is resolved before any socket opens; `watch` also refuses after reading the listings, when `--scan` matches nothing or more than 24 scan configs are selected. The `scans` / `drifts` verbs add a bare group, a missing or repeated `--project`, an unresolved or ambiguous `<scan>`, and a prompting write on a non-TTY without `--yes`. Either way **no JSON is emitted** and no write is sent. |
332
+ | 2 | Usage or configuration error. For `doctor` and `status` that is resolved before any socket opens; `watch` also refuses after reading the listings, when `--scan` matches nothing or more than 24 scan configs are selected. The `scans` / `drifts` verbs add a bare group, a missing or repeated `--project`, an unresolved or ambiguous `<scan>`, and a prompting write on a non-TTY without `--yes`; `annotate` adds a `--url` that is not http(s), an `--at` that is not RFC 3339, and half a scope. Either way **no JSON is emitted** and no write is sent. |
216
333
  | 3 | `doctor` only: at least one check failed, or `--strict` and at least one warning. Nothing else ever exits 3. |
334
+
335
+ `check` uses 0, 1 and 2: 1 when any call site or event has an error (or, with
336
+ `--strict`, a warning), and 2 for a bad check config or payload file.
337
+ `codegen` uses them too: 1 for drift under `--check` (or a plan without a
338
+ configured event type), 2 for a bad check config or template.
217
339
  | 130 | Interrupted (SIGINT). For `watch` this is the **normal** ending — a run without `--duration` has no other way to stop. |
218
340
 
219
341
  An unreachable instance therefore exits **3** out of `doctor`, not 1 — it
@@ -1,10 +1,9 @@
1
1
  [project]
2
2
  name = "tripl"
3
- # 0.2.0: `tripl_cli.api` gained the `page_items` / `page_total` re-exports after
4
- # 0.1.0 shipped, and `tripl-mcp` imports them. The published 0.1.0 does not have
5
- # them, so tripl-mcp cannot be released against it — this has to go out FIRST
6
- # (tag `cli-v0.2.0`), then tripl-mcp 0.2.0 (tripl-yh8t).
7
- version = "0.2.0"
3
+ # 0.2.2: `tripl_cli.api` gained the `docs` module and typed-property reads
4
+ # that tripl-mcp 0.2.2 imports, so this has to reach the index FIRST (tag
5
+ # `cli-v0.2.2`), then tripl-mcp 0.2.2. Versioned with the service release.
6
+ version = "0.2.2"
8
7
  description = "Operator CLI for a tripl tracking-plan instance: diagnostics, health and live monitoring"
9
8
  readme = "README.md"
10
9
  requires-python = ">=3.12"
@@ -91,6 +90,8 @@ build-backend = "hatchling.build"
91
90
  # package data already. Verified against a built wheel rather than assumed —
92
91
  # `uv build --wheel` then unzip, and both files are there (tripl-ey6j.3). They
93
92
  # are read through importlib.resources, which is why this matters at all.
93
+ # The same holds for src/tripl_cli/codegen/templates/*.mustache, the built-in
94
+ # `tripl codegen` templates (#262): package data by the same rule, read the same way.
94
95
  packages = ["src/tripl_cli"]
95
96
 
96
97
  [tool.pytest.ini_options]
@@ -46,10 +46,14 @@ from __future__ import annotations
46
46
  from tripl_cli.api import (
47
47
  auth,
48
48
  branches,
49
+ chart_annotations,
49
50
  data_sources,
51
+ docs,
50
52
  event_types,
51
53
  events,
52
54
  monitoring,
55
+ plan_export,
56
+ plan_validation,
53
57
  projects,
54
58
  scans,
55
59
  search,
@@ -65,12 +69,16 @@ __all__ = [
65
69
  "ApiRequest",
66
70
  "auth",
67
71
  "branches",
72
+ "chart_annotations",
68
73
  "data_sources",
74
+ "docs",
69
75
  "event_types",
70
76
  "events",
71
77
  "monitoring",
72
78
  "page_items",
73
79
  "page_total",
80
+ "plan_export",
81
+ "plan_validation",
74
82
  "projects",
75
83
  "scans",
76
84
  "search",
@@ -5,12 +5,13 @@ back to a human in the tripl UI (see ``tripl_mcp.tools.branches``). A builder
5
5
  would be the first half of shipping them.
6
6
 
7
7
  Note how the OTHER builders spell "the live plan": by leaving ``branch`` unset.
8
- ``deps.get_branch_id_override`` reads the RAW query string, resolves main
9
- whenever ``?branch=`` is missing or empty, and refuses anything that is not a
10
- UUID belonging to this project (400) or names no branch of it (404). So there is
11
- no literal for main, here or anywhere — which is also why ``?branch=`` is absent
12
- from ``backend/openapi.json``: it is a dependency rather than a declared query
13
- parameter, and the contract test therefore cannot see it.
8
+ ``deps.get_branch_id_override`` resolves main whenever ``?branch=`` is missing or
9
+ empty, and refuses anything that is not a UUID belonging to this project (400) or
10
+ names no branch of it (404). So there is no literal for main, here or anywhere.
11
+ It IS a declared query parameter, so ``backend/openapi.json`` carries it and the
12
+ contract test can see it — typed ``str`` rather than ``format: uuid`` because the
13
+ dependency parses the value itself, which is what keeps a malformed id a 400
14
+ instead of FastAPI's own 422.
14
15
  """
15
16
 
16
17
  from __future__ import annotations
@@ -31,8 +32,9 @@ ENDPOINTS: tuple[tuple[str, str], ...] = (
31
32
  def list_branches(slug: str, *, include_diff_counts: bool = False) -> ApiRequest:
32
33
  """Answers ``{items, total}``, not a bare list.
33
34
 
34
- ``include_diff_counts`` fills ``ahead`` and ``behind_base``; without it the
35
- service leaves both null, because each row costs a snapshot comparison. Off
35
+ ``include_diff_counts`` fills ``ahead`` and ``behind_base`` on open branches
36
+ (merged and closed ones, like main, stay null); without it the service
37
+ leaves both null everywhere, because each row costs a snapshot comparison. Off
36
38
  by default for that reason, and asked for in exactly one place: ``tripl plan
37
39
  branches``, where those two columns are the reason to run the command at
38
40
  all. The branch RESOLUTION path must keep the default — it needs only id and
@@ -0,0 +1,73 @@
1
+ """Chart annotations: the markers charts draw at a point in time.
2
+
3
+ Only the create route has a builder. ``tripl annotate`` is the one caller, and it
4
+ exists for CI: a deploy step posts "Deployed web 2026.09.25" with a link to the
5
+ release, and every monitoring (Volume tab) chart in the project shows it.
6
+
7
+ The route answers **201** for a new row and **200** when it de-duplicated: the
8
+ same label posted with source ``api`` to the same project within the last 24
9
+ hours returns the row that already exists instead of creating a second one.
10
+ Only ``api`` is de-duplicated here (``release`` is unique per label forever, and
11
+ is worker-only); ``manual`` creates are never de-duplicated and always 201. A retried CI job is
12
+ therefore safe to re-run, and the status code is the only way to tell the two
13
+ answers apart - the bodies have the same shape.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import datetime
19
+
20
+ from tripl_cli.api.request import ApiRequest
21
+ from tripl_cli.model import JsonDict, to_rfc3339
22
+
23
+ CREATE = "/projects/{slug}/annotations"
24
+
25
+ ENDPOINTS: tuple[tuple[str, str], ...] = (("post", CREATE),)
26
+
27
+ # `ChartAnnotationScopeType` verbatim. A scoped annotation only shows on charts
28
+ # of that scope; a project-level one (no scope) shows on every monitoring
29
+ # (Volume tab) chart in the project.
30
+ SCOPE_TYPES: tuple[str, ...] = ("project_total", "event_type", "event", "metric")
31
+
32
+ # What a client may send as `source`. `manual` is the app's own default and
33
+ # `release` is reserved to the metrics worker (the API answers 422), so the CLI
34
+ # always sends `api`.
35
+ SOURCE_API = "api"
36
+
37
+ # `ChartAnnotationCreate`'s own bounds, so a typo costs no request.
38
+ LABEL_MAX_LENGTH = 200
39
+ DESCRIPTION_MAX_LENGTH = 2000
40
+ SCOPE_REF_MAX_LENGTH = 120
41
+ URL_MAX_LENGTH = 500
42
+
43
+ # 201 is a new annotation; 200 is the one that already existed.
44
+ STATUS_CREATED = 201
45
+ STATUS_DEDUPLICATED = 200
46
+
47
+
48
+ def create_annotation(
49
+ slug: str,
50
+ *,
51
+ label: str,
52
+ at: datetime,
53
+ url: str | None = None,
54
+ description: str | None = None,
55
+ scope_type: str | None = None,
56
+ scope_ref: str | None = None,
57
+ ) -> ApiRequest:
58
+ """``POST /projects/{slug}/annotations`` with ``source="api"``.
59
+
60
+ Editor-gated: a ``tk_w_`` key backed by an editor or owner. ``bucket`` is the
61
+ route's name for the timestamp. Unset members are omitted rather than sent
62
+ as null, so the server's defaults (the colour among them) apply.
63
+ """
64
+ body: JsonDict = {"label": label, "bucket": to_rfc3339(at), "source": SOURCE_API}
65
+ if url is not None:
66
+ body["url"] = url
67
+ if description is not None:
68
+ body["description"] = description
69
+ if scope_type is not None:
70
+ body["scope_type"] = scope_type
71
+ if scope_ref is not None:
72
+ body["scope_ref"] = scope_ref
73
+ return ApiRequest("POST", CREATE.format(slug=slug), json_body=body)