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.
- capsule_engine/__init__.py +17 -0
- capsule_engine/bundle_viewer/__init__.py +21 -0
- capsule_engine/bundle_viewer/static/offline_shell.html +1509 -0
- capsule_engine/bundle_viewer/viewer.py +57 -0
- capsule_engine/cli/__init__.py +5 -0
- capsule_engine/cli/bundle_cmd.py +269 -0
- capsule_engine/cli/console_cmd.py +70 -0
- capsule_engine/cli/constraints_cmd.py +74 -0
- capsule_engine/cli/format.py +173 -0
- capsule_engine/cli/guard_cmds.py +361 -0
- capsule_engine/cli/init_cmds.py +169 -0
- capsule_engine/cli/main.py +120 -0
- capsule_engine/cli/packs_cmds.py +96 -0
- capsule_engine/cli/telemetry_cmd.py +80 -0
- capsule_engine/cli/tenant_cmds.py +199 -0
- capsule_engine/cli/verify_cmd.py +110 -0
- capsule_engine/compiler/__init__.py +13 -0
- capsule_engine/compiler/effect_model.py +79 -0
- capsule_engine/compiler/vocabulary.py +253 -0
- capsule_engine/console/__init__.py +10 -0
- capsule_engine/console/api.py +251 -0
- capsule_engine/console/components.css +406 -0
- capsule_engine/console/console.css +130 -0
- capsule_engine/console/console.html +51 -0
- capsule_engine/console/console.js +330 -0
- capsule_engine/console/gallery.html +274 -0
- capsule_engine/console/server.py +137 -0
- capsule_engine/console/tokens.css +68 -0
- capsule_engine/conversation/__init__.py +88 -0
- capsule_engine/conversation/capsules.py +268 -0
- capsule_engine/conversation/exchange.py +279 -0
- capsule_engine/conversation/merkle.py +80 -0
- capsule_engine/conversation/session.py +101 -0
- capsule_engine/envcompat.py +15 -0
- capsule_engine/events/__init__.py +14 -0
- capsule_engine/events/capsule.py +133 -0
- capsule_engine/examples/__init__.py +9 -0
- capsule_engine/examples/data/tau2_airline/LICENSE-tau2-bench +21 -0
- capsule_engine/examples/data/tau2_airline/PROVENANCE.json +70 -0
- capsule_engine/examples/data/tau2_airline/hand_labels.json +893 -0
- capsule_engine/examples/data/tau2_airline/pilot1_live_gemini-2-5-flash_airline_shift1.jsonl +190 -0
- capsule_engine/examples/data/tau2_airline/tau2_committed_claude-3-7-sonnet_airline_trial0.jsonl +556 -0
- capsule_engine/examples/data/tau2_airline/tau2_committed_gpt-4-1-mini_airline_trial0.jsonl +480 -0
- capsule_engine/examples/data/tau2_airline/tau2_committed_gpt-4-1_airline_trial0.jsonl +496 -0
- capsule_engine/examples/data/tau2_airline/tau2_committed_o4-mini_airline_trial0.jsonl +392 -0
- capsule_engine/examples/data/tau2_airline/tau2_conversations_claude-3-7-sonnet_airline_4trials.jsonl +200 -0
- capsule_engine/examples/tau2_airline_reference.py +460 -0
- capsule_engine/examples/tau2_conversation_exchange.py +92 -0
- capsule_engine/folds/__init__.py +104 -0
- capsule_engine/folds/account_core.py +89 -0
- capsule_engine/folds/approval_latency.py +86 -0
- capsule_engine/folds/catalog.py +79 -0
- capsule_engine/folds/catalog_defs/actions.count_by_developer.yaml +10 -0
- capsule_engine/folds/catalog_defs/actions.executed_count.yaml +16 -0
- capsule_engine/folds/catalog_defs/actions.last_decision.yaml +12 -0
- capsule_engine/folds/catalog_defs/counterparty.seen_before.v2.yaml +34 -0
- capsule_engine/folds/catalog_defs/counterparty.seen_before.v3.yaml +38 -0
- capsule_engine/folds/catalog_defs/counterparty.seen_before.yaml +28 -0
- capsule_engine/folds/catalog_defs/disclosure.class_seen.yaml +29 -0
- capsule_engine/folds/catalog_defs/disclosure.statement_seen.yaml +32 -0
- capsule_engine/folds/catalog_defs/hold.active_exposure.yaml +44 -0
- capsule_engine/folds/catalog_defs/spend.weekly.v2.yaml +38 -0
- capsule_engine/folds/catalog_defs/spend.weekly.v3.1.yaml +71 -0
- capsule_engine/folds/catalog_defs/spend.weekly.v3.yaml +76 -0
- capsule_engine/folds/catalog_defs/spend.weekly.yaml +33 -0
- capsule_engine/folds/counterparty_signals.py +179 -0
- capsule_engine/folds/definition.py +421 -0
- capsule_engine/folds/duration.py +16 -0
- capsule_engine/folds/engine.py +436 -0
- capsule_engine/folds/errors.py +44 -0
- capsule_engine/folds/loader.py +24 -0
- capsule_engine/folds/ordering.py +143 -0
- capsule_engine/folds/paths.py +25 -0
- capsule_engine/folds/record_type_coverage.py +58 -0
- capsule_engine/folds/reducers.py +75 -0
- capsule_engine/folds/retention_continuity.py +117 -0
- capsule_engine/folds/set_membership.py +59 -0
- capsule_engine/folds/taxonomy.py +231 -0
- capsule_engine/guards/__init__.py +59 -0
- capsule_engine/guards/action.py +446 -0
- capsule_engine/guards/action_taxonomy.json +27 -0
- capsule_engine/guards/action_taxonomy_v2.json +27 -0
- capsule_engine/guards/action_taxonomy_v3.json +27 -0
- capsule_engine/guards/action_taxonomy_v4.json +27 -0
- capsule_engine/guards/action_taxonomy_v5.json +27 -0
- capsule_engine/guards/capsule.py +377 -0
- capsule_engine/guards/checks/__init__.py +268 -0
- capsule_engine/guards/checks/action_class_gate.py +199 -0
- capsule_engine/guards/checks/base.py +33 -0
- capsule_engine/guards/checks/caps.py +517 -0
- capsule_engine/guards/checks/channel_change.py +64 -0
- capsule_engine/guards/checks/commitment.py +41 -0
- capsule_engine/guards/checks/counterparty_identity_change.py +95 -0
- capsule_engine/guards/checks/counterparty_list.py +176 -0
- capsule_engine/guards/checks/counterparty_seen_before.py +160 -0
- capsule_engine/guards/checks/credential_pattern.py +73 -0
- capsule_engine/guards/checks/dedupe.py +259 -0
- capsule_engine/guards/checks/destination_rail.py +120 -0
- capsule_engine/guards/checks/field_change_count.py +100 -0
- capsule_engine/guards/checks/offer_expiry.py +134 -0
- capsule_engine/guards/checks/plan_containment.py +180 -0
- capsule_engine/guards/checks/price_floor.py +211 -0
- capsule_engine/guards/checks/promise_never.py +236 -0
- capsule_engine/guards/checks/promise_requires_approval.py +214 -0
- capsule_engine/guards/checks/recipient_role.py +117 -0
- capsule_engine/guards/checks/recurring_charge.py +49 -0
- capsule_engine/guards/checks/refundability.py +46 -0
- capsule_engine/guards/checks/release_on_acceptance.py +286 -0
- capsule_engine/guards/checks/required_disclosure.py +233 -0
- capsule_engine/guards/checks/single_commitment.py +221 -0
- capsule_engine/guards/checks/task_authority.py +182 -0
- capsule_engine/guards/checks/upfront_amount.py +71 -0
- capsule_engine/guards/checks/verify_before_dispatch.py +75 -0
- capsule_engine/guards/classes.py +393 -0
- capsule_engine/guards/engine.py +926 -0
- capsule_engine/guards/history_state.py +127 -0
- capsule_engine/guards/plan.py +237 -0
- capsule_engine/guards/revocation.py +120 -0
- capsule_engine/guards/signing.py +60 -0
- capsule_engine/guards/statements.py +62 -0
- capsule_engine/guards/tool_call.py +83 -0
- capsule_engine/guards/wickets/__init__.py +23 -0
- capsule_engine/guards/wickets/catalog.py +75 -0
- capsule_engine/guards/wickets/catalog_defs/action_class_gate.v1.1.yaml +38 -0
- capsule_engine/guards/wickets/catalog_defs/action_class_gate.v1.2.yaml +52 -0
- capsule_engine/guards/wickets/catalog_defs/action_class_gate.yaml +34 -0
- capsule_engine/guards/wickets/catalog_defs/caps.v2.yaml +30 -0
- capsule_engine/guards/wickets/catalog_defs/caps.v3.yaml +49 -0
- capsule_engine/guards/wickets/catalog_defs/caps.v4.yaml +45 -0
- capsule_engine/guards/wickets/catalog_defs/caps.v5.1.yaml +53 -0
- capsule_engine/guards/wickets/catalog_defs/caps.v5.yaml +43 -0
- capsule_engine/guards/wickets/catalog_defs/caps.yaml +17 -0
- capsule_engine/guards/wickets/catalog_defs/caps_holds.yaml +18 -0
- capsule_engine/guards/wickets/catalog_defs/channel_change.v1.1.yaml +18 -0
- capsule_engine/guards/wickets/catalog_defs/channel_change.yaml +10 -0
- capsule_engine/guards/wickets/catalog_defs/counterparty_identity_change.yaml +9 -0
- capsule_engine/guards/wickets/catalog_defs/counterparty_list.yaml +20 -0
- capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.v2.yaml +15 -0
- capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.v3.yaml +23 -0
- capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.v4.yaml +26 -0
- capsule_engine/guards/wickets/catalog_defs/counterparty_seen_before.yaml +14 -0
- capsule_engine/guards/wickets/catalog_defs/credential_pattern.yaml +14 -0
- capsule_engine/guards/wickets/catalog_defs/dedupe.yaml +19 -0
- capsule_engine/guards/wickets/catalog_defs/destination_rail.seller.v1.1.yaml +32 -0
- capsule_engine/guards/wickets/catalog_defs/destination_rail.seller.yaml +10 -0
- capsule_engine/guards/wickets/catalog_defs/destination_rail.yaml +9 -0
- capsule_engine/guards/wickets/catalog_defs/hold_reconcile.yaml +14 -0
- capsule_engine/guards/wickets/catalog_defs/material_fields_changed.v1.1.yaml +22 -0
- capsule_engine/guards/wickets/catalog_defs/material_fields_changed.yaml +14 -0
- capsule_engine/guards/wickets/catalog_defs/offer_expiry.v1.1.yaml +31 -0
- capsule_engine/guards/wickets/catalog_defs/offer_expiry.yaml +24 -0
- capsule_engine/guards/wickets/catalog_defs/offer_fields_changed.v1.1.yaml +22 -0
- capsule_engine/guards/wickets/catalog_defs/offer_fields_changed.yaml +10 -0
- capsule_engine/guards/wickets/catalog_defs/price_floor.yaml +32 -0
- capsule_engine/guards/wickets/catalog_defs/promise_never.v1.1.yaml +64 -0
- capsule_engine/guards/wickets/catalog_defs/promise_never.yaml +16 -0
- capsule_engine/guards/wickets/catalog_defs/promise_requires_approval.v1.1.yaml +26 -0
- capsule_engine/guards/wickets/catalog_defs/promise_requires_approval.yaml +21 -0
- capsule_engine/guards/wickets/catalog_defs/recipient_role.seller.v1.1.yaml +23 -0
- capsule_engine/guards/wickets/catalog_defs/recipient_role.seller.v1.2.yaml +24 -0
- capsule_engine/guards/wickets/catalog_defs/recipient_role.seller.yaml +10 -0
- capsule_engine/guards/wickets/catalog_defs/recipient_role.v1.1.yaml +26 -0
- capsule_engine/guards/wickets/catalog_defs/recipient_role.yaml +11 -0
- capsule_engine/guards/wickets/catalog_defs/recipient_seen_before.yaml +16 -0
- capsule_engine/guards/wickets/catalog_defs/recurring_charge.yaml +9 -0
- capsule_engine/guards/wickets/catalog_defs/refundability.v1.1.yaml +15 -0
- capsule_engine/guards/wickets/catalog_defs/refundability.yaml +7 -0
- capsule_engine/guards/wickets/catalog_defs/release_on_acceptance.seller.v1.1.yaml +44 -0
- capsule_engine/guards/wickets/catalog_defs/release_on_acceptance.seller.v1.2.yaml +63 -0
- capsule_engine/guards/wickets/catalog_defs/release_on_acceptance.seller.yaml +36 -0
- capsule_engine/guards/wickets/catalog_defs/required_disclosure.v1.1.yaml +36 -0
- capsule_engine/guards/wickets/catalog_defs/required_disclosure.v1.2.yaml +42 -0
- capsule_engine/guards/wickets/catalog_defs/required_disclosure.yaml +25 -0
- capsule_engine/guards/wickets/catalog_defs/single_commitment.seller.yaml +14 -0
- capsule_engine/guards/wickets/catalog_defs/task_authority.v1.1.yaml +20 -0
- capsule_engine/guards/wickets/catalog_defs/task_authority.yaml +8 -0
- capsule_engine/guards/wickets/catalog_defs/upfront_amount.v1.1.yaml +19 -0
- capsule_engine/guards/wickets/catalog_defs/upfront_amount.yaml +11 -0
- capsule_engine/guards/wickets/catalog_defs/verify_before_dispatch.yaml +9 -0
- capsule_engine/guards/wickets/definition.py +164 -0
- capsule_engine/guards/wickets/errors.py +18 -0
- capsule_engine/guards/wickets/loader.py +26 -0
- capsule_engine/guards/wickets/retired.py +62 -0
- capsule_engine/holds/__init__.py +35 -0
- capsule_engine/holds/capsules.py +266 -0
- capsule_engine/holds/engine.py +476 -0
- capsule_engine/holds/errors.py +29 -0
- capsule_engine/holds/policy.py +62 -0
- capsule_engine/holds/scope.py +47 -0
- capsule_engine/mcp/__init__.py +16 -0
- capsule_engine/mcp/config.py +117 -0
- capsule_engine/mcp/server.py +338 -0
- capsule_engine/mcp/tools.py +424 -0
- capsule_engine/packaging.py +60 -0
- capsule_engine/packs/__init__.py +45 -0
- capsule_engine/packs/backfill_coverage.py +238 -0
- capsule_engine/packs/catalog/airline-engagement/AI-BOOTSTRAP.md +53 -0
- capsule_engine/packs/catalog/airline-engagement/folds/session_close.yaml +8 -0
- capsule_engine/packs/catalog/airline-engagement/pack.yaml +146 -0
- capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/good_execution.json +36 -0
- capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/oversight_declared.json +36 -0
- capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/tampered_execution.json +36 -0
- capsule_engine/packs/catalog/eu-ai-act/fixtures/trace/trusted_issuer.jwk.json +5 -0
- capsule_engine/packs/catalog/eu-ai-act/obligations-pack-note.md +68 -0
- capsule_engine/packs/catalog/eu-ai-act/obligations-register.yaml +761 -0
- capsule_engine/packs/catalog/eu-ai-act/pack.yaml +88 -0
- capsule_engine/packs/catalog/eu-ai-act/register.yaml +96 -0
- capsule_engine/packs/catalog/eu-ai-act-deterministic/fixtures/synthetic_internal_assist_sample.jsonl +6 -0
- capsule_engine/packs/catalog/eu-ai-act-deterministic/pack.yaml +580 -0
- capsule_engine/packs/catalog/everyday/AI-BOOTSTRAP.md +194 -0
- capsule_engine/packs/catalog/everyday/fixtures/mini_ledger.jsonl +45 -0
- capsule_engine/packs/catalog/everyday/pack.yaml +480 -0
- capsule_engine/packs/catalog/payments-safety/AI-BOOTSTRAP.md +83 -0
- capsule_engine/packs/catalog/payments-safety/fixtures/mini_ledger.jsonl +8 -0
- capsule_engine/packs/catalog/payments-safety/folds/spend_weekly.yaml +29 -0
- capsule_engine/packs/catalog/payments-safety/pack.yaml +112 -0
- capsule_engine/packs/catalog/seller/fixtures/mini_ledger.jsonl +83 -0
- capsule_engine/packs/catalog/seller/pack.yaml +503 -0
- capsule_engine/packs/catalog/standard-vendor/AI-BOOTSTRAP.md +70 -0
- capsule_engine/packs/catalog/standard-vendor/folds/session_close.yaml +8 -0
- capsule_engine/packs/catalog/standard-vendor/pack.yaml +466 -0
- capsule_engine/packs/contract_diff.py +809 -0
- capsule_engine/packs/contract_validate.py +185 -0
- capsule_engine/packs/corpus_verify.py +126 -0
- capsule_engine/packs/enforce.py +81 -0
- capsule_engine/packs/errors.py +159 -0
- capsule_engine/packs/eu_ai_act_trace_field_map.py +46 -0
- capsule_engine/packs/install.py +266 -0
- capsule_engine/packs/loader.py +1462 -0
- capsule_engine/packs/measurability_report.py +280 -0
- capsule_engine/packs/obligation_results.py +72 -0
- capsule_engine/packs/pins.py +92 -0
- capsule_engine/packs/schema.py +1053 -0
- capsule_engine/packs/thresholds.py +146 -0
- capsule_engine/packs/trace_attainment.py +188 -0
- capsule_engine/packs/trace_reader.py +104 -0
- capsule_engine/policy/__init__.py +37 -0
- capsule_engine/policy/activation.py +87 -0
- capsule_engine/policy/catalog_defs/default.yaml +23 -0
- capsule_engine/policy/catalog_defs/holds.yaml +27 -0
- capsule_engine/policy/errors.py +46 -0
- capsule_engine/policy/limits.py +287 -0
- capsule_engine/policy/loader.py +26 -0
- capsule_engine/policy/manifest.py +289 -0
- capsule_engine/policy/profile.py +281 -0
- capsule_engine/policy/resolve.py +224 -0
- capsule_engine/register/__init__.py +39 -0
- capsule_engine/register/compiler.py +219 -0
- capsule_engine/register/errors.py +40 -0
- capsule_engine/register/loader.py +247 -0
- capsule_engine/register/schema.py +129 -0
- capsule_engine/registry/__init__.py +19 -0
- capsule_engine/registry/conventions.json +162 -0
- capsule_engine/registry/conventions.py +188 -0
- capsule_engine/registry/cpb_registry.json +78 -0
- capsule_engine/report/__init__.py +62 -0
- capsule_engine/report/build.py +267 -0
- capsule_engine/report/coverage.py +777 -0
- capsule_engine/report/errors.py +35 -0
- capsule_engine/report/live_check.py +126 -0
- capsule_engine/report/live_history.py +420 -0
- capsule_engine/report/model.py +95 -0
- capsule_engine/report/render.py +467 -0
- capsule_engine/report/replay.py +1677 -0
- capsule_engine/report/result.py +504 -0
- capsule_engine/report/result_from_folds.py +222 -0
- capsule_engine/report/result_from_layers.py +670 -0
- capsule_engine/report/sale_bundle.py +864 -0
- capsule_engine/report/static/verify.js +451 -0
- capsule_engine/schemas/evidence-contract-v0.json +383 -0
- capsule_engine/schemas/vendor/epistemic-types.json +21 -0
- capsule_engine/schemas/vendor/evidence-result-v0.json +463 -0
- capsule_engine/telemetry/__init__.py +9 -0
- capsule_engine/telemetry/consent.py +39 -0
- capsule_engine/telemetry/events.py +93 -0
- capsule_engine/telemetry/fixtures/synthetic_events.jsonl +45 -0
- capsule_engine/telemetry/funnel.py +147 -0
- capsule_engine/telemetry/record.py +106 -0
- capsule_engine/telemetry/sink.py +61 -0
- capsule_engine/telemetry/state.py +60 -0
- capsule_engine/tenants.py +327 -0
- capsule_engine-0.1.0.dist-info/METADATA +47 -0
- capsule_engine-0.1.0.dist-info/RECORD +288 -0
- capsule_engine-0.1.0.dist-info/WHEEL +5 -0
- capsule_engine-0.1.0.dist-info/entry_points.txt +2 -0
- capsule_engine-0.1.0.dist-info/licenses/LICENSE +202 -0
- capsule_engine-0.1.0.dist-info/licenses/capsule_engine/examples/data/tau2_airline/LICENSE-tau2-bench +21 -0
- capsule_engine-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""release_on_acceptance check: personal data is released only to the buyer
|
|
3
|
+
who accepted.
|
|
4
|
+
|
|
5
|
+
A seller negotiates one sale with several buyers, one thread each. Personal
|
|
6
|
+
data may reach a buyer only once that buyer's acceptance is sealed. The
|
|
7
|
+
wicket's ``binding`` selects how a disclosure is bound to it.
|
|
8
|
+
|
|
9
|
+
**Cited** (no ``binding``: seller.release_on_acceptance/1.0.0 and 1.1.0).
|
|
10
|
+
``release_classes`` is the closed set of classes a disclosure may declare;
|
|
11
|
+
a disclosure in ``action_classes`` whose ``representation_class`` is in it
|
|
12
|
+
passes only when ALL hold:
|
|
13
|
+
|
|
14
|
+
- it names its sale (``task_authority_ref`` and ``item_ref``) and its
|
|
15
|
+
recipient (``target``);
|
|
16
|
+
- it cites (``cited_mandate_capsule_id``) the sale's acceptance, found by
|
|
17
|
+
``single_commitment.first_sealed_acceptance`` so both checks read the same
|
|
18
|
+
record: the sale is the ``item_ref``, under any task authority, and an
|
|
19
|
+
acceptance that is not known (a live history that cannot settle it) is
|
|
20
|
+
none;
|
|
21
|
+
- that acceptance was sealed for the same recipient (its ``target`` equals
|
|
22
|
+
the disclosure's);
|
|
23
|
+
- the cited record re-verifies in the ledger.
|
|
24
|
+
|
|
25
|
+
Anything else fails: no citation, a citation of another sale's or another
|
|
26
|
+
buyer's acceptance, a record that is not a sealed acceptance, a missing
|
|
27
|
+
reference, and a ``representation_class`` that is missing or outside
|
|
28
|
+
``release_classes``.
|
|
29
|
+
|
|
30
|
+
**Same deal, same buyer** (``binding: same_deal_same_buyer``,
|
|
31
|
+
seller.release_on_acceptance/1.2.0). The disclosure's classes of data
|
|
32
|
+
(``disclosed_classes``) are read, never ``representation_class``. Every
|
|
33
|
+
class must be in ``release_classes``; a disclosure naming none, or any
|
|
34
|
+
other, fails. One whose classes are all in ``pre_acceptance_classes`` (the
|
|
35
|
+
listing's public pickup area) passes to any buyer, before or after any
|
|
36
|
+
acceptance. Anything else passes only when it names its ``item_ref``, its
|
|
37
|
+
``deal_id`` and the counterparty fingerprint its check sealed
|
|
38
|
+
(``counterparty_ids`` under ``counterparty_fp_alg``), and the sale's
|
|
39
|
+
acceptance (``single_commitment.sale_acceptance``, the same record the cited
|
|
40
|
+
binding reads) is known, is in the same deal (``deal_id`` equal), and was
|
|
41
|
+
sealed for the same buyer: its ``thread_counterparty``, which the history
|
|
42
|
+
carries beside the act and no decision seals
|
|
43
|
+
(``report/live_history.py``), is under the same algorithm, and at least one
|
|
44
|
+
per-person identity kind (``email``, ``phone``, ``profile_id``,
|
|
45
|
+
``relay_address``) is on both sides and every such kind on both sides is
|
|
46
|
+
equal; ``name``, ``payee`` and ``domain`` are then not compared: a name is
|
|
47
|
+
written many ways, and the thread default seals ``payee`` from it, while a
|
|
48
|
+
marketplace domain is shared by every stranger on it. When neither side
|
|
49
|
+
seals any per-person kind (an opening that named the buyer by name and
|
|
50
|
+
domain only), the buyer is the same when both sides seal a ``name`` and a
|
|
51
|
+
``domain`` and both are equal; with the deal equal too, that is the thread's
|
|
52
|
+
own buyer. A per-person kind on one side only, a name alone or a domain
|
|
53
|
+
alone is no buyer. The ``payee`` is never compared. The acceptance is not
|
|
54
|
+
cited and not re-verified: the history entries are the producer's own
|
|
55
|
+
sealed records on the user's device, the trust ``single_commitment`` places
|
|
56
|
+
in them. A history that may be missing acts, an act of this sale that could
|
|
57
|
+
not be read, an acceptance-class act with no disposition, or an earlier
|
|
58
|
+
accepted commitment in the history that names no item, leave the
|
|
59
|
+
acceptance unknown, which is none here. The counterparty block must be a
|
|
60
|
+
check's, whole (``history_state.well_formed_counterparty``) on both sides: a
|
|
61
|
+
share whose block holds any member of another kind or shape fails naming
|
|
62
|
+
``counterparty``, so a member dropped for its shape never reads as absent
|
|
63
|
+
(and never opens the name-and-domain reading), and an acceptance whose
|
|
64
|
+
block is not whole carries none, so it binds no share. No acceptance, an unknown one, and
|
|
65
|
+
one in another deal or by another buyer fail with one reason, so a buyer
|
|
66
|
+
cannot learn whether someone else accepted. A share naming no class, or a
|
|
67
|
+
class outside ``release_classes`` (the credential, verification-code, card
|
|
68
|
+
and ID classes), has its own reason: it says nothing about other buyers.
|
|
69
|
+
The rule applies to every recipient but ``except_recipient_roles`` (a
|
|
70
|
+
buyer, a third party, the user), so those classes go to no one but the
|
|
71
|
+
platform.
|
|
72
|
+
|
|
73
|
+
Under either binding two inputs fail closed: an ``action_class`` that is
|
|
74
|
+
missing or has no row in the action taxonomy (it is resolved there, so a
|
|
75
|
+
legacy alias counts as its canonical class), because the action cannot be
|
|
76
|
+
shown to be out of scope. Only another known action class is ``n/a`` out of
|
|
77
|
+
scope, and, when the wicket configures ``except_recipient_roles`` (1.1.0
|
|
78
|
+
and 1.2.0: ``platform``), a disclosure whose ``recipient_role`` is one of
|
|
79
|
+
them: it goes to no buyer, so there is no acceptance to bind, and
|
|
80
|
+
``recipient_role`` measures what it may carry. The engine lists this as an
|
|
81
|
+
integrity check, so a failure refuses whatever the pack declares; a
|
|
82
|
+
one-shot approval does not clear it.
|
|
83
|
+
|
|
84
|
+
The outcome carries no value. A checker result is sealed into a record that
|
|
85
|
+
can reach a buyer's copy, so the reason and evidence never hold the
|
|
86
|
+
disclosed data, ``item_ref``, ``task_authority_ref``, a deal, a counterparty
|
|
87
|
+
or a capsule id. The evidence names only the class or classes read (class
|
|
88
|
+
names from a closed set), whether the release was bound to an acceptance,
|
|
89
|
+
and a missing field's name.
|
|
90
|
+
"""
|
|
91
|
+
from __future__ import annotations
|
|
92
|
+
|
|
93
|
+
from typing import TypedDict
|
|
94
|
+
|
|
95
|
+
from cll.ledger.api import LedgerAPI
|
|
96
|
+
|
|
97
|
+
from ..action import Action
|
|
98
|
+
from ..capsule import ConstraintOutcome, NotApplicableEvidence, not_applicable_evidence
|
|
99
|
+
from ..classes import resolve
|
|
100
|
+
from ..history_state import well_formed_counterparty
|
|
101
|
+
from .base import CheckOutcome
|
|
102
|
+
from .single_commitment import ThreadCounterparty, first_sealed_acceptance, sale_acceptance
|
|
103
|
+
|
|
104
|
+
__all__ = ["SAME_DEAL_SAME_BUYER", "check_release_on_acceptance", "same_buyer"]
|
|
105
|
+
|
|
106
|
+
_CHECK_ID = "release_on_acceptance"
|
|
107
|
+
_METHOD = "cited_sealed_acceptance_v1"
|
|
108
|
+
_NOT_BOUND = "the disclosure does not cite this sale's sealed acceptance by this recipient"
|
|
109
|
+
# The ``binding`` that binds the acceptance from the history (1.2.0).
|
|
110
|
+
SAME_DEAL_SAME_BUYER = "same_deal_same_buyer"
|
|
111
|
+
_HISTORY_METHOD = "same_deal_same_buyer_v1"
|
|
112
|
+
_NOT_ACCEPTED = "the history given shows no acceptance of this sale by this buyer in this deal"
|
|
113
|
+
# The identity kinds that name one person (capsule-cli deal_counterparty.go
|
|
114
|
+
# perPartyKinds). A name, a payee and a domain are fingerprinted too but are
|
|
115
|
+
# not identities.
|
|
116
|
+
_PER_PERSON_KINDS = ("email", "phone", "profile_id", "relay_address")
|
|
117
|
+
# What names the thread's own buyer when neither side seals a per-person
|
|
118
|
+
# kind: both of these, equal. The payee is never read.
|
|
119
|
+
_NO_PER_PERSON_KINDS = ("name", "domain")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ReleaseEvidence(TypedDict):
|
|
123
|
+
constraint_id: str
|
|
124
|
+
representation_class: str | None
|
|
125
|
+
bound_to_acceptance: bool
|
|
126
|
+
missing_field: str | None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ClassesEvidence(TypedDict):
|
|
130
|
+
"""1.2.0's evidence: the class names the disclosure gives, never the data."""
|
|
131
|
+
|
|
132
|
+
constraint_id: str
|
|
133
|
+
disclosed_classes: list[str] | None
|
|
134
|
+
bound_to_acceptance: bool
|
|
135
|
+
missing_field: str | None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _evidence(representation_class: str | None, bound: bool, missing_field: str | None = None) -> ReleaseEvidence:
|
|
139
|
+
return ReleaseEvidence(constraint_id=_CHECK_ID, representation_class=representation_class,
|
|
140
|
+
bound_to_acceptance=bound, missing_field=missing_field)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _classes_evidence(classes: tuple[str, ...] | None, bound: bool,
|
|
144
|
+
missing_field: str | None = None) -> ClassesEvidence:
|
|
145
|
+
return ClassesEvidence(constraint_id=_CHECK_ID, disclosed_classes=list(classes) if classes else None,
|
|
146
|
+
bound_to_acceptance=bound, missing_field=missing_field)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _outcome(result: str, reason: str, evidence: ReleaseEvidence | ClassesEvidence | NotApplicableEvidence,
|
|
150
|
+
method: str = _METHOD) -> CheckOutcome:
|
|
151
|
+
return CheckOutcome(
|
|
152
|
+
constraint=ConstraintOutcome(
|
|
153
|
+
id=_CHECK_ID, result=result, reason=reason, evidence=evidence, check_type="policy", method=method
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _missing_shape(action: Action, field: str) -> str:
|
|
159
|
+
# An ignored input is named, never the value it was given.
|
|
160
|
+
return f"the {field} input was not in its agreed shape" if field in action.ignored_inputs else f"no {field}"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _missing(action: Action, cls: str, field: str) -> CheckOutcome:
|
|
164
|
+
return _outcome("fail", f"the disclosure has {_missing_shape(action, field)}; it cannot be bound to an acceptance",
|
|
165
|
+
_evidence(cls, False, missing_field=field))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def same_buyer(acceptance: ThreadCounterparty | None, fp_alg: str | None, ids: dict[str, str] | None) -> bool:
|
|
169
|
+
"""Whether the counterparty an acceptance's check sealed and the one a
|
|
170
|
+
disclosure's check sealed are one buyer: both fingerprinted under the
|
|
171
|
+
same algorithm, and either at least one per-person identity kind on both
|
|
172
|
+
sides with every per-person kind on both sides equal, or, when neither
|
|
173
|
+
side seals any per-person kind, both a ``name`` and a ``domain`` on both
|
|
174
|
+
sides, each equal. A payee is never read (module docstring)."""
|
|
175
|
+
if acceptance is None or not fp_alg or not ids or acceptance["fp_alg"] != fp_alg:
|
|
176
|
+
return False
|
|
177
|
+
theirs = acceptance["ids"]
|
|
178
|
+
if not any(k in theirs or k in ids for k in _PER_PERSON_KINDS):
|
|
179
|
+
# The deal is checked apart: this is the thread's own buyer by the
|
|
180
|
+
# opening's name and domain.
|
|
181
|
+
return all(k in theirs and k in ids and theirs[k] == ids[k] for k in _NO_PER_PERSON_KINDS)
|
|
182
|
+
shared = [k for k in _PER_PERSON_KINDS if k in theirs and k in ids]
|
|
183
|
+
return bool(shared) and all(theirs[k] == ids[k] for k in shared)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def check_release_on_acceptance(
|
|
187
|
+
action: Action,
|
|
188
|
+
ledger: LedgerAPI,
|
|
189
|
+
*,
|
|
190
|
+
release_classes: list[str],
|
|
191
|
+
acceptance_classes: list[str],
|
|
192
|
+
action_classes: list[str],
|
|
193
|
+
except_recipient_roles: list[str] | None = None,
|
|
194
|
+
binding: str | None = None,
|
|
195
|
+
pre_acceptance_classes: list[str] | None = None,
|
|
196
|
+
) -> CheckOutcome:
|
|
197
|
+
"""The check under the wicket's ``binding``: cited when it is unset (the
|
|
198
|
+
1.0.0 and 1.1.0 configs, decided as they always were), from the history
|
|
199
|
+
under ``same_deal_same_buyer`` (module docstring)."""
|
|
200
|
+
if binding is not None and binding != SAME_DEAL_SAME_BUYER:
|
|
201
|
+
raise ValueError(f"release_on_acceptance has no binding {binding!r}")
|
|
202
|
+
method = _HISTORY_METHOD if binding is not None else _METHOD
|
|
203
|
+
ac = resolve(action.action_class) if action.action_class is not None else None
|
|
204
|
+
if ac is None:
|
|
205
|
+
evidence = (_classes_evidence(action.disclosed_classes, False, missing_field="action_class")
|
|
206
|
+
if binding is not None else _evidence(None, False, missing_field="action_class"))
|
|
207
|
+
return _outcome("fail", "the action class has no taxonomy row; it cannot be shown to be outside the rule",
|
|
208
|
+
evidence, method)
|
|
209
|
+
if ac.name not in action_classes:
|
|
210
|
+
return _outcome("n/a", "the rule is not configured for this action class",
|
|
211
|
+
not_applicable_evidence(_CHECK_ID, in_scope=False), method)
|
|
212
|
+
if action.recipient_role is not None and action.recipient_role in (except_recipient_roles or ()):
|
|
213
|
+
return _outcome("n/a", f"the recipient is a {action.recipient_role}, not a buyer; recipient_role measures it",
|
|
214
|
+
not_applicable_evidence(_CHECK_ID, in_scope=False), method)
|
|
215
|
+
if binding is not None:
|
|
216
|
+
return _release_from_history(action, ledger, release_classes=release_classes,
|
|
217
|
+
acceptance_classes=acceptance_classes,
|
|
218
|
+
pre_acceptance_classes=pre_acceptance_classes or [])
|
|
219
|
+
cls = action.representation_class
|
|
220
|
+
if cls not in release_classes:
|
|
221
|
+
# An undeclared class's name is the producer's free text, never recorded.
|
|
222
|
+
return _outcome("fail", "the disclosure declares no class this rule knows; it is not released",
|
|
223
|
+
_evidence(None, False, missing_field="representation_class"))
|
|
224
|
+
for field, value in (("task_authority_ref", action.task_authority_ref), ("item_ref", action.item_ref),
|
|
225
|
+
("target", action.target)):
|
|
226
|
+
if value is None:
|
|
227
|
+
return _missing(action, cls, field)
|
|
228
|
+
if action.cited_mandate_capsule_id is None:
|
|
229
|
+
return _outcome("fail", "the disclosure cites no acceptance", _evidence(cls, False))
|
|
230
|
+
|
|
231
|
+
acceptance = first_sealed_acceptance(action.operator, ledger, acceptance_classes, action.item_ref)
|
|
232
|
+
if (
|
|
233
|
+
acceptance is None
|
|
234
|
+
or action.cited_mandate_capsule_id != acceptance["capsule_id"]
|
|
235
|
+
or action.target != acceptance["counterparty"]
|
|
236
|
+
):
|
|
237
|
+
return _outcome("fail", _NOT_BOUND, _evidence(cls, False))
|
|
238
|
+
verified = ledger.verify(acceptance["capsule_id"])
|
|
239
|
+
if verified is None or not verified.ok:
|
|
240
|
+
return _outcome("fail", _NOT_BOUND, _evidence(cls, False))
|
|
241
|
+
return _outcome("pass", "the disclosure cites this sale's sealed acceptance by this recipient",
|
|
242
|
+
_evidence(cls, True))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _release_from_history(
|
|
246
|
+
action: Action,
|
|
247
|
+
ledger: LedgerAPI,
|
|
248
|
+
*,
|
|
249
|
+
release_classes: list[str],
|
|
250
|
+
acceptance_classes: list[str],
|
|
251
|
+
pre_acceptance_classes: list[str],
|
|
252
|
+
) -> CheckOutcome:
|
|
253
|
+
"""The same-deal, same-buyer binding (1.2.0) for a disclosure in scope."""
|
|
254
|
+
classes = action.disclosed_classes
|
|
255
|
+
if classes is None:
|
|
256
|
+
return _outcome("fail", "the share names no class of data",
|
|
257
|
+
_classes_evidence(None, False, missing_field="disclosed_classes"), _HISTORY_METHOD)
|
|
258
|
+
if any(c not in release_classes for c in classes):
|
|
259
|
+
return _outcome("fail", "a credential, a verification code, a payment card or an ID document is never shared "
|
|
260
|
+
"with anyone",
|
|
261
|
+
_classes_evidence(classes, False), _HISTORY_METHOD)
|
|
262
|
+
if all(c in pre_acceptance_classes for c in classes):
|
|
263
|
+
return _outcome("pass", "every class the disclosure names may go to a buyer before acceptance",
|
|
264
|
+
_classes_evidence(classes, False), _HISTORY_METHOD)
|
|
265
|
+
for field, present in (("item_ref", action.item_ref is not None), ("deal_id", action.deal_id is not None),
|
|
266
|
+
("counterparty", bool(action.counterparty_ids) and bool(action.counterparty_fp_alg))):
|
|
267
|
+
if not present:
|
|
268
|
+
return _outcome("fail", f"the disclosure has {_missing_shape(action, field)}; it cannot be bound to an "
|
|
269
|
+
"acceptance", _classes_evidence(classes, False, missing_field=field), _HISTORY_METHOD)
|
|
270
|
+
if action.counterparty_malformed or not well_formed_counterparty(action.counterparty_fp_alg,
|
|
271
|
+
action.counterparty_ids):
|
|
272
|
+
# A member dropped for its shape must not read as absent: the whole
|
|
273
|
+
# block is unusable.
|
|
274
|
+
return _outcome("fail", "the counterparty input was not in its agreed shape; it cannot be bound to an "
|
|
275
|
+
"acceptance", _classes_evidence(classes, False, missing_field="counterparty"),
|
|
276
|
+
_HISTORY_METHOD)
|
|
277
|
+
sale = sale_acceptance(action.operator, ledger, acceptance_classes, action.item_ref)
|
|
278
|
+
acceptance = sale.acceptance if sale.unknown is None else None
|
|
279
|
+
if (
|
|
280
|
+
acceptance is None
|
|
281
|
+
or acceptance["deal_id"] != action.deal_id
|
|
282
|
+
or not same_buyer(acceptance["thread_counterparty"], action.counterparty_fp_alg, action.counterparty_ids)
|
|
283
|
+
):
|
|
284
|
+
return _outcome("fail", _NOT_ACCEPTED, _classes_evidence(classes, False), _HISTORY_METHOD)
|
|
285
|
+
return _outcome("pass", "the history given shows this buyer accepted this sale in this deal",
|
|
286
|
+
_classes_evidence(classes, True), _HISTORY_METHOD)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""required_disclosure check: has each required class of statement been
|
|
3
|
+
made to this counterparty before the action?
|
|
4
|
+
|
|
5
|
+
For each class in ``required_classes``, the count is the
|
|
6
|
+
``disclosure.class_seen`` fold's own result, keyed by
|
|
7
|
+
``asg_payload.representation_class``, over this operator's records addressed
|
|
8
|
+
to ``Action.target``. Only accepted actions that were not dry runs count. A
|
|
9
|
+
zero for any required class fails the check; the evidence lists each class's
|
|
10
|
+
count and the classes still missing. It checks that a statement of the class
|
|
11
|
+
was recorded, never what it said or whether it was true. Applies only to
|
|
12
|
+
the configured ``action_classes``.
|
|
13
|
+
|
|
14
|
+
A failure means the action cannot proceed as constructed: make the
|
|
15
|
+
disclosure first, or change the task authority. It is never cleared by an
|
|
16
|
+
approval: this check reads no approval record and ignores
|
|
17
|
+
``Action.authorized_by``, because a one-shot approval does not waive a
|
|
18
|
+
statement the person already required.
|
|
19
|
+
|
|
20
|
+
With ``statement_definition`` (``required_disclosure/1.1.0``, its fold
|
|
21
|
+
``disclosure.statement_seen/1.0.0``), a class also counts as stated when the
|
|
22
|
+
action's deal (``Action.deal_id``) already holds a statement of it: a record
|
|
23
|
+
``report/replay.py``'s ``statement_record`` wrote for a claim the seller's
|
|
24
|
+
agent made in that deal before the action, read from the bundle in a replay
|
|
25
|
+
and from the check input's history live. Both counts are in the evidence,
|
|
26
|
+
the deal's beside the counterparty's. The deal stands for the counterparty
|
|
27
|
+
here: capsulectl opens one thread per buyer. A statement record names no
|
|
28
|
+
counterparty, so a counterparty that changes within a deal is not seen here
|
|
29
|
+
(``counterparty_identity_change`` reads that). An action with neither a
|
|
30
|
+
target nor a deal is not applicable, as one with no target is without it;
|
|
31
|
+
one with a target and no deal counts the counterparty's statements only, and
|
|
32
|
+
one with a deal and no target the deal's only.
|
|
33
|
+
|
|
34
|
+
With ``count_listing_classes`` (``required_disclosure/1.2.0``), a class also
|
|
35
|
+
counts as stated when it is in ``Action.listing_classes``: the classes every
|
|
36
|
+
live listing of the sale stated when the check was made, as capsulectl sealed
|
|
37
|
+
them in the check's body (the intersection over the latest post per platform).
|
|
38
|
+
A listing is the agent's words to every buyer, so a class it states is stated
|
|
39
|
+
to this one too. The value is read from the check, never recomputed: a re-post
|
|
40
|
+
changes later checks only. With no value (no post) or an empty one, a listing
|
|
41
|
+
counts for nothing, and the class needs one of the other two paths. The
|
|
42
|
+
evidence lists the value beside the two counts.
|
|
43
|
+
|
|
44
|
+
``required_classes`` must come from the closed set ``representation_classes``;
|
|
45
|
+
a class outside it is a configuration error and raises ``ValueError``.
|
|
46
|
+
"""
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
from typing import TypedDict
|
|
50
|
+
|
|
51
|
+
from cll.ledger.api import LedgerAPI, ScanQuery
|
|
52
|
+
|
|
53
|
+
from ...folds.definition import FoldDefinition
|
|
54
|
+
from ...folds.engine import EvaluationTrace, evaluate_one
|
|
55
|
+
from ..action import Action
|
|
56
|
+
from ..capsule import ConstraintOutcome, NotApplicableEvidence, not_applicable_evidence
|
|
57
|
+
from ..statements import STATED
|
|
58
|
+
from .base import CheckOutcome
|
|
59
|
+
|
|
60
|
+
__all__ = ["check_required_disclosure", "require_known_classes"]
|
|
61
|
+
|
|
62
|
+
_CHECK_ID = "required_disclosure"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DisclosureScope(TypedDict):
|
|
66
|
+
operator: str
|
|
67
|
+
target: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DisclosureEvidence(TypedDict):
|
|
71
|
+
fold: str
|
|
72
|
+
scope: DisclosureScope
|
|
73
|
+
prior_counts: dict[str, int]
|
|
74
|
+
missing_classes: list[str]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class StatementScope(TypedDict):
|
|
78
|
+
operator: str
|
|
79
|
+
target: str | None
|
|
80
|
+
deal_id: str | None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class StatementEvidence(TypedDict):
|
|
84
|
+
"""``required_disclosure/1.1.0``'s evidence: each fold's digest and
|
|
85
|
+
counts, and the classes neither counted."""
|
|
86
|
+
|
|
87
|
+
fold: str
|
|
88
|
+
statement_fold: str
|
|
89
|
+
scope: StatementScope
|
|
90
|
+
prior_counts: dict[str, int]
|
|
91
|
+
stated_counts: dict[str, int]
|
|
92
|
+
missing_classes: list[str]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ListingEvidence(StatementEvidence):
|
|
96
|
+
"""``required_disclosure/1.2.0``'s evidence: 1.1.0's, and the listing's
|
|
97
|
+
classes the action carried (``None`` with no post)."""
|
|
98
|
+
|
|
99
|
+
listing_classes: list[str] | None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def require_known_classes(classes: list[str], representation_classes: list[str], *, what: str) -> None:
|
|
103
|
+
unknown = [c for c in classes if c not in representation_classes]
|
|
104
|
+
if unknown:
|
|
105
|
+
raise ValueError(f"{what} names {unknown}, outside the closed set {representation_classes}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _outcome(
|
|
109
|
+
result: str, reason: str, evidence: DisclosureEvidence | StatementEvidence | NotApplicableEvidence, method: str,
|
|
110
|
+
traces: tuple[EvaluationTrace, ...] = (),
|
|
111
|
+
) -> CheckOutcome:
|
|
112
|
+
return CheckOutcome(
|
|
113
|
+
constraint=ConstraintOutcome(
|
|
114
|
+
id=_CHECK_ID, result=result, reason=reason, evidence=evidence, check_type="policy", method=method
|
|
115
|
+
),
|
|
116
|
+
fold_envelopes=tuple(trace.to_envelope() for trace in traces),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def check_required_disclosure(
|
|
121
|
+
action: Action,
|
|
122
|
+
ledger: LedgerAPI,
|
|
123
|
+
*,
|
|
124
|
+
definition: FoldDefinition,
|
|
125
|
+
representation_classes: list[str],
|
|
126
|
+
required_classes: list[str],
|
|
127
|
+
action_classes: list[str],
|
|
128
|
+
statement_definition: FoldDefinition | None = None,
|
|
129
|
+
count_listing_classes: bool = False,
|
|
130
|
+
) -> CheckOutcome:
|
|
131
|
+
require_known_classes(required_classes, representation_classes, what="required_classes")
|
|
132
|
+
if count_listing_classes and statement_definition is None:
|
|
133
|
+
raise ValueError("count_listing_classes goes with statement_fold_id: required_disclosure/1.2.0 builds on 1.1.0")
|
|
134
|
+
method = definition.fold_id
|
|
135
|
+
if action.action_class not in action_classes:
|
|
136
|
+
return _outcome("n/a", "the rule is not configured for this action class",
|
|
137
|
+
not_applicable_evidence(_CHECK_ID, in_scope=False), method)
|
|
138
|
+
if statement_definition is not None:
|
|
139
|
+
return _check_with_statements(action, ledger, definition, statement_definition, required_classes,
|
|
140
|
+
listing=count_listing_classes)
|
|
141
|
+
target = action.target
|
|
142
|
+
if target is None:
|
|
143
|
+
return _outcome("n/a", "the action names no target; the counterparty could not be identified",
|
|
144
|
+
not_applicable_evidence(_CHECK_ID, in_scope=True, missing_field="target"), method)
|
|
145
|
+
|
|
146
|
+
traces = _counted(definition, _to_target(action, ledger, target), required_classes)
|
|
147
|
+
counts = _counts(traces, required_classes)
|
|
148
|
+
missing = [cls for cls in required_classes if counts[cls] == 0]
|
|
149
|
+
evidence = DisclosureEvidence(
|
|
150
|
+
fold=definition.definition_digest(),
|
|
151
|
+
scope=DisclosureScope(operator=action.operator, target=target),
|
|
152
|
+
prior_counts=counts,
|
|
153
|
+
missing_classes=missing,
|
|
154
|
+
)
|
|
155
|
+
return _decided(missing, evidence, method, traces)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _to_target(action: Action, ledger: LedgerAPI, target: str) -> list[dict]:
|
|
159
|
+
"""This operator's records addressed to ``target``. ``ScanQuery.
|
|
160
|
+
counterparty`` is the ledger's filter on ``operator``; the ledger has no
|
|
161
|
+
filter on target, so the records are selected here."""
|
|
162
|
+
return [
|
|
163
|
+
r.capsule for r in ledger.scan(ScanQuery(counterparty=action.operator))
|
|
164
|
+
if (r.capsule.get("asg_payload") or {}).get("target") == target
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _in_deal(action: Action, ledger: LedgerAPI, deal_id: str) -> list[dict]:
|
|
169
|
+
"""This operator's statement records in the deal ``deal_id``."""
|
|
170
|
+
return [
|
|
171
|
+
r.capsule for r in ledger.scan(ScanQuery(counterparty=action.operator))
|
|
172
|
+
if (((r.capsule.get("asg_payload") or {}).get(STATED)) or {}).get("deal_id") == deal_id
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _counted(definition: FoldDefinition, records: list[dict], classes: list[str]) -> tuple[EvaluationTrace, ...]:
|
|
177
|
+
return tuple(evaluate_one(definition, records, key_value=cls) for cls in classes)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _counts(traces: tuple[EvaluationTrace, ...], classes: list[str]) -> dict[str, int]:
|
|
181
|
+
return {cls: trace.result or 0 for cls, trace in zip(classes, traces, strict=True)}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _decided(
|
|
185
|
+
missing: list[str], evidence: DisclosureEvidence | StatementEvidence | ListingEvidence, method: str,
|
|
186
|
+
traces: tuple[EvaluationTrace, ...],
|
|
187
|
+
) -> CheckOutcome:
|
|
188
|
+
if missing:
|
|
189
|
+
return _outcome("fail", f"no accepted prior statement of {missing} to this counterparty: make the disclosure "
|
|
190
|
+
"first or change the task authority; an approval does not waive it", evidence, method,
|
|
191
|
+
traces)
|
|
192
|
+
return _outcome("pass", "every required class was stated to this counterparty", evidence, method, traces)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _check_with_statements(
|
|
196
|
+
action: Action, ledger: LedgerAPI, definition: FoldDefinition, statement_definition: FoldDefinition,
|
|
197
|
+
required_classes: list[str], *, listing: bool = False,
|
|
198
|
+
) -> CheckOutcome:
|
|
199
|
+
"""``required_disclosure/1.1.0``: a class counts as stated to the
|
|
200
|
+
counterparty's target or in the action's deal; with ``listing`` (1.2.0),
|
|
201
|
+
also when the sale's listings in force state it (module docstring)."""
|
|
202
|
+
method = definition.fold_id
|
|
203
|
+
target, deal_id = action.target, action.deal_id
|
|
204
|
+
if target is None and deal_id is None:
|
|
205
|
+
return _outcome("n/a", "the action names no target and no deal; the counterparty could not be identified",
|
|
206
|
+
not_applicable_evidence(_CHECK_ID, in_scope=True, missing_field="target"), method)
|
|
207
|
+
told = _counted(definition, _to_target(action, ledger, target) if target is not None else [], required_classes)
|
|
208
|
+
stated = _counted(statement_definition, _in_deal(action, ledger, deal_id) if deal_id is not None else [],
|
|
209
|
+
required_classes)
|
|
210
|
+
prior, in_deal = _counts(told, required_classes), _counts(stated, required_classes)
|
|
211
|
+
listed = action.listing_classes if listing and action.listing_classes is not None else ()
|
|
212
|
+
missing = [cls for cls in required_classes if prior[cls] == 0 and in_deal[cls] == 0 and cls not in listed]
|
|
213
|
+
scope = StatementScope(operator=action.operator, target=target, deal_id=deal_id)
|
|
214
|
+
if listing:
|
|
215
|
+
evidence: StatementEvidence = ListingEvidence(
|
|
216
|
+
fold=definition.definition_digest(),
|
|
217
|
+
statement_fold=statement_definition.definition_digest(),
|
|
218
|
+
scope=scope,
|
|
219
|
+
prior_counts=prior,
|
|
220
|
+
stated_counts=in_deal,
|
|
221
|
+
missing_classes=missing,
|
|
222
|
+
listing_classes=None if action.listing_classes is None else list(action.listing_classes),
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
evidence = StatementEvidence(
|
|
226
|
+
fold=definition.definition_digest(),
|
|
227
|
+
statement_fold=statement_definition.definition_digest(),
|
|
228
|
+
scope=scope,
|
|
229
|
+
prior_counts=prior,
|
|
230
|
+
stated_counts=in_deal,
|
|
231
|
+
missing_classes=missing,
|
|
232
|
+
)
|
|
233
|
+
return _decided(missing, evidence, method, told + stated)
|