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,421 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Fold definitions (spec §2): parsing, validation, and the definition_digest.
|
|
3
|
+
|
|
4
|
+
A definition is a plain dict on the wire (YAML in, via ``loader.py``). This
|
|
5
|
+
module turns that dict into a validated, immutable ``FoldDefinition`` and
|
|
6
|
+
computes its ``definition_digest`` — SHA-256 over the JCS bytes of the
|
|
7
|
+
canonical form, reusing ``agent_action_capsule.canonical`` rather than
|
|
8
|
+
reimplementing JCS (the same canonicalization the capsule format itself uses,
|
|
9
|
+
per spec §2: "canonicalized (JCS) and identified by its digest").
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from agent_action_capsule.canonical import FloatInDigestError, UnsafeIntegerError, json_digest
|
|
18
|
+
|
|
19
|
+
from .account_core import (
|
|
20
|
+
DEFAULT_DERIVATION_CLASS,
|
|
21
|
+
DERIVATION_CLASSES,
|
|
22
|
+
SELECTION_RANGE,
|
|
23
|
+
AccountDefinition,
|
|
24
|
+
parse_account_definition,
|
|
25
|
+
)
|
|
26
|
+
from .duration import parse_duration_seconds
|
|
27
|
+
from .errors import (
|
|
28
|
+
DUPLICATE_READ_PATH,
|
|
29
|
+
FLOAT_IN_DEFINITION,
|
|
30
|
+
INVALID_FOLD_ID,
|
|
31
|
+
INVALID_REVERSAL,
|
|
32
|
+
MALFORMED_DEFINITION,
|
|
33
|
+
MISSING_REDUCE_FIELD,
|
|
34
|
+
UNBOUNDED_FILTER_OP,
|
|
35
|
+
UNDECLARED_FIELD_READ,
|
|
36
|
+
UNKNOWN_DERIVATION_CLASS,
|
|
37
|
+
UNKNOWN_ERASURE_CLASS,
|
|
38
|
+
UNKNOWN_REDUCER,
|
|
39
|
+
UNSAFE_INTEGER_IN_DEFINITION,
|
|
40
|
+
WALL_CLOCK_REFERENCE,
|
|
41
|
+
FoldDefinitionError,
|
|
42
|
+
)
|
|
43
|
+
from .paths import validate_path
|
|
44
|
+
|
|
45
|
+
# fold_id (spec §2): "human name + semver", namespace-dotted, e.g. "spend.weekly/1.0.0".
|
|
46
|
+
FOLD_ID_RE = re.compile(r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*/\d+\.\d+\.\d+$")
|
|
47
|
+
|
|
48
|
+
ERASURE_CLASSES = frozenset({"commitment-ok", "preimage"})
|
|
49
|
+
|
|
50
|
+
# Reducers: a closed, registered set (spec §2). distinct_count is deliberately
|
|
51
|
+
# excluded (spec §7 open question 1: sketches break byte-exact determinism).
|
|
52
|
+
FIELD_REDUCERS = frozenset({"sum", "min", "max", "last"})
|
|
53
|
+
NO_FIELD_REDUCERS = frozenset({"count"})
|
|
54
|
+
KNOWN_REDUCERS = FIELD_REDUCERS | NO_FIELD_REDUCERS
|
|
55
|
+
|
|
56
|
+
# filter (spec §2): "Bounded operations only: equality, ranges, set membership,
|
|
57
|
+
# prefix. No regex, no user-supplied code."
|
|
58
|
+
BOUNDED_FILTER_OPS = frozenset({"eq", "ne", "in", "not_in", "prefix", "gt", "gte", "lt", "lte"})
|
|
59
|
+
|
|
60
|
+
# Pseudo-fields that would make a fold consult something other than the
|
|
61
|
+
# declared capsule range (spec §3 rule 1). `evaluated_at` is explicitly
|
|
62
|
+
# output-only per spec §4 ("informational, never an input").
|
|
63
|
+
WALL_CLOCK_TOKENS = frozenset({"now", "current_time", "wall_clock", "_now", "evaluated_at", "system_time"})
|
|
64
|
+
|
|
65
|
+
# The chain relations a reversal may cite its charge under. ``supersedes`` is
|
|
66
|
+
# the only terminal relation in the chain.relation registry (Agent Action
|
|
67
|
+
# Capsule REGISTRY §6): it closes the parent's open state, which is what a
|
|
68
|
+
# cancel or refund does to a charge. The non-terminal ones (``confirms``,
|
|
69
|
+
# ``epoch_opens``, ``duplicates``) leave the parent standing.
|
|
70
|
+
REVERSAL_RELATIONS = frozenset({"supersedes"})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class ReadField:
|
|
75
|
+
path: str
|
|
76
|
+
erasure_class: str
|
|
77
|
+
default: Any = None
|
|
78
|
+
has_default: bool = False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class FilterClause:
|
|
83
|
+
field: str
|
|
84
|
+
op: str
|
|
85
|
+
value: Any
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class Window:
|
|
90
|
+
mode: str # "rolling" | "explicit"
|
|
91
|
+
duration: str | None = None # rolling mode: e.g. "7d"
|
|
92
|
+
start: str | None = None # explicit mode: ISO-8601 timestamp
|
|
93
|
+
end: str | None = None # explicit mode: ISO-8601 timestamp
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class Reduce:
|
|
98
|
+
reducer: str
|
|
99
|
+
field: str | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class Reversal:
|
|
104
|
+
"""Records whose ``class_field`` is in ``classes`` take back a prior
|
|
105
|
+
charge instead of adding to the sum (engine.py, ``_reverse``). One is
|
|
106
|
+
linked when its ``relation_field`` is ``relation``, its ``parent_field``
|
|
107
|
+
names a charge this fold already counted in the same group, and its
|
|
108
|
+
amount is positive; it then subtracts its amount, up to what that charge
|
|
109
|
+
has left. Any other is unlinked and contributes nothing. Every other
|
|
110
|
+
record is a charge, and a negative charge counts as zero."""
|
|
111
|
+
|
|
112
|
+
class_field: str
|
|
113
|
+
classes: tuple[str, ...]
|
|
114
|
+
parent_field: str
|
|
115
|
+
relation_field: str
|
|
116
|
+
relation: str
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class FoldDefinition:
|
|
121
|
+
fold_id: str
|
|
122
|
+
reads: tuple[ReadField, ...]
|
|
123
|
+
reduce: Reduce
|
|
124
|
+
emit: str
|
|
125
|
+
filter: tuple[FilterClause, ...] = ()
|
|
126
|
+
key: str | None = None
|
|
127
|
+
window: Window | None = None
|
|
128
|
+
# The derivation-class marker, mapped onto the neutral core's
|
|
129
|
+
# ``derivation_class`` (deterministic | model_assisted). Hand-authored /
|
|
130
|
+
# precondition-decomposable folds are ``deterministic`` (recompute+match);
|
|
131
|
+
# the compiler's model-judgment branch sets ``model_assisted`` on the fold
|
|
132
|
+
# it emits (see compiler/compile.py::_model_assisted_fold). Defaulted so
|
|
133
|
+
# every existing catalog YAML and fixture parses unchanged — the ledger's
|
|
134
|
+
# own ``definition_digest()`` (its authoring digest) does not include this
|
|
135
|
+
# field, so existing digests/vectors are untouched.
|
|
136
|
+
derivation_class: str = DEFAULT_DERIVATION_CLASS
|
|
137
|
+
# Optional, and absent from ``canonical_dict`` when unset, so a
|
|
138
|
+
# definition without one keeps its digest.
|
|
139
|
+
reversal: Reversal | None = None
|
|
140
|
+
|
|
141
|
+
def read_paths(self) -> frozenset[str]:
|
|
142
|
+
return frozenset(r.path for r in self.reads)
|
|
143
|
+
|
|
144
|
+
def to_account_definition(self) -> AccountDefinition:
|
|
145
|
+
"""Project this fold onto a neutral ``capsule_emit.account.AccountDefinition``.
|
|
146
|
+
|
|
147
|
+
This is the de-fork bridge: the SAME definition document,
|
|
148
|
+
evaluated by the ledger or via the core, yields the identical CORE
|
|
149
|
+
``definition_digest`` and identical result — the §7 cross-repo replay
|
|
150
|
+
property. The projection carries the fields the neutral account document
|
|
151
|
+
recognizes (name, selection_kind, reads, derivation_class, bounded
|
|
152
|
+
predicate); the ledger's authoring-only structure (erasure_class,
|
|
153
|
+
window, reduce, defaults) is NOT part of the neutral document by design,
|
|
154
|
+
so it cannot move the core digest.
|
|
155
|
+
|
|
156
|
+
A ledger fold selects over a contiguous ledger range, so its neutral
|
|
157
|
+
selection_kind is ``range``. The predicate is the fold's bounded filter,
|
|
158
|
+
whose op grammar is byte-identical to the core's bounded predicate ops.
|
|
159
|
+
"""
|
|
160
|
+
return parse_account_definition(
|
|
161
|
+
{
|
|
162
|
+
"name": self.fold_id,
|
|
163
|
+
"selection_kind": SELECTION_RANGE,
|
|
164
|
+
"reads": [r.path for r in self.reads],
|
|
165
|
+
"derivation_class": self.derivation_class,
|
|
166
|
+
"predicate": [{"field": f.field, "op": f.op, "value": f.value} for f in self.filter],
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def core_definition_digest(self) -> str:
|
|
171
|
+
"""The neutral ``definition_digest`` for cross-repo replay — computed by
|
|
172
|
+
the core over ``to_account_definition()``. There is exactly one
|
|
173
|
+
implementation of this digest (in ``capsule_emit.account``); the ledger
|
|
174
|
+
never re-implements it."""
|
|
175
|
+
return self.to_account_definition().definition_digest()
|
|
176
|
+
|
|
177
|
+
def canonical_dict(self) -> dict:
|
|
178
|
+
"""The JCS-canonicalizable form of this definition — drives definition_digest."""
|
|
179
|
+
out: dict[str, Any] = {
|
|
180
|
+
"fold_id": self.fold_id,
|
|
181
|
+
"reads": [
|
|
182
|
+
{
|
|
183
|
+
"path": r.path,
|
|
184
|
+
"erasure_class": r.erasure_class,
|
|
185
|
+
**({"default": r.default} if r.has_default else {}),
|
|
186
|
+
}
|
|
187
|
+
for r in self.reads
|
|
188
|
+
],
|
|
189
|
+
"reduce": {
|
|
190
|
+
"reducer": self.reduce.reducer,
|
|
191
|
+
**({"field": self.reduce.field} if self.reduce.field else {}),
|
|
192
|
+
},
|
|
193
|
+
"emit": self.emit,
|
|
194
|
+
}
|
|
195
|
+
if self.filter:
|
|
196
|
+
out["filter"] = [{"field": f.field, "op": f.op, "value": f.value} for f in self.filter]
|
|
197
|
+
if self.key is not None:
|
|
198
|
+
out["key"] = self.key
|
|
199
|
+
if self.window is not None:
|
|
200
|
+
w: dict[str, Any] = {"mode": self.window.mode}
|
|
201
|
+
if self.window.duration is not None:
|
|
202
|
+
w["duration"] = self.window.duration
|
|
203
|
+
if self.window.start is not None:
|
|
204
|
+
w["start"] = self.window.start
|
|
205
|
+
if self.window.end is not None:
|
|
206
|
+
w["end"] = self.window.end
|
|
207
|
+
out["window"] = w
|
|
208
|
+
if self.reversal is not None:
|
|
209
|
+
out["reversal"] = {
|
|
210
|
+
"class_field": self.reversal.class_field,
|
|
211
|
+
"classes": list(self.reversal.classes),
|
|
212
|
+
"parent_field": self.reversal.parent_field,
|
|
213
|
+
"relation_field": self.reversal.relation_field,
|
|
214
|
+
"relation": self.reversal.relation,
|
|
215
|
+
}
|
|
216
|
+
return out
|
|
217
|
+
|
|
218
|
+
def definition_digest(self) -> str:
|
|
219
|
+
"""SHA-256 over the JCS bytes of the canonical definition (spec §2)."""
|
|
220
|
+
try:
|
|
221
|
+
return json_digest(self.canonical_dict())
|
|
222
|
+
except FloatInDigestError as exc:
|
|
223
|
+
raise FoldDefinitionError(FLOAT_IN_DEFINITION, str(exc)) from exc
|
|
224
|
+
except UnsafeIntegerError as exc:
|
|
225
|
+
raise FoldDefinitionError(UNSAFE_INTEGER_IN_DEFINITION, str(exc)) from exc
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _reject_wall_clock(path: str) -> None:
|
|
229
|
+
for part in path.split("."):
|
|
230
|
+
if part in WALL_CLOCK_TOKENS:
|
|
231
|
+
raise FoldDefinitionError(
|
|
232
|
+
WALL_CLOCK_REFERENCE,
|
|
233
|
+
f"reads path {path!r} references a wall-clock/output-only pseudo-field {part!r}; "
|
|
234
|
+
"folds MUST NOT consult a wall clock or anything outside the declared capsule "
|
|
235
|
+
"range (spec §3 rule 1)",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _reject_float(value: Any, context: str) -> None:
|
|
240
|
+
if isinstance(value, float):
|
|
241
|
+
raise FoldDefinitionError(FLOAT_IN_DEFINITION, f"{context} is a float ({value!r}); floats are forbidden (spec §3 rule 2)")
|
|
242
|
+
if isinstance(value, list):
|
|
243
|
+
for item in value:
|
|
244
|
+
_reject_float(item, context)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def parse_definition(data: Any) -> FoldDefinition:
|
|
248
|
+
"""Validate a plain dict (as loaded from YAML) into a ``FoldDefinition``."""
|
|
249
|
+
if not isinstance(data, dict):
|
|
250
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, "definition must be a mapping")
|
|
251
|
+
|
|
252
|
+
fold_id = data.get("fold_id")
|
|
253
|
+
if not isinstance(fold_id, str) or not FOLD_ID_RE.match(fold_id):
|
|
254
|
+
raise FoldDefinitionError(
|
|
255
|
+
INVALID_FOLD_ID,
|
|
256
|
+
f"fold_id {fold_id!r} must match '<namespace>[.<namespace>...]/<major>.<minor>.<patch>' "
|
|
257
|
+
"(e.g. 'spend.weekly/1.0.0')",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
raw_reads = data.get("reads")
|
|
261
|
+
if not isinstance(raw_reads, list) or not raw_reads:
|
|
262
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, "reads must be a non-empty list")
|
|
263
|
+
|
|
264
|
+
reads: list[ReadField] = []
|
|
265
|
+
seen_paths: set[str] = set()
|
|
266
|
+
for entry in raw_reads:
|
|
267
|
+
if not isinstance(entry, dict) or "path" not in entry:
|
|
268
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, f"each reads entry needs a 'path': {entry!r}")
|
|
269
|
+
path = entry["path"]
|
|
270
|
+
try:
|
|
271
|
+
validate_path(path)
|
|
272
|
+
except ValueError as exc:
|
|
273
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, str(exc)) from exc
|
|
274
|
+
_reject_wall_clock(path)
|
|
275
|
+
if path in seen_paths:
|
|
276
|
+
raise FoldDefinitionError(DUPLICATE_READ_PATH, f"path {path!r} declared more than once in reads")
|
|
277
|
+
seen_paths.add(path)
|
|
278
|
+
erasure_class = entry.get("erasure_class")
|
|
279
|
+
if erasure_class not in ERASURE_CLASSES:
|
|
280
|
+
raise FoldDefinitionError(
|
|
281
|
+
UNKNOWN_ERASURE_CLASS,
|
|
282
|
+
f"reads[{path!r}].erasure_class must be one of {sorted(ERASURE_CLASSES)}, got {erasure_class!r}",
|
|
283
|
+
)
|
|
284
|
+
has_default = "default" in entry
|
|
285
|
+
if has_default:
|
|
286
|
+
_reject_float(entry["default"], f"reads[{path!r}].default")
|
|
287
|
+
reads.append(
|
|
288
|
+
ReadField(path=path, erasure_class=erasure_class, default=entry.get("default"), has_default=has_default)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
declared = {r.path for r in reads}
|
|
292
|
+
|
|
293
|
+
raw_filter = data.get("filter") or []
|
|
294
|
+
if not isinstance(raw_filter, list):
|
|
295
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, "filter must be a list")
|
|
296
|
+
filters: list[FilterClause] = []
|
|
297
|
+
for clause in raw_filter:
|
|
298
|
+
if not isinstance(clause, dict):
|
|
299
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, f"filter clause must be a mapping: {clause!r}")
|
|
300
|
+
f_field, op, value = clause.get("field"), clause.get("op"), clause.get("value")
|
|
301
|
+
if op not in BOUNDED_FILTER_OPS:
|
|
302
|
+
raise FoldDefinitionError(
|
|
303
|
+
UNBOUNDED_FILTER_OP,
|
|
304
|
+
f"filter op {op!r} is not a bounded operation; allowed: {sorted(BOUNDED_FILTER_OPS)} "
|
|
305
|
+
"(spec §2: no regex, no user-supplied code)",
|
|
306
|
+
)
|
|
307
|
+
if f_field not in declared:
|
|
308
|
+
raise FoldDefinitionError(UNDECLARED_FIELD_READ, f"filter references undeclared field {f_field!r}")
|
|
309
|
+
if op in ("in", "not_in") and not isinstance(value, list):
|
|
310
|
+
raise FoldDefinitionError(UNBOUNDED_FILTER_OP, f"filter op {op!r} requires a list value")
|
|
311
|
+
_reject_float(value, f"filter[{f_field!r}].value")
|
|
312
|
+
filters.append(FilterClause(field=f_field, op=op, value=value))
|
|
313
|
+
|
|
314
|
+
key = data.get("key")
|
|
315
|
+
if key is not None:
|
|
316
|
+
if not isinstance(key, str):
|
|
317
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, "key must be a string field path")
|
|
318
|
+
if key not in declared:
|
|
319
|
+
raise FoldDefinitionError(UNDECLARED_FIELD_READ, f"key references undeclared field {key!r}")
|
|
320
|
+
|
|
321
|
+
window: Window | None = None
|
|
322
|
+
raw_window = data.get("window")
|
|
323
|
+
if raw_window is not None:
|
|
324
|
+
if not isinstance(raw_window, dict) or "mode" not in raw_window:
|
|
325
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, "window must be a mapping with a 'mode'")
|
|
326
|
+
mode = raw_window["mode"]
|
|
327
|
+
if mode == "rolling":
|
|
328
|
+
duration = raw_window.get("duration")
|
|
329
|
+
if not isinstance(duration, str):
|
|
330
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, "window.duration is required for mode 'rolling'")
|
|
331
|
+
try:
|
|
332
|
+
parse_duration_seconds(duration)
|
|
333
|
+
except ValueError as exc:
|
|
334
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, str(exc)) from exc
|
|
335
|
+
window = Window(mode="rolling", duration=duration)
|
|
336
|
+
elif mode == "explicit":
|
|
337
|
+
start, end = raw_window.get("start"), raw_window.get("end")
|
|
338
|
+
if not isinstance(start, str) or not isinstance(end, str):
|
|
339
|
+
raise FoldDefinitionError(
|
|
340
|
+
MALFORMED_DEFINITION, "window.start/end (ISO-8601 timestamp strings) are required for mode 'explicit'"
|
|
341
|
+
)
|
|
342
|
+
window = Window(mode="explicit", start=start, end=end)
|
|
343
|
+
else:
|
|
344
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, f"window.mode {mode!r} must be 'rolling' or 'explicit'")
|
|
345
|
+
if "timestamp" not in declared:
|
|
346
|
+
raise FoldDefinitionError(
|
|
347
|
+
UNDECLARED_FIELD_READ,
|
|
348
|
+
"a window is evaluated against record timestamps (spec §2); declare 'timestamp' in reads",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
raw_reduce = data.get("reduce")
|
|
352
|
+
if not isinstance(raw_reduce, dict) or "reducer" not in raw_reduce:
|
|
353
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, "reduce.reducer is required")
|
|
354
|
+
reducer = raw_reduce["reducer"]
|
|
355
|
+
if reducer not in KNOWN_REDUCERS:
|
|
356
|
+
raise FoldDefinitionError(
|
|
357
|
+
UNKNOWN_REDUCER,
|
|
358
|
+
f"reducer {reducer!r} is not in the closed registered set {sorted(KNOWN_REDUCERS)} "
|
|
359
|
+
"(e.g. distinct_count is deliberately excluded — spec §7 open question 1)",
|
|
360
|
+
)
|
|
361
|
+
reduce_field = raw_reduce.get("field")
|
|
362
|
+
if reducer in FIELD_REDUCERS:
|
|
363
|
+
if not isinstance(reduce_field, str):
|
|
364
|
+
raise FoldDefinitionError(MISSING_REDUCE_FIELD, f"reducer {reducer!r} requires reduce.field")
|
|
365
|
+
if reduce_field not in declared:
|
|
366
|
+
raise FoldDefinitionError(UNDECLARED_FIELD_READ, f"reduce.field {reduce_field!r} is not declared in reads")
|
|
367
|
+
elif reduce_field is not None:
|
|
368
|
+
raise FoldDefinitionError(MISSING_REDUCE_FIELD, f"reducer {reducer!r} does not take a reduce.field")
|
|
369
|
+
|
|
370
|
+
emit = data.get("emit")
|
|
371
|
+
if not isinstance(emit, str) or not emit:
|
|
372
|
+
raise FoldDefinitionError(MALFORMED_DEFINITION, "emit is required and must be a non-empty string")
|
|
373
|
+
|
|
374
|
+
# derivation_class: optional on the wire (defaults to deterministic so every
|
|
375
|
+
# existing catalog YAML parses unchanged). When present it MUST be one of the
|
|
376
|
+
# neutral core's classes — validated against the re-imported vocabulary, not
|
|
377
|
+
# a ledger-local copy.
|
|
378
|
+
derivation_class = data.get("derivation_class", DEFAULT_DERIVATION_CLASS)
|
|
379
|
+
if derivation_class not in DERIVATION_CLASSES:
|
|
380
|
+
raise FoldDefinitionError(
|
|
381
|
+
UNKNOWN_DERIVATION_CLASS,
|
|
382
|
+
f"derivation_class {derivation_class!r} must be one of {sorted(DERIVATION_CLASSES)}",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
raw_reversal = data.get("reversal")
|
|
386
|
+
reversal = None if raw_reversal is None else _parse_reversal(raw_reversal, declared, reducer)
|
|
387
|
+
|
|
388
|
+
return FoldDefinition(
|
|
389
|
+
fold_id=fold_id,
|
|
390
|
+
reads=tuple(reads),
|
|
391
|
+
filter=tuple(filters),
|
|
392
|
+
key=key,
|
|
393
|
+
window=window,
|
|
394
|
+
reduce=Reduce(reducer=reducer, field=reduce_field),
|
|
395
|
+
emit=emit,
|
|
396
|
+
derivation_class=derivation_class,
|
|
397
|
+
reversal=reversal,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _parse_reversal(raw: Any, declared: set[str], reducer: str) -> Reversal:
|
|
402
|
+
if not isinstance(raw, dict):
|
|
403
|
+
raise FoldDefinitionError(INVALID_REVERSAL, "reversal must be a mapping")
|
|
404
|
+
if reducer != "sum":
|
|
405
|
+
raise FoldDefinitionError(INVALID_REVERSAL, f"a reversal takes back part of a sum; reducer {reducer!r} is not sum")
|
|
406
|
+
fields: dict[str, str] = {}
|
|
407
|
+
for name in ("class_field", "parent_field", "relation_field"):
|
|
408
|
+
path = raw.get(name)
|
|
409
|
+
if not isinstance(path, str) or path not in declared:
|
|
410
|
+
raise FoldDefinitionError(UNDECLARED_FIELD_READ, f"reversal.{name} {path!r} is undeclared in reads")
|
|
411
|
+
fields[name] = path
|
|
412
|
+
classes = raw.get("classes")
|
|
413
|
+
if not isinstance(classes, list) or not classes or not all(isinstance(c, str) and c for c in classes):
|
|
414
|
+
raise FoldDefinitionError(INVALID_REVERSAL, "reversal.classes must be a non-empty list of class names")
|
|
415
|
+
relation = raw.get("relation")
|
|
416
|
+
if relation not in REVERSAL_RELATIONS:
|
|
417
|
+
raise FoldDefinitionError(
|
|
418
|
+
INVALID_REVERSAL,
|
|
419
|
+
f"reversal.relation {relation!r} must be a registered terminal chain relation: {sorted(REVERSAL_RELATIONS)}",
|
|
420
|
+
)
|
|
421
|
+
return Reversal(classes=tuple(classes), relation=relation, **fields)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Duration string parsing for rolling windows (spec §2: "rolling duration")."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
_DURATION_RE = re.compile(r"^(\d+)([smhd])$")
|
|
8
|
+
_UNIT_SECONDS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_duration_seconds(duration: str) -> int:
|
|
12
|
+
match = _DURATION_RE.match(duration)
|
|
13
|
+
if not match:
|
|
14
|
+
raise ValueError(f"duration {duration!r} must match '<int>[smhd]' (e.g. '7d', '24h')")
|
|
15
|
+
count, unit = match.groups()
|
|
16
|
+
return int(count) * _UNIT_SECONDS[unit]
|