capsule-engine 0.1.0__py3-none-any.whl

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 (288) hide show
  1. capsule_engine/__init__.py +17 -0
  2. capsule_engine/bundle_viewer/__init__.py +21 -0
  3. capsule_engine/bundle_viewer/static/offline_shell.html +1509 -0
  4. capsule_engine/bundle_viewer/viewer.py +57 -0
  5. capsule_engine/cli/__init__.py +5 -0
  6. capsule_engine/cli/bundle_cmd.py +269 -0
  7. capsule_engine/cli/console_cmd.py +70 -0
  8. capsule_engine/cli/constraints_cmd.py +74 -0
  9. capsule_engine/cli/format.py +173 -0
  10. capsule_engine/cli/guard_cmds.py +361 -0
  11. capsule_engine/cli/init_cmds.py +169 -0
  12. capsule_engine/cli/main.py +120 -0
  13. capsule_engine/cli/packs_cmds.py +96 -0
  14. capsule_engine/cli/telemetry_cmd.py +80 -0
  15. capsule_engine/cli/tenant_cmds.py +199 -0
  16. capsule_engine/cli/verify_cmd.py +110 -0
  17. capsule_engine/compiler/__init__.py +13 -0
  18. capsule_engine/compiler/effect_model.py +79 -0
  19. capsule_engine/compiler/vocabulary.py +253 -0
  20. capsule_engine/console/__init__.py +10 -0
  21. capsule_engine/console/api.py +251 -0
  22. capsule_engine/console/components.css +406 -0
  23. capsule_engine/console/console.css +130 -0
  24. capsule_engine/console/console.html +51 -0
  25. capsule_engine/console/console.js +330 -0
  26. capsule_engine/console/gallery.html +274 -0
  27. capsule_engine/console/server.py +137 -0
  28. capsule_engine/console/tokens.css +68 -0
  29. capsule_engine/conversation/__init__.py +88 -0
  30. capsule_engine/conversation/capsules.py +268 -0
  31. capsule_engine/conversation/exchange.py +279 -0
  32. capsule_engine/conversation/merkle.py +80 -0
  33. capsule_engine/conversation/session.py +101 -0
  34. capsule_engine/envcompat.py +15 -0
  35. capsule_engine/events/__init__.py +14 -0
  36. capsule_engine/events/capsule.py +133 -0
  37. capsule_engine/examples/__init__.py +9 -0
  38. capsule_engine/examples/data/tau2_airline/LICENSE-tau2-bench +21 -0
  39. capsule_engine/examples/data/tau2_airline/PROVENANCE.json +70 -0
  40. capsule_engine/examples/data/tau2_airline/hand_labels.json +893 -0
  41. capsule_engine/examples/data/tau2_airline/pilot1_live_gemini-2-5-flash_airline_shift1.jsonl +190 -0
  42. capsule_engine/examples/data/tau2_airline/tau2_committed_claude-3-7-sonnet_airline_trial0.jsonl +556 -0
  43. capsule_engine/examples/data/tau2_airline/tau2_committed_gpt-4-1-mini_airline_trial0.jsonl +480 -0
  44. capsule_engine/examples/data/tau2_airline/tau2_committed_gpt-4-1_airline_trial0.jsonl +496 -0
  45. capsule_engine/examples/data/tau2_airline/tau2_committed_o4-mini_airline_trial0.jsonl +392 -0
  46. capsule_engine/examples/data/tau2_airline/tau2_conversations_claude-3-7-sonnet_airline_4trials.jsonl +200 -0
  47. capsule_engine/examples/tau2_airline_reference.py +460 -0
  48. capsule_engine/examples/tau2_conversation_exchange.py +92 -0
  49. capsule_engine/folds/__init__.py +104 -0
  50. capsule_engine/folds/account_core.py +89 -0
  51. capsule_engine/folds/approval_latency.py +86 -0
  52. capsule_engine/folds/catalog.py +79 -0
  53. capsule_engine/folds/catalog_defs/actions.count_by_developer.yaml +10 -0
  54. capsule_engine/folds/catalog_defs/actions.executed_count.yaml +16 -0
  55. capsule_engine/folds/catalog_defs/actions.last_decision.yaml +12 -0
  56. capsule_engine/folds/catalog_defs/counterparty.seen_before.v2.yaml +34 -0
  57. capsule_engine/folds/catalog_defs/counterparty.seen_before.v3.yaml +38 -0
  58. capsule_engine/folds/catalog_defs/counterparty.seen_before.yaml +28 -0
  59. capsule_engine/folds/catalog_defs/disclosure.class_seen.yaml +29 -0
  60. capsule_engine/folds/catalog_defs/disclosure.statement_seen.yaml +32 -0
  61. capsule_engine/folds/catalog_defs/hold.active_exposure.yaml +44 -0
  62. capsule_engine/folds/catalog_defs/spend.weekly.v2.yaml +38 -0
  63. capsule_engine/folds/catalog_defs/spend.weekly.v3.1.yaml +71 -0
  64. capsule_engine/folds/catalog_defs/spend.weekly.v3.yaml +76 -0
  65. capsule_engine/folds/catalog_defs/spend.weekly.yaml +33 -0
  66. capsule_engine/folds/counterparty_signals.py +179 -0
  67. capsule_engine/folds/definition.py +421 -0
  68. capsule_engine/folds/duration.py +16 -0
  69. capsule_engine/folds/engine.py +436 -0
  70. capsule_engine/folds/errors.py +44 -0
  71. capsule_engine/folds/loader.py +24 -0
  72. capsule_engine/folds/ordering.py +143 -0
  73. capsule_engine/folds/paths.py +25 -0
  74. capsule_engine/folds/record_type_coverage.py +58 -0
  75. capsule_engine/folds/reducers.py +75 -0
  76. capsule_engine/folds/retention_continuity.py +117 -0
  77. capsule_engine/folds/set_membership.py +59 -0
  78. capsule_engine/folds/taxonomy.py +231 -0
  79. capsule_engine/guards/__init__.py +59 -0
  80. capsule_engine/guards/action.py +446 -0
  81. capsule_engine/guards/action_taxonomy.json +27 -0
  82. capsule_engine/guards/action_taxonomy_v2.json +27 -0
  83. capsule_engine/guards/action_taxonomy_v3.json +27 -0
  84. capsule_engine/guards/action_taxonomy_v4.json +27 -0
  85. capsule_engine/guards/action_taxonomy_v5.json +27 -0
  86. capsule_engine/guards/capsule.py +377 -0
  87. capsule_engine/guards/checks/__init__.py +268 -0
  88. capsule_engine/guards/checks/action_class_gate.py +199 -0
  89. capsule_engine/guards/checks/base.py +33 -0
  90. capsule_engine/guards/checks/caps.py +517 -0
  91. capsule_engine/guards/checks/channel_change.py +64 -0
  92. capsule_engine/guards/checks/commitment.py +41 -0
  93. capsule_engine/guards/checks/counterparty_identity_change.py +95 -0
  94. capsule_engine/guards/checks/counterparty_list.py +176 -0
  95. capsule_engine/guards/checks/counterparty_seen_before.py +160 -0
  96. capsule_engine/guards/checks/credential_pattern.py +73 -0
  97. capsule_engine/guards/checks/dedupe.py +259 -0
  98. capsule_engine/guards/checks/destination_rail.py +120 -0
  99. capsule_engine/guards/checks/field_change_count.py +100 -0
  100. capsule_engine/guards/checks/offer_expiry.py +134 -0
  101. capsule_engine/guards/checks/plan_containment.py +180 -0
  102. capsule_engine/guards/checks/price_floor.py +211 -0
  103. capsule_engine/guards/checks/promise_never.py +236 -0
  104. capsule_engine/guards/checks/promise_requires_approval.py +214 -0
  105. capsule_engine/guards/checks/recipient_role.py +117 -0
  106. capsule_engine/guards/checks/recurring_charge.py +49 -0
  107. capsule_engine/guards/checks/refundability.py +46 -0
  108. capsule_engine/guards/checks/release_on_acceptance.py +286 -0
  109. capsule_engine/guards/checks/required_disclosure.py +233 -0
  110. capsule_engine/guards/checks/single_commitment.py +221 -0
  111. capsule_engine/guards/checks/task_authority.py +182 -0
  112. capsule_engine/guards/checks/upfront_amount.py +71 -0
  113. capsule_engine/guards/checks/verify_before_dispatch.py +75 -0
  114. capsule_engine/guards/classes.py +393 -0
  115. capsule_engine/guards/engine.py +926 -0
  116. capsule_engine/guards/history_state.py +127 -0
  117. capsule_engine/guards/plan.py +237 -0
  118. capsule_engine/guards/revocation.py +120 -0
  119. capsule_engine/guards/signing.py +60 -0
  120. capsule_engine/guards/statements.py +62 -0
  121. capsule_engine/guards/tool_call.py +83 -0
  122. capsule_engine/guards/wickets/__init__.py +23 -0
  123. capsule_engine/guards/wickets/catalog.py +75 -0
  124. capsule_engine/guards/wickets/catalog_defs/action_class_gate.v1.1.yaml +38 -0
  125. capsule_engine/guards/wickets/catalog_defs/action_class_gate.v1.2.yaml +52 -0
  126. capsule_engine/guards/wickets/catalog_defs/action_class_gate.yaml +34 -0
  127. capsule_engine/guards/wickets/catalog_defs/caps.v2.yaml +30 -0
  128. capsule_engine/guards/wickets/catalog_defs/caps.v3.yaml +49 -0
  129. capsule_engine/guards/wickets/catalog_defs/caps.v4.yaml +45 -0
  130. capsule_engine/guards/wickets/catalog_defs/caps.v5.1.yaml +53 -0
  131. capsule_engine/guards/wickets/catalog_defs/caps.v5.yaml +43 -0
  132. capsule_engine/guards/wickets/catalog_defs/caps.yaml +17 -0
  133. capsule_engine/guards/wickets/catalog_defs/caps_holds.yaml +18 -0
  134. capsule_engine/guards/wickets/catalog_defs/channel_change.v1.1.yaml +18 -0
  135. capsule_engine/guards/wickets/catalog_defs/channel_change.yaml +10 -0
  136. capsule_engine/guards/wickets/catalog_defs/counterparty_identity_change.yaml +9 -0
  137. capsule_engine/guards/wickets/catalog_defs/counterparty_list.yaml +20 -0
  138. capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.v2.yaml +15 -0
  139. capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.v3.yaml +23 -0
  140. capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.v4.yaml +26 -0
  141. capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.yaml +14 -0
  142. capsule_engine/guards/wickets/catalog_defs/credential_pattern.yaml +14 -0
  143. capsule_engine/guards/wickets/catalog_defs/dedupe.yaml +19 -0
  144. capsule_engine/guards/wickets/catalog_defs/destination_rail.seller.v1.1.yaml +32 -0
  145. capsule_engine/guards/wickets/catalog_defs/destination_rail.seller.yaml +10 -0
  146. capsule_engine/guards/wickets/catalog_defs/destination_rail.yaml +9 -0
  147. capsule_engine/guards/wickets/catalog_defs/hold_reconcile.yaml +14 -0
  148. capsule_engine/guards/wickets/catalog_defs/material_fields_changed.v1.1.yaml +22 -0
  149. capsule_engine/guards/wickets/catalog_defs/material_fields_changed.yaml +14 -0
  150. capsule_engine/guards/wickets/catalog_defs/offer_expiry.v1.1.yaml +31 -0
  151. capsule_engine/guards/wickets/catalog_defs/offer_expiry.yaml +24 -0
  152. capsule_engine/guards/wickets/catalog_defs/offer_fields_changed.v1.1.yaml +22 -0
  153. capsule_engine/guards/wickets/catalog_defs/offer_fields_changed.yaml +10 -0
  154. capsule_engine/guards/wickets/catalog_defs/price_floor.yaml +32 -0
  155. capsule_engine/guards/wickets/catalog_defs/promise_never.v1.1.yaml +64 -0
  156. capsule_engine/guards/wickets/catalog_defs/promise_never.yaml +16 -0
  157. capsule_engine/guards/wickets/catalog_defs/promise_requires_approval.v1.1.yaml +26 -0
  158. capsule_engine/guards/wickets/catalog_defs/promise_requires_approval.yaml +21 -0
  159. capsule_engine/guards/wickets/catalog_defs/recipient_role.seller.v1.1.yaml +23 -0
  160. capsule_engine/guards/wickets/catalog_defs/recipient_role.seller.v1.2.yaml +24 -0
  161. capsule_engine/guards/wickets/catalog_defs/recipient_role.seller.yaml +10 -0
  162. capsule_engine/guards/wickets/catalog_defs/recipient_role.v1.1.yaml +26 -0
  163. capsule_engine/guards/wickets/catalog_defs/recipient_role.yaml +11 -0
  164. capsule_engine/guards/wickets/catalog_defs/recipient_seen_before.yaml +16 -0
  165. capsule_engine/guards/wickets/catalog_defs/recurring_charge.yaml +9 -0
  166. capsule_engine/guards/wickets/catalog_defs/refundability.v1.1.yaml +15 -0
  167. capsule_engine/guards/wickets/catalog_defs/refundability.yaml +7 -0
  168. capsule_engine/guards/wickets/catalog_defs/release_on_acceptance.seller.v1.1.yaml +44 -0
  169. capsule_engine/guards/wickets/catalog_defs/release_on_acceptance.seller.v1.2.yaml +63 -0
  170. capsule_engine/guards/wickets/catalog_defs/release_on_acceptance.seller.yaml +36 -0
  171. capsule_engine/guards/wickets/catalog_defs/required_disclosure.v1.1.yaml +36 -0
  172. capsule_engine/guards/wickets/catalog_defs/required_disclosure.v1.2.yaml +42 -0
  173. capsule_engine/guards/wickets/catalog_defs/required_disclosure.yaml +25 -0
  174. capsule_engine/guards/wickets/catalog_defs/single_commitment.seller.yaml +14 -0
  175. capsule_engine/guards/wickets/catalog_defs/task_authority.v1.1.yaml +20 -0
  176. capsule_engine/guards/wickets/catalog_defs/task_authority.yaml +8 -0
  177. capsule_engine/guards/wickets/catalog_defs/upfront_amount.v1.1.yaml +19 -0
  178. capsule_engine/guards/wickets/catalog_defs/upfront_amount.yaml +11 -0
  179. capsule_engine/guards/wickets/catalog_defs/verify_before_dispatch.yaml +9 -0
  180. capsule_engine/guards/wickets/definition.py +164 -0
  181. capsule_engine/guards/wickets/errors.py +18 -0
  182. capsule_engine/guards/wickets/loader.py +26 -0
  183. capsule_engine/guards/wickets/retired.py +62 -0
  184. capsule_engine/holds/__init__.py +35 -0
  185. capsule_engine/holds/capsules.py +266 -0
  186. capsule_engine/holds/engine.py +476 -0
  187. capsule_engine/holds/errors.py +29 -0
  188. capsule_engine/holds/policy.py +62 -0
  189. capsule_engine/holds/scope.py +47 -0
  190. capsule_engine/mcp/__init__.py +16 -0
  191. capsule_engine/mcp/config.py +117 -0
  192. capsule_engine/mcp/server.py +338 -0
  193. capsule_engine/mcp/tools.py +424 -0
  194. capsule_engine/packaging.py +60 -0
  195. capsule_engine/packs/__init__.py +45 -0
  196. capsule_engine/packs/backfill_coverage.py +238 -0
  197. capsule_engine/packs/catalog/airline-engagement/AI-BOOTSTRAP.md +53 -0
  198. capsule_engine/packs/catalog/airline-engagement/folds/session_close.yaml +8 -0
  199. capsule_engine/packs/catalog/airline-engagement/pack.yaml +146 -0
  200. capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/good_execution.json +36 -0
  201. capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/oversight_declared.json +36 -0
  202. capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/tampered_execution.json +36 -0
  203. capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/trusted_issuer.jwk.json +5 -0
  204. capsule_engine/packs/catalog/eu-ai-act/obligations-pack-note.md +68 -0
  205. capsule_engine/packs/catalog/eu-ai-act/obligations-register.yaml +761 -0
  206. capsule_engine/packs/catalog/eu-ai-act/pack.yaml +88 -0
  207. capsule_engine/packs/catalog/eu-ai-act/register.yaml +96 -0
  208. capsule_engine/packs/catalog/eu-ai-act-deterministic/fixtures/synthetic_internal_assist_sample.jsonl +6 -0
  209. capsule_engine/packs/catalog/eu-ai-act-deterministic/pack.yaml +580 -0
  210. capsule_engine/packs/catalog/everyday/AI-BOOTSTRAP.md +194 -0
  211. capsule_engine/packs/catalog/everyday/fixtures/mini_ledger.jsonl +45 -0
  212. capsule_engine/packs/catalog/everyday/pack.yaml +480 -0
  213. capsule_engine/packs/catalog/payments-safety/AI-BOOTSTRAP.md +83 -0
  214. capsule_engine/packs/catalog/payments-safety/fixtures/mini_ledger.jsonl +8 -0
  215. capsule_engine/packs/catalog/payments-safety/folds/spend_weekly.yaml +29 -0
  216. capsule_engine/packs/catalog/payments-safety/pack.yaml +112 -0
  217. capsule_engine/packs/catalog/seller/fixtures/mini_ledger.jsonl +83 -0
  218. capsule_engine/packs/catalog/seller/pack.yaml +503 -0
  219. capsule_engine/packs/catalog/standard-vendor/AI-BOOTSTRAP.md +70 -0
  220. capsule_engine/packs/catalog/standard-vendor/folds/session_close.yaml +8 -0
  221. capsule_engine/packs/catalog/standard-vendor/pack.yaml +466 -0
  222. capsule_engine/packs/contract_diff.py +809 -0
  223. capsule_engine/packs/contract_validate.py +185 -0
  224. capsule_engine/packs/corpus_verify.py +126 -0
  225. capsule_engine/packs/enforce.py +81 -0
  226. capsule_engine/packs/errors.py +159 -0
  227. capsule_engine/packs/eu_ai_act_trace_field_map.py +46 -0
  228. capsule_engine/packs/install.py +266 -0
  229. capsule_engine/packs/loader.py +1462 -0
  230. capsule_engine/packs/measurability_report.py +280 -0
  231. capsule_engine/packs/obligation_results.py +72 -0
  232. capsule_engine/packs/pins.py +92 -0
  233. capsule_engine/packs/schema.py +1053 -0
  234. capsule_engine/packs/thresholds.py +146 -0
  235. capsule_engine/packs/trace_attainment.py +188 -0
  236. capsule_engine/packs/trace_reader.py +104 -0
  237. capsule_engine/policy/__init__.py +37 -0
  238. capsule_engine/policy/activation.py +87 -0
  239. capsule_engine/policy/catalog_defs/default.yaml +23 -0
  240. capsule_engine/policy/catalog_defs/holds.yaml +27 -0
  241. capsule_engine/policy/errors.py +46 -0
  242. capsule_engine/policy/limits.py +287 -0
  243. capsule_engine/policy/loader.py +26 -0
  244. capsule_engine/policy/manifest.py +289 -0
  245. capsule_engine/policy/profile.py +281 -0
  246. capsule_engine/policy/resolve.py +224 -0
  247. capsule_engine/register/__init__.py +39 -0
  248. capsule_engine/register/compiler.py +219 -0
  249. capsule_engine/register/errors.py +40 -0
  250. capsule_engine/register/loader.py +247 -0
  251. capsule_engine/register/schema.py +129 -0
  252. capsule_engine/registry/__init__.py +19 -0
  253. capsule_engine/registry/conventions.json +162 -0
  254. capsule_engine/registry/conventions.py +188 -0
  255. capsule_engine/registry/cpb_registry.json +78 -0
  256. capsule_engine/report/__init__.py +62 -0
  257. capsule_engine/report/build.py +267 -0
  258. capsule_engine/report/coverage.py +777 -0
  259. capsule_engine/report/errors.py +35 -0
  260. capsule_engine/report/live_check.py +126 -0
  261. capsule_engine/report/live_history.py +420 -0
  262. capsule_engine/report/model.py +95 -0
  263. capsule_engine/report/render.py +467 -0
  264. capsule_engine/report/replay.py +1677 -0
  265. capsule_engine/report/result.py +504 -0
  266. capsule_engine/report/result_from_folds.py +222 -0
  267. capsule_engine/report/result_from_layers.py +670 -0
  268. capsule_engine/report/sale_bundle.py +864 -0
  269. capsule_engine/report/static/verify.js +451 -0
  270. capsule_engine/schemas/evidence-contract-v0.json +383 -0
  271. capsule_engine/schemas/vendor/epistemic-types.json +21 -0
  272. capsule_engine/schemas/vendor/evidence-result-v0.json +463 -0
  273. capsule_engine/telemetry/__init__.py +9 -0
  274. capsule_engine/telemetry/consent.py +39 -0
  275. capsule_engine/telemetry/events.py +93 -0
  276. capsule_engine/telemetry/fixtures/synthetic_events.jsonl +45 -0
  277. capsule_engine/telemetry/funnel.py +147 -0
  278. capsule_engine/telemetry/record.py +106 -0
  279. capsule_engine/telemetry/sink.py +61 -0
  280. capsule_engine/telemetry/state.py +60 -0
  281. capsule_engine/tenants.py +327 -0
  282. capsule_engine-0.1.0.dist-info/METADATA +47 -0
  283. capsule_engine-0.1.0.dist-info/RECORD +288 -0
  284. capsule_engine-0.1.0.dist-info/WHEEL +5 -0
  285. capsule_engine-0.1.0.dist-info/entry_points.txt +2 -0
  286. capsule_engine-0.1.0.dist-info/licenses/LICENSE +202 -0
  287. capsule_engine-0.1.0.dist-info/licenses/capsule_engine/examples/data/tau2_airline/LICENSE-tau2-bench +21 -0
  288. capsule_engine-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,45 @@
1
+ {"metric": "install_seen", "arm": "guards-only", "install_id": "synthetic-g1", "value": true, "emitted_at": "2026-08-05T00:00:00Z"}
2
+ {"metric": "install_seen", "arm": "guards-only", "install_id": "synthetic-g2", "value": true, "emitted_at": "2026-08-05T00:00:01Z"}
3
+ {"metric": "install_seen", "arm": "guards-only", "install_id": "synthetic-g3", "value": true, "emitted_at": "2026-08-05T00:00:02Z"}
4
+ {"metric": "install_seen", "arm": "guards-only", "install_id": "synthetic-g4", "value": true, "emitted_at": "2026-08-05T00:00:03Z"}
5
+ {"metric": "install_seen", "arm": "guards-only", "install_id": "synthetic-g5", "value": true, "emitted_at": "2026-08-05T00:00:04Z"}
6
+ {"metric": "install_seen", "arm": "guards-only", "install_id": "synthetic-g6", "value": true, "emitted_at": "2026-08-05T00:00:05Z"}
7
+ {"metric": "install_seen", "arm": "guards-only", "install_id": "synthetic-g7", "value": true, "emitted_at": "2026-08-05T00:00:06Z"}
8
+ {"metric": "install_seen", "arm": "guards-only", "install_id": "synthetic-g8", "value": true, "emitted_at": "2026-08-05T00:00:07Z"}
9
+ {"metric": "install_seen", "arm": "guards-only", "install_id": "synthetic-g9", "value": true, "emitted_at": "2026-08-05T00:00:08Z"}
10
+ {"metric": "install_seen", "arm": "guards-only", "install_id": "synthetic-g10", "value": true, "emitted_at": "2026-08-05T00:00:09Z"}
11
+ {"metric": "m1_activation", "arm": "guards-only", "install_id": "synthetic-g1", "value": true, "emitted_at": "2026-08-05T04:00:00Z"}
12
+ {"metric": "m1_activation", "arm": "guards-only", "install_id": "synthetic-g2", "value": true, "emitted_at": "2026-08-05T04:00:01Z"}
13
+ {"metric": "m1_activation", "arm": "guards-only", "install_id": "synthetic-g3", "value": true, "emitted_at": "2026-08-05T04:00:02Z"}
14
+ {"metric": "m1_activation", "arm": "guards-only", "install_id": "synthetic-g4", "value": true, "emitted_at": "2026-08-05T04:00:03Z"}
15
+ {"metric": "m2_enforcement_on", "arm": "guards-only", "install_id": "synthetic-g1", "value": true, "emitted_at": "2026-08-12T00:00:00Z"}
16
+ {"metric": "m2_enforcement_on", "arm": "guards-only", "install_id": "synthetic-g2", "value": true, "emitted_at": "2026-08-12T00:00:01Z"}
17
+ {"metric": "m3_day14_alive", "arm": "guards-only", "install_id": "synthetic-g1", "value": true, "emitted_at": "2026-08-18T00:00:00Z"}
18
+ {"metric": "m3_day14_alive", "arm": "guards-only", "install_id": "synthetic-g2", "value": true, "emitted_at": "2026-08-18T00:00:01Z"}
19
+ {"metric": "m3_day14_alive", "arm": "guards-only", "install_id": "synthetic-g3", "value": true, "emitted_at": "2026-08-18T00:00:02Z"}
20
+ {"metric": "install_seen", "arm": "full", "install_id": "synthetic-f1", "value": true, "emitted_at": "2026-08-05T00:00:00Z"}
21
+ {"metric": "install_seen", "arm": "full", "install_id": "synthetic-f2", "value": true, "emitted_at": "2026-08-05T00:00:01Z"}
22
+ {"metric": "install_seen", "arm": "full", "install_id": "synthetic-f3", "value": true, "emitted_at": "2026-08-05T00:00:02Z"}
23
+ {"metric": "install_seen", "arm": "full", "install_id": "synthetic-f4", "value": true, "emitted_at": "2026-08-05T00:00:03Z"}
24
+ {"metric": "install_seen", "arm": "full", "install_id": "synthetic-f5", "value": true, "emitted_at": "2026-08-05T00:00:04Z"}
25
+ {"metric": "install_seen", "arm": "full", "install_id": "synthetic-f6", "value": true, "emitted_at": "2026-08-05T00:00:05Z"}
26
+ {"metric": "install_seen", "arm": "full", "install_id": "synthetic-f7", "value": true, "emitted_at": "2026-08-05T00:00:06Z"}
27
+ {"metric": "install_seen", "arm": "full", "install_id": "synthetic-f8", "value": true, "emitted_at": "2026-08-05T00:00:07Z"}
28
+ {"metric": "install_seen", "arm": "full", "install_id": "synthetic-f9", "value": true, "emitted_at": "2026-08-05T00:00:08Z"}
29
+ {"metric": "install_seen", "arm": "full", "install_id": "synthetic-f10", "value": true, "emitted_at": "2026-08-05T00:00:09Z"}
30
+ {"metric": "m1_activation", "arm": "full", "install_id": "synthetic-f1", "value": true, "emitted_at": "2026-08-05T04:00:00Z"}
31
+ {"metric": "m1_activation", "arm": "full", "install_id": "synthetic-f2", "value": true, "emitted_at": "2026-08-05T04:00:01Z"}
32
+ {"metric": "m1_activation", "arm": "full", "install_id": "synthetic-f3", "value": true, "emitted_at": "2026-08-05T04:00:02Z"}
33
+ {"metric": "m1_activation", "arm": "full", "install_id": "synthetic-f4", "value": true, "emitted_at": "2026-08-05T04:00:03Z"}
34
+ {"metric": "m1_activation", "arm": "full", "install_id": "synthetic-f5", "value": true, "emitted_at": "2026-08-05T04:00:04Z"}
35
+ {"metric": "m2_enforcement_on", "arm": "full", "install_id": "synthetic-f1", "value": true, "emitted_at": "2026-08-12T00:00:00Z"}
36
+ {"metric": "m3_day14_alive", "arm": "full", "install_id": "synthetic-f1", "value": true, "emitted_at": "2026-08-18T00:00:00Z"}
37
+ {"metric": "m3_day14_alive", "arm": "full", "install_id": "synthetic-f2", "value": true, "emitted_at": "2026-08-18T00:00:01Z"}
38
+ {"metric": "m5_evidence_pull", "arm": "full", "install_id": "synthetic-f1", "value": true, "emitted_at": "2026-08-06T00:00:00Z"}
39
+ {"metric": "m5_evidence_pull", "arm": "full", "install_id": "synthetic-f3", "value": true, "emitted_at": "2026-08-06T00:00:01Z"}
40
+ {"metric": "m6_report_opened", "report_token": "synthetic-tok-A", "emitted_at": "2026-08-06T00:00:00Z"}
41
+ {"metric": "m6_report_opened", "report_token": "synthetic-tok-A", "emitted_at": "2026-08-06T01:00:00Z"}
42
+ {"metric": "m6_report_opened", "report_token": "synthetic-tok-A", "emitted_at": "2026-08-06T02:00:00Z"}
43
+ {"metric": "m6_report_opened", "report_token": "synthetic-tok-B", "emitted_at": "2026-08-07T00:00:00Z"}
44
+ {"metric": "m6_report_opened", "report_token": "synthetic-tok-B", "emitted_at": "2026-08-07T05:00:00Z"}
45
+ {"metric": "m6_report_opened", "report_token": "synthetic-tok-C", "emitted_at": "2026-08-08T00:00:00Z"}
@@ -0,0 +1,147 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The 6-metric funnel report: a reporting tool over already-collected
3
+ telemetry events, not the collection itself. Renders exactly the six
4
+ metrics this package's instrumentation emits, as raw counts/rates -- and
5
+ nothing else. No pass/worry/fail banding, no thresholds, no verdict:
6
+ turning a rate into a judgment is a decision made elsewhere, later,
7
+ against a table this repository does not carry. See ``events.py``'s module
8
+ docstring for the same point made about the events themselves.
9
+
10
+ Two arms are always both present in the output (even a synthetic run with
11
+ data for only one arm shows the other at 0/0) because the report's own
12
+ shape -- what gets rendered -- must not depend on which arm happens to have
13
+ data; only the numbers may vary.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from collections import defaultdict
18
+ from collections.abc import Iterable
19
+ from dataclasses import dataclass
20
+
21
+ from ..packaging import FULL, GUARDS_ONLY
22
+ from .events import ALLOWED_FIELDS
23
+
24
+ ARMS = (GUARDS_ONLY, FULL)
25
+
26
+ __all__ = ["ArmMetric", "FunnelReport", "compute_funnel", "render_funnel_report"]
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class ArmMetric:
31
+ """A raw count out of a raw count. ``rate`` is arithmetic only --
32
+ ``None`` (not 0) when the denominator is 0, so a report never implies
33
+ "0%" for a metric that simply has no data yet."""
34
+
35
+ numerator: int
36
+ denominator: int
37
+
38
+ @property
39
+ def rate(self) -> float | None:
40
+ return (self.numerator / self.denominator) if self.denominator else None
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class FunnelReport:
45
+ m1_activation: dict[str, ArmMetric]
46
+ m2_enforcement_on: dict[str, ArmMetric]
47
+ m3_day14_alive: dict[str, ArmMetric]
48
+ m4_evidence_tax: float | None
49
+ m5_evidence_pull: ArmMetric
50
+ m6_viral_unit: int
51
+
52
+
53
+ def _validate(raw: dict) -> None:
54
+ """Same schema check ``sink.emit`` applies on the way out -- re-applied
55
+ on the way in, so a funnel run over an untrusted/hand-edited events file
56
+ still cannot smuggle ledger-shaped content into the report."""
57
+ if set(raw) - {"report_token"} - ALLOWED_FIELDS:
58
+ raise ValueError(f"telemetry event carries unexpected fields: {set(raw) - ALLOWED_FIELDS - {'report_token'}}")
59
+
60
+
61
+ def compute_funnel(raw_events: Iterable[dict]) -> FunnelReport:
62
+ installs_seen: dict[str, set[str]] = defaultdict(set) # arm -> install_ids
63
+ activated: dict[str, set[str]] = defaultdict(set)
64
+ enforced: dict[str, set[str]] = defaultdict(set)
65
+ alive: dict[str, set[str]] = defaultdict(set)
66
+ evidence_pulled: set[str] = set()
67
+ report_opens: dict[str, int] = defaultdict(int)
68
+
69
+ for raw in raw_events:
70
+ _validate(raw)
71
+ metric = raw.get("metric")
72
+ arm = raw.get("arm")
73
+ install_id = raw.get("install_id")
74
+ value = raw.get("value")
75
+
76
+ if metric == "m6_report_opened":
77
+ token = raw.get("report_token")
78
+ if token:
79
+ report_opens[token] += 1
80
+ continue
81
+
82
+ if arm not in ARMS or not install_id:
83
+ continue
84
+ if metric == "install_seen":
85
+ installs_seen[arm].add(install_id)
86
+ elif metric == "m1_activation" and value:
87
+ activated[arm].add(install_id)
88
+ elif metric == "m2_enforcement_on" and value:
89
+ enforced[arm].add(install_id)
90
+ elif metric == "m3_day14_alive" and value:
91
+ alive[arm].add(install_id)
92
+ elif metric == "m5_evidence_pull" and value:
93
+ evidence_pulled.add(install_id)
94
+
95
+ m1 = {arm: ArmMetric(len(activated[arm]), len(installs_seen[arm])) for arm in ARMS}
96
+ m2 = {arm: ArmMetric(len(enforced[arm]), len(activated[arm])) for arm in ARMS}
97
+ m3 = {arm: ArmMetric(len(alive[arm]), len(activated[arm])) for arm in ARMS}
98
+
99
+ guards_only_rate = m1["guards-only"].rate
100
+ full_rate = m1["full"].rate
101
+ m4 = (full_rate / guards_only_rate) if guards_only_rate else None
102
+
103
+ m5 = ArmMetric(len(evidence_pulled & activated["full"]), len(activated["full"]))
104
+
105
+ m6 = sum(1 for count in report_opens.values() if count >= 2)
106
+
107
+ return FunnelReport(
108
+ m1_activation=m1,
109
+ m2_enforcement_on=m2,
110
+ m3_day14_alive=m3,
111
+ m4_evidence_tax=m4,
112
+ m5_evidence_pull=m5,
113
+ m6_viral_unit=m6,
114
+ )
115
+
116
+
117
+ def _fmt_rate(metric: ArmMetric) -> str:
118
+ if metric.rate is None:
119
+ return f"n/a ({metric.numerator}/{metric.denominator})"
120
+ return f"{metric.rate:.1%} ({metric.numerator}/{metric.denominator})"
121
+
122
+
123
+ def render_funnel_report(report: FunnelReport) -> str:
124
+ """Plain text: the six metrics, and nothing else."""
125
+ lines = ["capsule-ledger two-arm funnel report", "=" * 33, ""]
126
+
127
+ def arm_line(label: str, metrics: dict[str, ArmMetric]) -> None:
128
+ lines.append(label)
129
+ for arm in ARMS:
130
+ lines.append(f" {arm:<11} {_fmt_rate(metrics[arm])}")
131
+
132
+ arm_line("M1 activation (>=1 guard configured near install)", report.m1_activation)
133
+ lines.append("")
134
+ arm_line("M2 enforcement-on (dry_run -> enforce, of activated)", report.m2_enforcement_on)
135
+ lines.append("")
136
+ arm_line("M3 day-14-alive (guard evaluated recently, of activated)", report.m3_day14_alive)
137
+ lines.append("")
138
+ lines.append("M4 evidence tax (full M1 rate / guards-only M1 rate)")
139
+ lines.append(f" {report.m4_evidence_tax:.2f}" if report.m4_evidence_tax is not None else " n/a")
140
+ lines.append("")
141
+ lines.append("M5 evidence-pull (full arm only, of activated)")
142
+ lines.append(f" {_fmt_rate(report.m5_evidence_pull)}")
143
+ lines.append("")
144
+ lines.append("M6 viral unit (share link opened by a distinct second party)")
145
+ lines.append(f" {report.m6_viral_unit} distinct instance(s)")
146
+ lines.append("")
147
+ return "\n".join(lines)
@@ -0,0 +1,106 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """CLI-facing hooks: update local state idempotently, emit the corresponding
3
+ metric event -- all of it, including the local state file itself, gated on
4
+ ``consent.is_opted_in()``. An install that never opts in leaves no
5
+ telemetry footprint at all: no state file, no local bookkeeping, nothing.
6
+
7
+ Each function only ever fires its event the first time the underlying fact
8
+ becomes true for this install (a guard is "configured" once; re-running
9
+ ``guard dry-run --cap ...`` a second time updates nothing and emits
10
+ nothing new) -- these are one-shot facts, matching what ``events.py``'s
11
+ docstring says M1/M2/M5 are. ``record_guard_evaluated`` re-affirms
12
+ "alive" on every real evaluation (the state timestamp always advances) but
13
+ is still summarized centrally as a single per-install yes/no fact (M3), not
14
+ a per-evaluation count. ``record_install_seen`` is not one of the six
15
+ metrics -- it is the denominator bookkeeping (this install exists, in this
16
+ arm) the central funnel report needs to turn the six one-shot facts into
17
+ rates; see ``funnel.py``.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from datetime import datetime, timezone
22
+
23
+ from . import events
24
+ from .consent import is_opted_in
25
+ from .sink import EventSink, emit
26
+ from .state import TelemetryState, load_state, save_state
27
+
28
+ __all__ = [
29
+ "record_install_seen",
30
+ "record_guard_configured",
31
+ "record_enforcement_flip",
32
+ "record_guard_evaluated",
33
+ "record_evidence_touch",
34
+ ]
35
+
36
+ _INSTALL_SEEN_METRIC = "install_seen"
37
+
38
+
39
+ def _now_utc() -> str:
40
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
41
+
42
+
43
+ def _emit_bookkeeping(install_id: str, arm: str, *, sink: EventSink | None) -> None:
44
+ """Not one of the six metrics -- the denominator fact an install exists
45
+ in a given arm, so the funnel report can turn one-shot facts into rates
46
+ without needing an external cohort list."""
47
+ emit(
48
+ events.MetricEvent(metric=_INSTALL_SEEN_METRIC, arm=arm, install_id=install_id, value=True, emitted_at=_now_utc()),
49
+ opted_in=True,
50
+ sink=sink,
51
+ )
52
+
53
+
54
+ def record_install_seen(arm: str, *, sink: EventSink | None = None) -> None:
55
+ if not is_opted_in():
56
+ return
57
+ s = load_state()
58
+ _emit_bookkeeping(s.install_id, arm, sink=sink)
59
+
60
+
61
+ def record_guard_configured(arm: str, *, sink: EventSink | None = None, state: TelemetryState | None = None) -> None:
62
+ if not is_opted_in():
63
+ return
64
+ s = state or load_state()
65
+ if s.first_guard_configured_at is not None:
66
+ return
67
+ s.first_guard_configured_at = _now_utc()
68
+ save_state(s)
69
+ emit(events.m1_activation_event(install_id=s.install_id, arm=arm), opted_in=True, sink=sink)
70
+
71
+
72
+ def record_enforcement_flip(arm: str, *, sink: EventSink | None = None, state: TelemetryState | None = None) -> None:
73
+ if not is_opted_in():
74
+ return
75
+ s = state or load_state()
76
+ if s.enforce_flipped_at is not None:
77
+ return
78
+ s.enforce_flipped_at = _now_utc()
79
+ save_state(s)
80
+ emit(events.m2_enforcement_on_event(install_id=s.install_id, arm=arm), opted_in=True, sink=sink)
81
+
82
+
83
+ def record_guard_evaluated(arm: str, *, sink: EventSink | None = None, state: TelemetryState | None = None) -> None:
84
+ if not is_opted_in():
85
+ return
86
+ s = state or load_state()
87
+ already_alive = s.last_guard_evaluated_at is not None
88
+ s.last_guard_evaluated_at = _now_utc()
89
+ save_state(s)
90
+ if not already_alive:
91
+ emit(events.m3_day14_alive_event(install_id=s.install_id, arm=arm), opted_in=True, sink=sink)
92
+
93
+
94
+ def record_evidence_touch(arm: str, *, sink: EventSink | None = None, state: TelemetryState | None = None) -> None:
95
+ """M5, full arm only -- callers already gate this on the arm (the
96
+ evidence-surfacing commands are only registered in the full arm), but
97
+ this function re-asserts it so a future caller can't emit a
98
+ meaningless guards-only M5 fact by mistake."""
99
+ if arm != "full" or not is_opted_in():
100
+ return
101
+ s = state or load_state()
102
+ if s.evidence_touched_at is not None:
103
+ return
104
+ s.evidence_touched_at = _now_utc()
105
+ save_state(s)
106
+ emit(events.m5_evidence_pull_event(install_id=s.install_id, arm=arm), opted_in=True, sink=sink)
@@ -0,0 +1,61 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Where an opted-in event goes. No sink here ever makes a network call --
3
+ this package ships no telemetry backend and points at none by default; a
4
+ real transport is a deployment-time concern for whoever operates one,
5
+ wired in by passing a different ``EventSink``, never by editing this file.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Protocol
12
+
13
+ from ..envcompat import env_get
14
+ from .events import ALLOWED_FIELDS, MetricEvent
15
+
16
+ __all__ = ["EventSink", "NullSink", "LocalJSONLSink", "default_sink", "emit"]
17
+
18
+
19
+ class EventSink(Protocol):
20
+ def write(self, event: MetricEvent) -> None: ...
21
+
22
+
23
+ class NullSink:
24
+ """The default: opted-in or not, nothing leaves this process."""
25
+
26
+ def write(self, event: MetricEvent) -> None:
27
+ return None
28
+
29
+
30
+ class LocalJSONLSink:
31
+ """Append-only local file, one JSON object per line. Intended for
32
+ operators who want to inspect or forward their own install's telemetry
33
+ themselves -- this package never reads this file back or transmits it."""
34
+
35
+ def __init__(self, path: str | Path) -> None:
36
+ self._path = Path(path)
37
+
38
+ def write(self, event: MetricEvent) -> None:
39
+ self._path.parent.mkdir(parents=True, exist_ok=True)
40
+ with self._path.open("a", encoding="utf-8") as fh:
41
+ fh.write(json.dumps(event.to_dict(), sort_keys=True) + "\n")
42
+
43
+
44
+ def default_sink() -> EventSink:
45
+ path = env_get("CAPSULE_LEDGER_TELEMETRY_SINK_PATH")
46
+ return LocalJSONLSink(path) if path else NullSink()
47
+
48
+
49
+ def emit(event: MetricEvent, *, opted_in: bool, sink: EventSink | None = None) -> bool:
50
+ """Write ``event`` to ``sink`` iff ``opted_in``. Returns whether it was
51
+ written. Re-validates the payload's keys against the fixed schema before
52
+ writing -- belt and suspenders alongside ``MetricEvent`` itself being a
53
+ closed dataclass, so a future edit to this module can't silently widen
54
+ what a payload may carry."""
55
+ if not opted_in:
56
+ return False
57
+ payload = event.to_dict()
58
+ if set(payload) != ALLOWED_FIELDS:
59
+ raise ValueError(f"telemetry payload carries unexpected fields: {set(payload) - ALLOWED_FIELDS}")
60
+ (sink or default_sink()).write(event)
61
+ return True
@@ -0,0 +1,60 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Local, per-install state the six metrics are computed from.
3
+
4
+ Stored as a small JSON file outside the ledger and outside any repo --
5
+ never ledger data, never PII: a random install id and a handful of "first
6
+ time this happened" timestamps. Written regardless of telemetry opt-in (the
7
+ facts are needed locally either way, e.g. for ``capsule telemetry status``);
8
+ whether they are ever *emitted* anywhere is gated separately by
9
+ ``consent.is_opted_in()``.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import uuid
15
+ from dataclasses import asdict, dataclass, fields
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+
19
+ from ..envcompat import env_get
20
+
21
+ __all__ = ["TelemetryState", "state_path", "load_state", "save_state"]
22
+
23
+
24
+ def _utc_now() -> str:
25
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
26
+
27
+
28
+ def state_path() -> Path:
29
+ override = env_get("CAPSULE_LEDGER_STATE_DIR")
30
+ base = Path(override) if override else Path.home() / ".local" / "state" / "capsule-ledger"
31
+ return base / "telemetry_state.json"
32
+
33
+
34
+ @dataclass
35
+ class TelemetryState:
36
+ install_id: str
37
+ install_at: str
38
+ first_guard_configured_at: str | None = None
39
+ enforce_flipped_at: str | None = None
40
+ last_guard_evaluated_at: str | None = None
41
+ evidence_touched_at: str | None = None
42
+
43
+
44
+ def load_state(path: Path | None = None) -> TelemetryState:
45
+ """Load the local state, creating it (a fresh random install id, "now"
46
+ as the install timestamp) on first ever call."""
47
+ p = path or state_path()
48
+ if p.exists():
49
+ raw = json.loads(p.read_text(encoding="utf-8"))
50
+ known = {f.name for f in fields(TelemetryState)}
51
+ return TelemetryState(**{k: v for k, v in raw.items() if k in known})
52
+ state = TelemetryState(install_id=str(uuid.uuid4()), install_at=_utc_now())
53
+ save_state(state, p)
54
+ return state
55
+
56
+
57
+ def save_state(state: TelemetryState, path: Path | None = None) -> None:
58
+ p = path or state_path()
59
+ p.parent.mkdir(parents=True, exist_ok=True)
60
+ p.write_text(json.dumps(asdict(state), indent=2, sort_keys=True), encoding="utf-8")