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,35 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Named-reason errors for Result v0 construction (mirrors
3
+ ``packs/errors.py``'s ``PackDefinitionError`` discipline: a stable reason
4
+ code for tests/tooling, a message that names the field and what was
5
+ expected).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ INVALID_HEX_DIGEST = "invalid_hex_digest"
10
+ INVALID_TIER = "invalid_result_tier"
11
+ INVALID_GRADE = "invalid_result_grade"
12
+ INVALID_SUFFICIENCY = "invalid_sufficiency"
13
+ INVALID_VERDICT = "invalid_verdict"
14
+ SUFFICIENCY_VERDICT_MISMATCH = "sufficiency_verdict_mismatch"
15
+ INVALID_DISCLOSED_STATUS = "invalid_disclosed_status"
16
+ INVALID_EVIDENCE_STATUS = "invalid_evidence_status"
17
+ DISCLOSURE_NOT_LEGAL_FOR_STATUS = "disclosure_not_legal_for_status"
18
+ DUPLICATE_CLAIM_ID = "duplicate_claim_id"
19
+ BUCKET_CLAIM_MISMATCH = "bucket_claim_mismatch"
20
+ COVERAGE_CLAIM_MISMATCH = "coverage_claim_mismatch"
21
+ SCHEMA_VALIDATION_FAILED = "schema_validation_failed"
22
+ INVALID_COVERAGE_REPORT = "invalid_coverage_report"
23
+ INVALID_LAYER_TALLY = "invalid_layer_tally"
24
+ COVERAGE_NOT_COMPUTED = "coverage_not_computed"
25
+
26
+
27
+ class ResultError(ValueError):
28
+ """A Result v0 document, or one of its parts, fails to validate --
29
+ "an untiered claim is a bug here, not a rendering problem".
30
+ Always fail-closed at
31
+ construction time, same discipline as ``PackDefinitionError``."""
32
+
33
+ def __init__(self, reason: str, message: str) -> None:
34
+ self.reason = reason
35
+ super().__init__(f"{reason}: {message}")
@@ -0,0 +1,126 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """One live check, decided the way a capsulectl rules checker decides it.
3
+
4
+ capsulectl hands its rules checker one external-check-input/v0 envelope per
5
+ check. ``decide_check_input`` decides one envelope under one pack: it installs
6
+ the pack in observe mode, builds the engine from the installed manifest as
7
+ ``build_engine`` does (its runnable checks and ask sets, the caps limits read
8
+ from the ledger's activation records, so each limit names its source, and
9
+ the per-action reads), writes the envelope's history into a throwaway ledger
10
+ (``history_ledger``, with the pack's caps fold), and checks the record entry
11
+ (``action_for_check_input``) with what capsulectl passes beside it: the
12
+ task-authority record, the floor opening, and the record entry's
13
+ representing opening. The Go guard decides the same envelope with
14
+ ``DecodeCheckInput`` and ``CheckWithHistory``; the shared conformance vectors
15
+ hold the two to the same decision.
16
+
17
+ The ledger lives in a temporary directory and is gone when the call returns;
18
+ its records are signed by ``signer`` (a fixed local key when none is given),
19
+ and no record leaves the call except as the returned capsules.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import tempfile
24
+ from dataclasses import dataclass
25
+ from pathlib import Path
26
+
27
+ from cll.ledger import LedgerStore
28
+
29
+ from ..guards import Action, GuardDecision, GuardEngine, LocalSigner
30
+ from ..guards.checks import RUNNABLE_CHECKS
31
+ from ..packs import install_pack
32
+ from ..packs.install import engine_ask_sets
33
+ from ..packs.schema import PackDefinition
34
+ from ..policy.limits import read_caps_limits
35
+ from .live_history import HistoryLedger, history_ledger
36
+ from .replay import action_for_check_input
37
+
38
+ __all__ = ["LiveCheck", "decide_check_input", "decide_sale_bundle", "decide_steps", "live_engine"]
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class LiveCheck:
43
+ """A live check's decision, the ledger it was decided on (each record's
44
+ capsule, in ledger order), and what ``history_ledger`` wrote."""
45
+
46
+ decision: GuardDecision
47
+ ledger: list[dict]
48
+ written: HistoryLedger
49
+
50
+
51
+ def _default_signer() -> LocalSigner:
52
+ return LocalSigner(key_id="live-check", secret=b"live-check")
53
+
54
+
55
+ def live_engine(pack: PackDefinition, store: LedgerStore, project_dir: str | Path, *, signer=None) -> GuardEngine:
56
+ """The engine a live check runs over ``store``: ``pack`` installed in
57
+ observe mode under ``project_dir``, wired as ``build_engine`` wires it. A
58
+ live decision never evaluates an action's class under another taxonomy
59
+ than the engine's (``guards/engine.py``); only a replay does."""
60
+ return _wired(install_pack(pack, project_dir=project_dir, mode="observe"), store, signer)
61
+
62
+
63
+ def _wired(installed, store: LedgerStore, signer) -> GuardEngine:
64
+ resolved = installed.resolved
65
+ wickets = resolved.configured_wickets(RUNNABLE_CHECKS)
66
+ gates, asks = engine_ask_sets(installed.pack, wickets)
67
+ signer = signer or _default_signer()
68
+ return GuardEngine(
69
+ ledger=store, caps_fold=resolved.caps_fold(),
70
+ caps_limits=lambda s: read_caps_limits(resolved, store, s),
71
+ per_action_reads=resolved.per_action_reads(), signer_provider=lambda: signer,
72
+ manifest_digest=resolved.manifest_digest, wickets=wickets,
73
+ ask_gate_selectors=gates, ask_wickets=asks,
74
+ )
75
+
76
+
77
+ def decide_check_input(
78
+ pack: PackDefinition, envelope: dict, *, with_history: bool = True, signer=None
79
+ ) -> LiveCheck:
80
+ """The decision on one external-check-input/v0 envelope under ``pack``,
81
+ by a fresh engine whose ledger is the envelope's history, or empty when
82
+ ``with_history`` is false (the live path before history was passed)."""
83
+ with tempfile.TemporaryDirectory() as tmp, LedgerStore(Path(tmp) / "ledger") as store:
84
+ installed = install_pack(pack, project_dir=Path(tmp) / "live-project", mode="observe")
85
+ engine = _wired(installed, store, signer)
86
+ caps_fold = installed.resolved.caps_fold()
87
+ written = history_ledger(envelope, store, caps_fold=caps_fold) if with_history else HistoryLedger(True, 0, 0, 0)
88
+ ledger = [r.capsule for r in store.scan()]
89
+ record = envelope["record"]
90
+ action = action_for_check_input(record, item_ref=envelope.get("item_ref"))
91
+ decision = engine.check(
92
+ action,
93
+ task_authority_record=envelope.get("task_authority_record"),
94
+ commercial_bounds_opening=envelope.get("commercial_bounds_opening"),
95
+ representing_opening=record.get("representing_opening"),
96
+ )
97
+ return LiveCheck(decision=decision, ledger=ledger, written=written)
98
+
99
+
100
+ def decide_steps(pack: PackDefinition, steps: list[dict], *, signer=None) -> list[GuardDecision]:
101
+ """The decisions on a run of actions under ``pack``, each one an
102
+ ``Action``'s fields as JSON (an array is a tuple), decided in order by one
103
+ engine whose ledger starts empty and holds each decision before the next
104
+ step is checked."""
105
+ with tempfile.TemporaryDirectory() as tmp, LedgerStore(Path(tmp) / "ledger") as store:
106
+ engine = _wired(install_pack(pack, project_dir=Path(tmp) / "live-project", mode="observe"), store, signer)
107
+ return [engine.check(Action(**{k: tuple(v) if isinstance(v, list) else v for k, v in step.items()})) for step in steps]
108
+
109
+
110
+ def decide_sale_bundle(pack: PackDefinition, bundle: dict):
111
+ """A sale bundle read and verified (``read_sale_bundle``) and replayed
112
+ under ``pack`` (``replay_sale``), as a replay of a sale decides each of
113
+ its checks: the sale, and the replay's result."""
114
+ from .sale_bundle import read_sale_bundle, replay_sale
115
+
116
+ sale = read_sale_bundle(bundle)
117
+ with tempfile.TemporaryDirectory() as tmp:
118
+ installed = install_pack(pack, project_dir=Path(tmp) / "replay-project", mode="observe")
119
+ resolved = installed.resolved
120
+ result = replay_sale(
121
+ sale, caps_fold=resolved.caps_fold(), caps_minor=resolved.caps_minor(),
122
+ per_action_minor=resolved.per_action_minor(), per_action_reads=resolved.per_action_reads(),
123
+ manifest_digest=resolved.manifest_digest, wickets=resolved.configured_wickets(RUNNABLE_CHECKS),
124
+ pack=installed.pack,
125
+ )
126
+ return sale, result
@@ -0,0 +1,420 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The ledger a live check decides on, built from its check input's history.
3
+
4
+ capsulectl hands its rules checker one external-check-input/v0 envelope per
5
+ check: the ``record`` being checked and, as ``history``, the profile's
6
+ earlier acts from every deal, newest first, each one the sealed capsule of an
7
+ act (an ``x-deal-v0`` action step, or a typed ``action-record/v0``) with its
8
+ disclosed ``agent_input`` and, beside it, the deal's ``item_ref`` when the
9
+ deal is a sale's thread.
10
+ ``history_scope.complete`` says whether every act in the window is there.
11
+
12
+ ``history_ledger`` writes those acts into the ledger the engine reads, once,
13
+ before the check, so every check that reads earlier acts (``dedupe``,
14
+ ``counterparty_seen_before``, ``single_commitment``) reads one ledger built
15
+ one way. Each act is read by ``action_for_history_entry`` and written as the
16
+ record a decision on it would carry (``act_payload``), in ledger order:
17
+ oldest first by the capsule's ``timestamp``, with acts of the same second in
18
+ the order the envelope gives them (capsulectl's sort is stable, so that is
19
+ the order they were sealed in). An act given twice (the same ``capsule_id``)
20
+ is written once. A history entry that is a claim the user's
21
+ agent made to the counterparty is written as the replay writes it
22
+ (``statement_for_history_entry``), in state ``statement``, so ``required_disclosure``
23
+ reads a statement made first in the deal the same way live and in a replay.
24
+ It is not an act, so it is never read as an unread one and never adds to a
25
+ spend window. Only a claim sealed at or before the checked record's
26
+ ``timestamp`` is written, so a claim made after the check never counts for
27
+ it; when the checked record's time cannot be read, no claim is written. A
28
+ claim given twice is written once. The claims capsulectl passes beside the
29
+ checked record as ``deal_claims`` (AMENDMENT 9) are written the same way,
30
+ after the history's records, as ``deal_claim_statements`` reads them.
31
+
32
+ Each record states what is known about it (``guards/history_state.py``),
33
+ and an act read carries, beside ``act_payload``, the counterparty its check
34
+ sealed (``thread_counterparty``), which no decision seals. Its
35
+ ``disposition`` is the one the act's capsule seals, and nothing else: an act
36
+ sealed with none is written as ``no_disposition``, never as accepted and
37
+ never as declined, and no decision is inferred from its ``authority_basis``.
38
+ An entry whose act cannot be read is written as ``unread``, with its
39
+ ``item_ref`` when that is in its agreed shape (64 lowercase hex). When the
40
+ history is missing, is not a list, is not declared complete, or holds an
41
+ entry with no readable time or an ``item_ref`` in another shape, one
42
+ ``incomplete`` record is written after the acts.
43
+
44
+ Under a caps fold that counts executed acts (``spend.weekly/3.1.0``), each act
45
+ is also written as the spend window's record of it (``ExecutedActs``), right
46
+ after it: every history entry is an executed act, counted once. An entry
47
+ whose ``agent_input`` its capsule does not bind, or whose spend cannot be
48
+ read, is written with ``spend_unreadable``, so the window is not evaluated
49
+ rather than counted without it. Under any other fold nothing more is
50
+ written.
51
+
52
+ The capsules are read as given: their signatures are not verified here.
53
+ capsulectl hands the envelope to the user's own checker, and every record
54
+ this writes stays in that check's ledger.
55
+ """
56
+ from __future__ import annotations
57
+
58
+ import re
59
+ from dataclasses import dataclass
60
+ from datetime import datetime
61
+ from typing import TypedDict
62
+
63
+ from agent_action_capsule import json_digest
64
+ from cll.ledger.api import LedgerAPI
65
+
66
+ from ..folds.definition import FoldDefinition
67
+ from ..folds.duration import parse_duration_seconds
68
+ from ..guards.action import Action
69
+ from ..guards.capsule import act_payload
70
+ from ..guards.checks.caps import counts_executed_acts, in_window_count
71
+ from ..guards.classes import ENGINE_TAXONOMY, same_classes
72
+ from ..guards.history_state import (
73
+ DISPOSITION,
74
+ INCOMPLETE,
75
+ LIVE_HISTORY,
76
+ NO_DISPOSITION,
77
+ STATEMENT,
78
+ THREAD_COUNTERPARTY,
79
+ UNREAD,
80
+ WINDOW_HELD,
81
+ ThreadCounterparty,
82
+ )
83
+ from ..guards.statements import StatementRecord, claim_of
84
+ from .replay import (
85
+ MONEY_IN,
86
+ ExecutedActs,
87
+ action_for_history_entry,
88
+ bound_agent_input,
89
+ deal_claim_statements,
90
+ names_two_deals,
91
+ statement_for_history_entry,
92
+ )
93
+
94
+ __all__ = ["HistoryLedger", "history_act_payload", "history_ledger"]
95
+
96
+
97
+ _HEX64 = re.compile(r"^[0-9a-f]{64}$")
98
+
99
+
100
+ class _Disposition(TypedDict):
101
+ decision: str
102
+
103
+
104
+ class _Sealed(TypedDict, total=False):
105
+ disposition: _Disposition
106
+
107
+
108
+ class _HistoryRecord(_Sealed):
109
+ """A record ``history_ledger`` writes for one history entry, with a
110
+ ``disposition`` only when its capsule seals one. Its ``asg_payload`` is
111
+ what a decision on the act seals (``act_payload``) plus
112
+ ``live_history``."""
113
+
114
+ capsule_id: str
115
+ operator: object
116
+ developer: object
117
+ action_type: object
118
+ action_id: object
119
+ timestamp: object
120
+ asg_payload: dict[str, object]
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class HistoryLedger:
125
+ """What ``history_ledger`` wrote: how many records in each state (acts,
126
+ and statements, which are not acts), and whether the history was known to
127
+ be complete."""
128
+
129
+ complete: bool
130
+ disposition: int
131
+ no_disposition: int
132
+ unread: int
133
+ statement: int = 0
134
+
135
+
136
+ def history_ledger(envelope: dict, ledger: LedgerAPI, *, caps_fold: FoldDefinition | None = None) -> HistoryLedger:
137
+ """Write the acts of ``envelope``'s ``history`` into ``ledger``, oldest
138
+ first, and an ``incomplete`` record after them when the history is not
139
+ known to be complete (module docstring). ``caps_fold`` is the engine's;
140
+ when it counts executed acts, each act's spend record follows it."""
141
+ history = envelope.get("history")
142
+ scope = envelope.get("history_scope")
143
+ complete = isinstance(history, list) and isinstance(scope, dict) and scope.get("complete") is True
144
+ dated: list[tuple[datetime, int, dict]] = []
145
+ for index, entry in enumerate(history if isinstance(history, list) else []):
146
+ at = _time(entry.get("timestamp")) if isinstance(entry, dict) else None
147
+ if at is None:
148
+ complete = False
149
+ continue
150
+ if "item_ref" in entry and not _is_item_ref(entry["item_ref"]):
151
+ # Which sale the act belongs to cannot be read.
152
+ complete = False
153
+ dated.append((at, index, entry))
154
+ dated.sort(key=lambda item: (item[0], item[1]))
155
+ checked = envelope.get("record")
156
+ checked_at = _time(checked.get("timestamp")) if isinstance(checked, dict) else None
157
+ counts = {DISPOSITION: 0, NO_DISPOSITION: 0, UNREAD: 0, STATEMENT: 0}
158
+ executed = ExecutedActs([entry for _, _, entry in dated]) if counts_executed_acts(caps_fold) else None
159
+ written: set[str] = set()
160
+ claims: set[str] = set()
161
+ for at, _, entry in dated:
162
+ stated = _statement_record(entry)
163
+ if stated is not None and (checked_at is None or at > checked_at):
164
+ # A claim sealed after the check, or with no check time to hold it to.
165
+ continue
166
+ record = stated or _act_record(entry)
167
+ if record["capsule_id"] in written or (stated is not None and claim_of(stated) in claims):
168
+ # The same act, or claim, given twice: written, and counted, once.
169
+ continue
170
+ written.add(record["capsule_id"])
171
+ if stated is not None:
172
+ claims.add(claim_of(stated))
173
+ counts[str(record["asg_payload"][LIVE_HISTORY])] += 1
174
+ ledger.append(dict(record), consequential=False)
175
+ if executed is not None and stated is None:
176
+ shown = bound_agent_input(entry)
177
+ spend = executed.record(entry, shown, json_digest(shown) if shown is not None else record["capsule_id"],
178
+ every_record_is_an_act=True)
179
+ if spend is not None:
180
+ ledger.append(dict(spend), consequential=False)
181
+ if isinstance(checked, dict):
182
+ for stated in deal_claim_statements(checked).statements:
183
+ if claim_of(stated) in claims:
184
+ # Given in the history too: one claim, one statement.
185
+ continue
186
+ claims.add(claim_of(stated))
187
+ counts[STATEMENT] += 1
188
+ ledger.append(dict(_marked(stated)), consequential=False)
189
+ if not complete:
190
+ ledger.append(_incomplete_record(envelope.get("record")), consequential=False)
191
+ else:
192
+ held = window_held_reason(scope, [entry for _, _, entry in dated], caps_fold, checked)
193
+ if held is not None:
194
+ ledger.append(_window_held_record(envelope.get("record"), held), consequential=False)
195
+ return HistoryLedger(
196
+ complete=complete,
197
+ disposition=counts[DISPOSITION],
198
+ no_disposition=counts[NO_DISPOSITION],
199
+ unread=counts[UNREAD],
200
+ statement=counts[STATEMENT],
201
+ )
202
+
203
+
204
+ def _statement_record(entry: dict) -> StatementRecord | None:
205
+ """The record the replay writes for a history entry that is a claim the
206
+ user's agent made (``statement_for_history_entry``), marked
207
+ ``statement``; ``None`` for any other entry."""
208
+ stated = statement_for_history_entry(entry)
209
+ return _marked(stated) if stated is not None else None
210
+
211
+
212
+ def _marked(stated: StatementRecord) -> StatementRecord:
213
+ """A statement record with its ``live_history`` marker."""
214
+ return StatementRecord(**{**stated, "asg_payload": {**stated["asg_payload"], LIVE_HISTORY: STATEMENT}})
215
+
216
+
217
+ def _time(value: object) -> datetime | None:
218
+ """``value`` as an aware time, when it is an RFC 3339 string with a zone."""
219
+ if not isinstance(value, str):
220
+ return None
221
+ try:
222
+ at = datetime.fromisoformat(value)
223
+ except ValueError:
224
+ # Not a time: the caller reads the history as incomplete.
225
+ return None
226
+ return at if at.tzinfo is not None else None
227
+
228
+
229
+ def _sealed_decision(entry: dict) -> str | None:
230
+ """The ``disposition.decision`` an entry's capsule seals, or ``None``."""
231
+ disposition = entry.get("disposition")
232
+ decision = disposition.get("decision") if isinstance(disposition, dict) else None
233
+ return decision if isinstance(decision, str) and decision else None
234
+
235
+
236
+ def _act_record(entry: dict) -> _HistoryRecord:
237
+ """The record ``history_ledger`` writes for one history entry: its
238
+ capsule's own identity fields, the act as ``act_payload`` carries it, and
239
+ the disposition its capsule seals, when it seals one."""
240
+ action = action_for_history_entry(entry)
241
+ decision = _sealed_decision(entry)
242
+ if action is None:
243
+ payload: dict[str, object] = {LIVE_HISTORY: UNREAD}
244
+ item_ref = entry.get("item_ref")
245
+ if _is_item_ref(item_ref):
246
+ payload["item_ref"] = item_ref
247
+ else:
248
+ payload = history_act_payload(action, sealed_disposition=decision is not None)
249
+ capsule_id = entry.get("capsule_id")
250
+ record = _HistoryRecord(
251
+ capsule_id=capsule_id if isinstance(capsule_id, str) and capsule_id else json_digest(entry),
252
+ operator=entry.get("operator", ""),
253
+ developer=entry.get("developer", ""),
254
+ action_type=entry.get("action_type", ""),
255
+ action_id=entry.get("action_id"),
256
+ timestamp=entry.get("timestamp"),
257
+ asg_payload=payload,
258
+ )
259
+ if decision is not None:
260
+ record["disposition"] = _Disposition(decision=decision)
261
+ return record
262
+
263
+
264
+ def history_act_payload(action: Action, *, sealed_disposition: bool) -> dict[str, object]:
265
+ """The ``asg_payload`` ``history_ledger`` writes for a history act it
266
+ read: what a decision on ``action`` seals about the act (``act_payload``),
267
+ its state (``disposition`` when its capsule seals one), and, beside them,
268
+ the counterparty its check sealed as ``thread_counterparty`` when the
269
+ action carries one, whole (``Action.counterparty_malformed``). That member is this ledger's own state: it is never in
270
+ ``act_payload``, so no decision seals it (``guards/history_state.py``)."""
271
+ payload: dict[str, object] = {**act_payload(action),
272
+ LIVE_HISTORY: DISPOSITION if sealed_disposition else NO_DISPOSITION}
273
+ if action.counterparty_ids and action.counterparty_fp_alg and not action.counterparty_malformed:
274
+ payload[THREAD_COUNTERPARTY] = ThreadCounterparty(fp_alg=action.counterparty_fp_alg,
275
+ ids=dict(action.counterparty_ids))
276
+ return payload
277
+
278
+
279
+ def _incomplete_record(checked: object) -> dict:
280
+ """The one record saying the history may be missing acts, under the
281
+ checked record's operator, at its time."""
282
+ checked = checked if isinstance(checked, dict) else {}
283
+ body = {
284
+ "operator": checked.get("operator", ""),
285
+ "timestamp": checked.get("timestamp"),
286
+ "asg_payload": {LIVE_HISTORY: INCOMPLETE},
287
+ }
288
+ return {**body, "capsule_id": json_digest(body)}
289
+
290
+
291
+ def window_held_reason(scope: dict, entries: list[dict], caps_fold: FoldDefinition | None,
292
+ checked: object) -> str | None:
293
+ """Why a complete history cannot decide the caps window, or None: it
294
+ reaches back fewer days (``history_scope.days``, 0 when absent) than the
295
+ window, some act the window would count carries no recorded decision and
296
+ a spend that cannot be read, or some act names a taxonomy version whose
297
+ classes are not the engine's. A history that holds an act it reaches back
298
+ over is no proof that the acts before its first are absent, so a short one
299
+ is held; an undecided act's spend is not in a total that counts accepted
300
+ acts only, and when it cannot be read the total is not known (Sherlock2,
301
+ ops u504); an act classed under other names cannot be summed against this
302
+ engine's limits. ``checked`` is the record being checked, whose key and
303
+ time place the window. With no caps fold there is no window to hold. The
304
+ Go guard holds the same window with the same reasons, in this order
305
+ (go/guard/engine.go windowCoverage)."""
306
+ if caps_fold is None or caps_fold.window is None or not caps_fold.window.duration:
307
+ return None
308
+ window = caps_fold.window.duration
309
+ days = scope.get("days")
310
+ days = days if isinstance(days, int) and not isinstance(days, bool) else 0
311
+ if days * 86400 < parse_duration_seconds(window):
312
+ return f"the history supplied reaches back {days} day(s); the {window} total needs {window}"
313
+ undecided: list[dict] = []
314
+ other = 0
315
+ for entry in entries:
316
+ if _statement_record(entry) is not None:
317
+ continue
318
+ shown = bound_agent_input(entry)
319
+ if not counts_executed_acts(caps_fold) and _undecided_unreadable(entry, shown):
320
+ undecided.append(entry)
321
+ version = _act_taxonomy_version(shown)
322
+ if version is not None and not same_classes(version):
323
+ other += 1
324
+ held = _in_window(caps_fold, undecided, checked)
325
+ if held:
326
+ return (
327
+ f"{held} earlier act(s) carry no recorded decision; the {window} total as the engine computes it "
328
+ "counts accepted acts only, so it cannot be computed"
329
+ )
330
+ if other:
331
+ return (
332
+ f"{other} earlier act(s) were classed under another taxonomy version than the engine's "
333
+ f"({ENGINE_TAXONOMY.version}), so the {window} total cannot be computed"
334
+ )
335
+ return None
336
+
337
+
338
+ def _undecided_unreadable(entry: dict, shown: dict | None) -> bool:
339
+ """Whether a history act is one the window cannot leave out: checked,
340
+ with no recorded decision, not moving money in, and with a spend that
341
+ cannot be read. As the Go guard reads it (``DecodeHistoryAct``,
342
+ ``windowCoverage``): a decision is any ``disposition.decision`` the
343
+ capsule seals, and an act naming two deals is decided (refused); an act
344
+ taken without a check is counted as spend, and one that moved money in
345
+ never adds to the window; the spend is the act's ``spend_minor``, an
346
+ integer of at least 0. A capsule that does not bind its ``agent_input``
347
+ states no spend that can be read. Under a fold that counts executed acts
348
+ the window's own records decide this instead (``ExecutedActs``), so it
349
+ is not read there."""
350
+ disposition = entry.get("disposition")
351
+ if isinstance(disposition, dict) and disposition.get("decision") is not None:
352
+ return False
353
+ acted, unchecked = _act_member(shown)
354
+ if acted is None:
355
+ return True
356
+ if unchecked or names_two_deals(shown) or acted.get("direction") == MONEY_IN:
357
+ return False
358
+ spend = acted.get("spend_minor")
359
+ return not (isinstance(spend, int) and not isinstance(spend, bool) and spend >= 0)
360
+
361
+
362
+ def _in_window(caps_fold: FoldDefinition, acts: list[dict], checked: object) -> int:
363
+ """How many of ``acts`` the caps window would count, by key and time
364
+ alone: under the checked record's value for the fold's key, in the window
365
+ as of its time (``caps.in_window_count``). When the checked record's time
366
+ cannot be read, every one: caps cannot place the window."""
367
+ if not acts:
368
+ return 0
369
+ checked = checked if isinstance(checked, dict) else {}
370
+ if _time(checked.get("timestamp")) is None:
371
+ return len(acts)
372
+ key_value = checked.get(caps_fold.key, "") if caps_fold.key is not None else None
373
+ placed = [{"timestamp": act.get("timestamp"), "developer": act.get("developer"), "operator": act.get("operator")}
374
+ for act in acts]
375
+ return in_window_count(caps_fold, placed, key_value, checked["timestamp"])
376
+
377
+
378
+ def _act_member(shown: object) -> tuple[dict | None, bool]:
379
+ """The member of a disclosed act that records the act, and whether it
380
+ was taken without a check, as the Go guard reads it
381
+ (``DecodeHistoryAct``): ``body.attempted`` of a typed
382
+ ``action-outcome/v0`` or ``body.unchecked``, else the body itself;
383
+ ``(None, False)`` when there is no body."""
384
+ body = shown.get("body") if isinstance(shown, dict) else None
385
+ if not isinstance(body, dict):
386
+ return None, False
387
+ if shown.get("type") == "action-outcome/v0" and isinstance(body.get("attempted"), dict):
388
+ return body["attempted"], True
389
+ if isinstance(body.get("unchecked"), dict):
390
+ return body["unchecked"], True
391
+ return body, False
392
+
393
+
394
+ def _act_taxonomy_version(shown: object) -> object:
395
+ """The taxonomy version an act names, as the Go guard reads it
396
+ (``DecodeHistoryAct``): an act taken without a check names it in the
397
+ member that records the act (``_act_member``), and the body's own stands
398
+ in when that member names none."""
399
+ acted, unchecked = _act_member(shown)
400
+ if acted is None:
401
+ return None
402
+ version = acted.get("taxonomy_version") if unchecked else None
403
+ return version if version is not None else shown["body"].get("taxonomy_version")
404
+
405
+
406
+ def _window_held_record(checked: object, reason: str) -> dict:
407
+ """The one record saying the caps window cannot be summed over this
408
+ history, why, under the checked record's operator, at its time."""
409
+ checked = checked if isinstance(checked, dict) else {}
410
+ body = {
411
+ "operator": checked.get("operator", ""),
412
+ "timestamp": checked.get("timestamp"),
413
+ "asg_payload": {LIVE_HISTORY: WINDOW_HELD, "held_reason": reason},
414
+ }
415
+ return {**body, "capsule_id": json_digest(body)}
416
+
417
+
418
+ def _is_item_ref(value: object) -> bool:
419
+ """Whether ``value`` is an ``item_ref`` in its agreed shape."""
420
+ return isinstance(value, str) and _HEX64.fullmatch(value) is not None
@@ -0,0 +1,95 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Dry-run report data shapes.
3
+
4
+ These carry only what the design's headline folds / per-guard tables / the
5
+ single "most consequential" callout need. Every field here is either read
6
+ straight off a real ``GuardDecision`` (T3) or computed from one — nothing in
7
+ this module invents a number; see ``build.py`` for where each field comes
8
+ from.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+
14
+ __all__ = ["ReportRow", "GuardSection", "ModelNote", "DryRunReport"]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ReportRow:
19
+ """One would-have-held decision, attributed to the guard whose
20
+ constraint failed it. ``capsule`` is this decision's own sealed capsule
21
+ (dry_run decisions are still built, signed, and appended to the guard's
22
+ own replay-local view — see ``docs/failure-semantics.md``'s "an unsigned
23
+ record is not a record" and ``engine.py``'s dry_run handling). ``cited_capsule``
24
+ is the prior record this row's check matched against (e.g. the earlier
25
+ payment a dedupe hit repeats), when the check produced one — carried in
26
+ full so the browser can re-verify it, not just display its id.
27
+ """
28
+
29
+ when: str
30
+ agent: str
31
+ why: str
32
+ capsule: dict
33
+ amount_minor: int | None = None
34
+ currency: str | None = None
35
+ cited_capsule_id: str | None = None
36
+ cited_capsule: dict | None = None
37
+
38
+ @property
39
+ def fingerprint(self) -> str:
40
+ capsule_id = self.capsule.get("capsule_id", "")
41
+ return capsule_id[:8] + "…" if capsule_id else ""
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class GuardSection:
46
+ guard_id: str
47
+ what: str
48
+ rows: tuple[ReportRow, ...] = field(default_factory=tuple)
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class ModelNote:
53
+ """Optional, operator-supplied model commentary. Never generated by this
54
+ module — see ``build.py``'s docstring. Omitted entirely (``None``) is the
55
+ default and fully supported path."""
56
+
57
+ quote: str
58
+ model_id: str
59
+ record_count: int
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class DryRunReport:
64
+ operator: str
65
+ agents: tuple[str, ...]
66
+ since_label: str | None
67
+ actions_replayed: int
68
+ record_range: tuple[int, int]
69
+ checkpoint: int
70
+ guards: tuple[GuardSection, ...]
71
+ replay_command: str
72
+ generated_at: str
73
+ model_note: ModelNote | None = None
74
+ # The active policy manifest's own digest (``capsule_ledger.policy``), when
75
+ # one was resolved for this replay -- "evaluated under manifest <digest>"
76
+ # (see ``cli/guard_cmds.py``). ``None`` when no manifest was configured.
77
+ manifest_digest: str | None = None
78
+
79
+ @property
80
+ def held_rows(self) -> list[tuple[str, ReportRow]]:
81
+ return [(section.guard_id, row) for section in self.guards for row in section.rows]
82
+
83
+ @property
84
+ def would_have_held_count(self) -> int:
85
+ return len(self.held_rows)
86
+
87
+ def consequential(self) -> tuple[str, ReportRow] | None:
88
+ """The single highest-amount would-have-held row, across all guards.
89
+
90
+ A pick, never a sum -- money in different currencies is never added
91
+ together (see ``value_held_label`` in ``build.py``)."""
92
+ candidates = [(g, r) for g, r in self.held_rows if r.amount_minor is not None]
93
+ if not candidates:
94
+ return self.held_rows[0] if self.held_rows else None
95
+ return max(candidates, key=lambda gr: gr[1].amount_minor)