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,504 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Evidence Result v0 emitter -- ``report/`` -> ``result`` projection.
|
|
3
|
+
|
|
4
|
+
The wire shape and every closed vocabulary below mirror
|
|
5
|
+
``agent-action-capsule``'s ``spec/evidence-result-v0.md`` /
|
|
6
|
+
``schemas/evidence-result-v0.json`` field-for-field -- that document governs,
|
|
7
|
+
this module is the encoding. A drift between the two is a defect here, never
|
|
8
|
+
a second legitimate spelling. At the time this module was written, that
|
|
9
|
+
schema had landed on an ``agent-action-capsule`` branch (ruled, no longer
|
|
10
|
+
DRAFT) but was not yet on its main branch -- not yet importable from the ``agent-action-capsule``
|
|
11
|
+
dependency this repo already pins -- so ``capsule_engine/schemas/vendor/
|
|
12
|
+
evidence-result-v0.json`` carries a vendored copy (see
|
|
13
|
+
``capsule_engine/schemas/vendor/README.md`` for its
|
|
14
|
+
provenance and the drop-vendoring-once-shipped note).
|
|
15
|
+
|
|
16
|
+
**Claims never self-declare.** Exactly like ``EvidenceContract`` upstream
|
|
17
|
+
(``capsule_engine/schemas/evidence-contract-v0.json``'s ``$comment``: "the evidence record
|
|
18
|
+
never self-declares that it satisfies a requirement"), nothing in this module
|
|
19
|
+
computes sufficiency or verdict from data it also emits as evidence -- both
|
|
20
|
+
are supplied by the caller, who is expected to have derived them from real
|
|
21
|
+
Evidence Contract evaluation (a fold's own verdict, or a capsule-judge
|
|
22
|
+
verdict record). This module's job is projection and validation: build a
|
|
23
|
+
schema-shaped ``Claim``/``EvidenceResult``, refuse to construct one that
|
|
24
|
+
violates the ruled vocabulary rules, and compute the aggregate (coverage +
|
|
25
|
+
buckets) FROM the claims actually built -- never accept a caller-supplied
|
|
26
|
+
aggregate. See ``result_from_folds.py`` for the adapters that produce claims
|
|
27
|
+
from this repo's own deterministic folds.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import re
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from importlib import resources
|
|
35
|
+
from typing import TYPE_CHECKING, Any
|
|
36
|
+
|
|
37
|
+
import jsonschema
|
|
38
|
+
|
|
39
|
+
from .errors import (
|
|
40
|
+
BUCKET_CLAIM_MISMATCH,
|
|
41
|
+
COVERAGE_CLAIM_MISMATCH,
|
|
42
|
+
DISCLOSURE_NOT_LEGAL_FOR_STATUS,
|
|
43
|
+
DUPLICATE_CLAIM_ID,
|
|
44
|
+
INVALID_DISCLOSED_STATUS,
|
|
45
|
+
INVALID_EVIDENCE_STATUS,
|
|
46
|
+
INVALID_GRADE,
|
|
47
|
+
INVALID_HEX_DIGEST,
|
|
48
|
+
INVALID_SUFFICIENCY,
|
|
49
|
+
INVALID_TIER,
|
|
50
|
+
INVALID_VERDICT,
|
|
51
|
+
SUFFICIENCY_VERDICT_MISMATCH,
|
|
52
|
+
ResultError,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if TYPE_CHECKING:
|
|
56
|
+
from .coverage import CoverageReport
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
"RESULT_VERSION",
|
|
60
|
+
"TIER_VALUES",
|
|
61
|
+
"GRADE_VALUES",
|
|
62
|
+
"SUFFICIENCY_VALUES",
|
|
63
|
+
"VERDICT_VALUES",
|
|
64
|
+
"EVIDENCE_STATUS_VALUES",
|
|
65
|
+
"DISCLOSED_STATUS_VALUES",
|
|
66
|
+
"DigestRef",
|
|
67
|
+
"ProofRef",
|
|
68
|
+
"DisclosureCarrier",
|
|
69
|
+
"AnalysisCarrier",
|
|
70
|
+
"StoryCarrier",
|
|
71
|
+
"Presentation",
|
|
72
|
+
"Claim",
|
|
73
|
+
"Coverage",
|
|
74
|
+
"Buckets",
|
|
75
|
+
"Aggregate",
|
|
76
|
+
"View",
|
|
77
|
+
"EvidenceResult",
|
|
78
|
+
"build_result",
|
|
79
|
+
"SCHEMA_PATH",
|
|
80
|
+
"load_schema",
|
|
81
|
+
"validate_against_schema",
|
|
82
|
+
"verify_result",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
RESULT_VERSION = "evidence-result-v0"
|
|
86
|
+
|
|
87
|
+
# Closed vocabularies -- spec/evidence-result-v0.md sections 1, 2, 4, 6.
|
|
88
|
+
TIER_VALUES = frozenset({"recomputed", "judged"})
|
|
89
|
+
GRADE_VALUES = frozenset({"self-attested", "witnessed", "countersigned"})
|
|
90
|
+
SUFFICIENCY_VALUES = frozenset({"SATISFIED", "GAP", "INSUFFICIENT", "UNKNOWN"})
|
|
91
|
+
VERDICT_VALUES = frozenset({"met", "not_met", "not_evaluable"})
|
|
92
|
+
EVIDENCE_STATUS_VALUES = frozenset(
|
|
93
|
+
{"SATISFIED", "INSUFFICIENT", "NOT_FOUND", "NOT_COMMITTED", "WITHHELD", "CONTRADICTED", "NOT_APPLICABLE", "UNKNOWN"}
|
|
94
|
+
)
|
|
95
|
+
# EvidenceStatus minus the two the disclosure policy reserves for analysis/story (spec section 2).
|
|
96
|
+
DISCLOSED_STATUS_VALUES = EVIDENCE_STATUS_VALUES - {"WITHHELD", "NOT_COMMITTED"}
|
|
97
|
+
|
|
98
|
+
_HEX_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
99
|
+
|
|
100
|
+
# Resolved inside the installed package, so it is present in a wheel install
|
|
101
|
+
# and not only in a source checkout (scripts/clean_room_wheel.sh checks this).
|
|
102
|
+
SCHEMA_PATH = resources.files("capsule_engine") / "schemas" / "vendor" / "evidence-result-v0.json"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def load_schema() -> dict[str, Any]:
|
|
106
|
+
return json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _check_hex_digest(digest: str) -> None:
|
|
110
|
+
if not _HEX_DIGEST_RE.fullmatch(digest):
|
|
111
|
+
raise ResultError(INVALID_HEX_DIGEST, f"{digest!r} is not 64 lowercase hex characters")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class DigestRef:
|
|
116
|
+
"""Evidence-by-digest-only ref (spec section 5) -- never inline bytes."""
|
|
117
|
+
|
|
118
|
+
digest: str
|
|
119
|
+
digest_alg: str = "SHA-256"
|
|
120
|
+
|
|
121
|
+
def __post_init__(self) -> None:
|
|
122
|
+
if self.digest_alg != "SHA-256":
|
|
123
|
+
raise ResultError(INVALID_HEX_DIGEST, f"digest_alg must be 'SHA-256', got {self.digest_alg!r}")
|
|
124
|
+
_check_hex_digest(self.digest)
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> dict:
|
|
127
|
+
return {"digest_alg": self.digest_alg, "digest": self.digest}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class ProofRef:
|
|
132
|
+
"""An inclusion-proof/receipt ref, by digest only (spec section 5)."""
|
|
133
|
+
|
|
134
|
+
kind: str # "inclusion_proof" | "receipt"
|
|
135
|
+
digest: str
|
|
136
|
+
digest_alg: str = "SHA-256"
|
|
137
|
+
|
|
138
|
+
def __post_init__(self) -> None:
|
|
139
|
+
if self.kind not in ("inclusion_proof", "receipt"):
|
|
140
|
+
raise ResultError(INVALID_HEX_DIGEST, f"proof kind must be inclusion_proof|receipt, got {self.kind!r}")
|
|
141
|
+
if self.digest_alg != "SHA-256":
|
|
142
|
+
raise ResultError(INVALID_HEX_DIGEST, f"digest_alg must be 'SHA-256', got {self.digest_alg!r}")
|
|
143
|
+
_check_hex_digest(self.digest)
|
|
144
|
+
|
|
145
|
+
def to_dict(self) -> dict:
|
|
146
|
+
return {"kind": self.kind, "digest_alg": self.digest_alg, "digest": self.digest}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True)
|
|
150
|
+
class DisclosureCarrier:
|
|
151
|
+
"""spec section 2/6 -- the claim's evidence, by digest, plus the status
|
|
152
|
+
that licensed showing it. Legal ONLY when ``status`` is not WITHHELD or
|
|
153
|
+
NOT_COMMITTED (the disclosure gate) -- enforced in ``__post_init__``, not
|
|
154
|
+
left to a caller to remember."""
|
|
155
|
+
|
|
156
|
+
status: str
|
|
157
|
+
evidence: tuple[DigestRef, ...] = ()
|
|
158
|
+
kind: str = field(default="disclosure", init=False)
|
|
159
|
+
|
|
160
|
+
def __post_init__(self) -> None:
|
|
161
|
+
if self.status not in DISCLOSED_STATUS_VALUES:
|
|
162
|
+
raise ResultError(
|
|
163
|
+
INVALID_DISCLOSED_STATUS,
|
|
164
|
+
f"disclosure carrier status must be one of {sorted(DISCLOSED_STATUS_VALUES)}, got {self.status!r}",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def to_dict(self) -> dict:
|
|
168
|
+
return {"kind": "disclosure", "status": self.status, "evidence": [e.to_dict() for e in self.evidence]}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class AnalysisCarrier:
|
|
173
|
+
"""spec section 2/6 -- a derived characterization, never a quote of the
|
|
174
|
+
underlying payload. Accepts any of the eight EvidenceStatus values."""
|
|
175
|
+
|
|
176
|
+
status: str
|
|
177
|
+
summary: str
|
|
178
|
+
kind: str = field(default="analysis", init=False)
|
|
179
|
+
|
|
180
|
+
def __post_init__(self) -> None:
|
|
181
|
+
if self.status not in EVIDENCE_STATUS_VALUES:
|
|
182
|
+
raise ResultError(INVALID_EVIDENCE_STATUS, f"status must be one of {sorted(EVIDENCE_STATUS_VALUES)}, got {self.status!r}")
|
|
183
|
+
if not self.summary:
|
|
184
|
+
raise ResultError(INVALID_EVIDENCE_STATUS, "analysis carrier summary must be non-empty")
|
|
185
|
+
|
|
186
|
+
def to_dict(self) -> dict:
|
|
187
|
+
return {"kind": "analysis", "status": self.status, "summary": self.summary}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass(frozen=True)
|
|
191
|
+
class StoryCarrier:
|
|
192
|
+
"""spec section 2/6 -- narrative-only, the weakest carrier."""
|
|
193
|
+
|
|
194
|
+
status: str
|
|
195
|
+
narrative: str
|
|
196
|
+
kind: str = field(default="story", init=False)
|
|
197
|
+
|
|
198
|
+
def __post_init__(self) -> None:
|
|
199
|
+
if self.status not in EVIDENCE_STATUS_VALUES:
|
|
200
|
+
raise ResultError(INVALID_EVIDENCE_STATUS, f"status must be one of {sorted(EVIDENCE_STATUS_VALUES)}, got {self.status!r}")
|
|
201
|
+
if not self.narrative:
|
|
202
|
+
raise ResultError(INVALID_EVIDENCE_STATUS, "story carrier narrative must be non-empty")
|
|
203
|
+
|
|
204
|
+
def to_dict(self) -> dict:
|
|
205
|
+
return {"kind": "story", "status": self.status, "narrative": self.narrative}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
Presentation = DisclosureCarrier | AnalysisCarrier | StoryCarrier
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass(frozen=True)
|
|
212
|
+
class Claim:
|
|
213
|
+
"""spec section 4 -- one requirement of one contract version, evaluated
|
|
214
|
+
once. ``sufficiency``/``verdict`` are never derived here (see module
|
|
215
|
+
docstring) -- only checked for internal consistency (spec section 1's
|
|
216
|
+
rule) at construction time, so a malformed claim never gets built in the
|
|
217
|
+
first place: "an untiered claim is a bug here, not a rendering problem."
|
|
218
|
+
|
|
219
|
+
``contract_ref`` is the compact ``<contract_id>@<version>`` shape
|
|
220
|
+
(evidence-plan-ir-v0.md section 2's convention, mirrored by
|
|
221
|
+
evidence-result-v0.md section 1) -- not two separate
|
|
222
|
+
``contract_id``/``contract_version`` fields; the frozen schema uses
|
|
223
|
+
``contract_ref`` (a rename applied before the schema froze), so this
|
|
224
|
+
module follows the shipped schema.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
id: str
|
|
228
|
+
contract_ref: str
|
|
229
|
+
requirement_ref: str
|
|
230
|
+
tier: str
|
|
231
|
+
grade: str
|
|
232
|
+
sufficiency: str
|
|
233
|
+
verdict: str
|
|
234
|
+
evidence: tuple[DigestRef, ...]
|
|
235
|
+
proofs: tuple[ProofRef, ...]
|
|
236
|
+
presentation: Presentation
|
|
237
|
+
|
|
238
|
+
def __post_init__(self) -> None:
|
|
239
|
+
if not self.id:
|
|
240
|
+
raise ResultError(INVALID_TIER, "claim id must be non-empty")
|
|
241
|
+
if "@" not in self.contract_ref or self.contract_ref.startswith("@") or self.contract_ref.endswith("@"):
|
|
242
|
+
raise ResultError(INVALID_TIER, f"contract_ref must be <contract_id>@<version>, got {self.contract_ref!r}")
|
|
243
|
+
if not self.requirement_ref:
|
|
244
|
+
raise ResultError(INVALID_TIER, "requirement_ref must be non-empty")
|
|
245
|
+
if self.tier not in TIER_VALUES:
|
|
246
|
+
raise ResultError(INVALID_TIER, f"tier must be one of {sorted(TIER_VALUES)}, got {self.tier!r}")
|
|
247
|
+
if self.grade not in GRADE_VALUES:
|
|
248
|
+
raise ResultError(INVALID_GRADE, f"grade must be one of {sorted(GRADE_VALUES)}, got {self.grade!r}")
|
|
249
|
+
if self.sufficiency not in SUFFICIENCY_VALUES:
|
|
250
|
+
raise ResultError(INVALID_SUFFICIENCY, f"sufficiency must be one of {sorted(SUFFICIENCY_VALUES)}, got {self.sufficiency!r}")
|
|
251
|
+
if self.verdict not in VERDICT_VALUES:
|
|
252
|
+
raise ResultError(INVALID_VERDICT, f"verdict must be one of {sorted(VERDICT_VALUES)}, got {self.verdict!r}")
|
|
253
|
+
# spec section 1's rule, restated normatively there from contract v3 section 8:
|
|
254
|
+
# verdict is met/not_met ONLY when sufficiency == SATISFIED; otherwise not_evaluable.
|
|
255
|
+
if self.sufficiency == "SATISFIED":
|
|
256
|
+
if self.verdict not in ("met", "not_met"):
|
|
257
|
+
raise ResultError(
|
|
258
|
+
SUFFICIENCY_VERDICT_MISMATCH,
|
|
259
|
+
f"sufficiency SATISFIED requires verdict met|not_met, got {self.verdict!r}",
|
|
260
|
+
)
|
|
261
|
+
elif self.verdict != "not_evaluable":
|
|
262
|
+
raise ResultError(
|
|
263
|
+
SUFFICIENCY_VERDICT_MISMATCH,
|
|
264
|
+
f"sufficiency {self.sufficiency!r} (not SATISFIED) requires verdict not_evaluable, got {self.verdict!r}",
|
|
265
|
+
)
|
|
266
|
+
# spec section 2's gate: disclosure is not a legal carrier when the
|
|
267
|
+
# carrier's own status is WITHHELD/NOT_COMMITTED -- DisclosureCarrier
|
|
268
|
+
# itself already refuses that status, so this is a second, cheap
|
|
269
|
+
# belt-and-suspenders check against a carrier built via replace()/
|
|
270
|
+
# object.__new__ bypassing __post_init__.
|
|
271
|
+
if isinstance(self.presentation, DisclosureCarrier) and self.presentation.status not in DISCLOSED_STATUS_VALUES:
|
|
272
|
+
raise ResultError(
|
|
273
|
+
DISCLOSURE_NOT_LEGAL_FOR_STATUS,
|
|
274
|
+
f"disclosure carrier illegal for status {self.presentation.status!r}",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def to_dict(self) -> dict:
|
|
278
|
+
return {
|
|
279
|
+
"id": self.id,
|
|
280
|
+
"contract_ref": self.contract_ref,
|
|
281
|
+
"requirement_ref": self.requirement_ref,
|
|
282
|
+
"tier": self.tier,
|
|
283
|
+
"grade": self.grade,
|
|
284
|
+
"sufficiency": self.sufficiency,
|
|
285
|
+
"verdict": self.verdict,
|
|
286
|
+
"evidence": [e.to_dict() for e in self.evidence],
|
|
287
|
+
"proofs": [p.to_dict() for p in self.proofs],
|
|
288
|
+
"presentation": self.presentation.to_dict(),
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@dataclass(frozen=True)
|
|
293
|
+
class Coverage:
|
|
294
|
+
"""spec section 3/7 -- mandatory, computed. Never constructed with
|
|
295
|
+
values the caller invented; see ``build_result``."""
|
|
296
|
+
|
|
297
|
+
evaluated_population: int
|
|
298
|
+
excluded_not_applicable: int
|
|
299
|
+
unknown_count: int
|
|
300
|
+
|
|
301
|
+
def __post_init__(self) -> None:
|
|
302
|
+
for name, value in (
|
|
303
|
+
("evaluated_population", self.evaluated_population),
|
|
304
|
+
("excluded_not_applicable", self.excluded_not_applicable),
|
|
305
|
+
("unknown_count", self.unknown_count),
|
|
306
|
+
):
|
|
307
|
+
if value < 0:
|
|
308
|
+
raise ResultError(INVALID_SUFFICIENCY, f"coverage.{name} must be >= 0, got {value}")
|
|
309
|
+
|
|
310
|
+
def to_dict(self) -> dict:
|
|
311
|
+
return {
|
|
312
|
+
"evaluated_population": self.evaluated_population,
|
|
313
|
+
"excluded_not_applicable": self.excluded_not_applicable,
|
|
314
|
+
"unknown_count": self.unknown_count,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass(frozen=True)
|
|
319
|
+
class Buckets:
|
|
320
|
+
"""spec section 3/7 -- always three keys, each an array. Never
|
|
321
|
+
constructed by a caller; see ``build_result``."""
|
|
322
|
+
|
|
323
|
+
met: tuple[str, ...]
|
|
324
|
+
not_met: tuple[str, ...]
|
|
325
|
+
not_evaluable: tuple[str, ...]
|
|
326
|
+
|
|
327
|
+
def to_dict(self) -> dict:
|
|
328
|
+
return {"met": list(self.met), "not_met": list(self.not_met), "not_evaluable": list(self.not_evaluable)}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@dataclass(frozen=True)
|
|
332
|
+
class Aggregate:
|
|
333
|
+
coverage: Coverage
|
|
334
|
+
buckets: Buckets
|
|
335
|
+
|
|
336
|
+
def to_dict(self) -> dict:
|
|
337
|
+
return {"coverage": self.coverage.to_dict(), "buckets": self.buckets.to_dict()}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@dataclass(frozen=True)
|
|
341
|
+
class View:
|
|
342
|
+
"""spec section 8 -- presentation hints only, never data."""
|
|
343
|
+
|
|
344
|
+
producer_name: str | None = None
|
|
345
|
+
logo_data_url: str | None = None
|
|
346
|
+
title: str | None = None
|
|
347
|
+
spec_version: str = field(default="presentation/v1", init=False)
|
|
348
|
+
|
|
349
|
+
def to_dict(self) -> dict:
|
|
350
|
+
out: dict[str, Any] = {"spec_version": "presentation/v1"}
|
|
351
|
+
if self.producer_name is not None:
|
|
352
|
+
out["producer_name"] = self.producer_name
|
|
353
|
+
if self.logo_data_url is not None:
|
|
354
|
+
out["logo_data_url"] = self.logo_data_url
|
|
355
|
+
if self.title is not None:
|
|
356
|
+
out["title"] = self.title
|
|
357
|
+
return out
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@dataclass(frozen=True)
|
|
361
|
+
class EvidenceResult:
|
|
362
|
+
generated_at: str
|
|
363
|
+
claims: tuple[Claim, ...]
|
|
364
|
+
aggregate: Aggregate
|
|
365
|
+
view: View | None = None
|
|
366
|
+
coverage_report: CoverageReport | None = None
|
|
367
|
+
result_version: str = field(default=RESULT_VERSION, init=False)
|
|
368
|
+
|
|
369
|
+
def to_dict(self) -> dict:
|
|
370
|
+
out: dict[str, Any] = {
|
|
371
|
+
"result_version": RESULT_VERSION,
|
|
372
|
+
"generated_at": self.generated_at,
|
|
373
|
+
"claims": [c.to_dict() for c in self.claims],
|
|
374
|
+
"aggregate": self.aggregate.to_dict(),
|
|
375
|
+
}
|
|
376
|
+
if self.view is not None:
|
|
377
|
+
out["view"] = self.view.to_dict()
|
|
378
|
+
if self.coverage_report is not None:
|
|
379
|
+
out["coverage_report"] = self.coverage_report.to_dict()
|
|
380
|
+
return out
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def build_result(
|
|
384
|
+
claims: list[Claim],
|
|
385
|
+
*,
|
|
386
|
+
generated_at: str,
|
|
387
|
+
excluded_not_applicable: int = 0,
|
|
388
|
+
view: View | None = None,
|
|
389
|
+
coverage_report: CoverageReport | None = None,
|
|
390
|
+
) -> EvidenceResult:
|
|
391
|
+
"""The projection's aggregate step (spec section 3): coverage and
|
|
392
|
+
buckets are COMPUTED from ``claims``, never templated or accepted
|
|
393
|
+
pre-summed from a caller.
|
|
394
|
+
|
|
395
|
+
``excluded_not_applicable`` is the one count that cannot be derived from
|
|
396
|
+
``claims`` -- a requirement excluded as NOT_APPLICABLE never becomes a
|
|
397
|
+
claim at all (spec section 3), so the caller (typically a
|
|
398
|
+
``result_from_folds`` adapter, which sees the excluded requirements
|
|
399
|
+
directly) supplies the count of what it declined to emit as claims.
|
|
400
|
+
Every other coverage/bucket field is counted off ``claims`` itself.
|
|
401
|
+
|
|
402
|
+
``coverage_report`` is the optional per-requirement section built by
|
|
403
|
+
``coverage.build_coverage_report`` over the same claims; it is carried
|
|
404
|
+
as built, and ``verify_result`` re-checks it against the claims.
|
|
405
|
+
"""
|
|
406
|
+
if excluded_not_applicable < 0:
|
|
407
|
+
raise ResultError(INVALID_SUFFICIENCY, f"excluded_not_applicable must be >= 0, got {excluded_not_applicable}")
|
|
408
|
+
|
|
409
|
+
met: list[str] = []
|
|
410
|
+
not_met: list[str] = []
|
|
411
|
+
not_evaluable: list[str] = []
|
|
412
|
+
unknown_count = 0
|
|
413
|
+
seen_ids: set[str] = set()
|
|
414
|
+
for claim in claims:
|
|
415
|
+
if claim.id in seen_ids:
|
|
416
|
+
raise ResultError(DUPLICATE_CLAIM_ID, f"claim id {claim.id!r} appears more than once")
|
|
417
|
+
seen_ids.add(claim.id)
|
|
418
|
+
if claim.verdict == "met":
|
|
419
|
+
met.append(claim.id)
|
|
420
|
+
elif claim.verdict == "not_met":
|
|
421
|
+
not_met.append(claim.id)
|
|
422
|
+
else:
|
|
423
|
+
not_evaluable.append(claim.id)
|
|
424
|
+
if claim.sufficiency == "UNKNOWN":
|
|
425
|
+
unknown_count += 1
|
|
426
|
+
|
|
427
|
+
coverage = Coverage(
|
|
428
|
+
evaluated_population=len(claims),
|
|
429
|
+
excluded_not_applicable=excluded_not_applicable,
|
|
430
|
+
unknown_count=unknown_count,
|
|
431
|
+
)
|
|
432
|
+
buckets = Buckets(met=tuple(met), not_met=tuple(not_met), not_evaluable=tuple(not_evaluable))
|
|
433
|
+
return EvidenceResult(
|
|
434
|
+
generated_at=generated_at,
|
|
435
|
+
claims=tuple(claims),
|
|
436
|
+
aggregate=Aggregate(coverage=coverage, buckets=buckets),
|
|
437
|
+
view=view,
|
|
438
|
+
coverage_report=coverage_report,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def validate_against_schema(doc: dict[str, Any]) -> None:
|
|
443
|
+
"""Validate a Result document against the vendored public JSON Schema
|
|
444
|
+
(item 4: "Validate every emitted Result against the public schema in
|
|
445
|
+
CI"). Raises ``jsonschema.exceptions.ValidationError`` on the first
|
|
446
|
+
violation."""
|
|
447
|
+
schema = load_schema()
|
|
448
|
+
jsonschema.Draft202012Validator(schema).validate(doc)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def verify_result(doc: dict[str, Any]) -> None:
|
|
452
|
+
"""The cross-element checks spec/evidence-result-v0.md section 4 names
|
|
453
|
+
as "normative, not schema-enforced in v0": claim ``id`` uniqueness, and
|
|
454
|
+
every ``aggregate.buckets`` entry naming a claim that actually exists
|
|
455
|
+
with the matching verdict. Then the two coverage counts a reader can
|
|
456
|
+
recompute from the claims: ``evaluated_population`` (the number of
|
|
457
|
+
claims) and ``unknown_count`` (claims whose sufficiency is UNKNOWN).
|
|
458
|
+
``excluded_not_applicable`` is not checked -- excluded requirements
|
|
459
|
+
never become claims, so nothing here can recount it. A missing
|
|
460
|
+
``coverage`` is the schema's to reject. Plus, when the document carries a
|
|
461
|
+
``coverage_report``, ``coverage.verify_coverage_report``'s checks
|
|
462
|
+
against the same claims. Raises ``ResultError`` on the first
|
|
463
|
+
violation. Callers wanting full conformance run this AND
|
|
464
|
+
``validate_against_schema`` -- neither alone is the whole check."""
|
|
465
|
+
claims_by_id: dict[str, dict] = {}
|
|
466
|
+
for claim in doc.get("claims", []):
|
|
467
|
+
claim_id = claim.get("id")
|
|
468
|
+
if claim_id in claims_by_id:
|
|
469
|
+
raise ResultError(DUPLICATE_CLAIM_ID, f"claim id {claim_id!r} appears more than once")
|
|
470
|
+
claims_by_id[claim_id] = claim
|
|
471
|
+
|
|
472
|
+
buckets = doc.get("aggregate", {}).get("buckets", {})
|
|
473
|
+
for verdict, ids in (("met", buckets.get("met", [])), ("not_met", buckets.get("not_met", [])), ("not_evaluable", buckets.get("not_evaluable", []))):
|
|
474
|
+
for claim_id in ids:
|
|
475
|
+
claim = claims_by_id.get(claim_id)
|
|
476
|
+
if claim is None:
|
|
477
|
+
raise ResultError(BUCKET_CLAIM_MISMATCH, f"bucket {verdict!r} names claim id {claim_id!r}, which has no claim")
|
|
478
|
+
if claim.get("verdict") != verdict:
|
|
479
|
+
raise ResultError(
|
|
480
|
+
BUCKET_CLAIM_MISMATCH,
|
|
481
|
+
f"claim {claim_id!r} is in bucket {verdict!r} but its own verdict is {claim.get('verdict')!r}",
|
|
482
|
+
)
|
|
483
|
+
bucketed_ids = {cid for ids in buckets.values() for cid in ids}
|
|
484
|
+
for claim_id, claim in claims_by_id.items():
|
|
485
|
+
if claim_id not in bucketed_ids:
|
|
486
|
+
raise ResultError(BUCKET_CLAIM_MISMATCH, f"claim {claim_id!r} (verdict {claim.get('verdict')!r}) is in no bucket")
|
|
487
|
+
|
|
488
|
+
coverage = doc.get("aggregate", {}).get("coverage")
|
|
489
|
+
if coverage is not None:
|
|
490
|
+
recounted = (
|
|
491
|
+
("evaluated_population", len(claims_by_id)),
|
|
492
|
+
("unknown_count", sum(1 for claim in claims_by_id.values() if claim.get("sufficiency") == "UNKNOWN")),
|
|
493
|
+
)
|
|
494
|
+
for name, count in recounted:
|
|
495
|
+
if coverage.get(name) != count:
|
|
496
|
+
raise ResultError(
|
|
497
|
+
COVERAGE_CLAIM_MISMATCH,
|
|
498
|
+
f"coverage {name} {coverage.get(name)!r} disagrees with the {count} counted from claims",
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
if "coverage_report" in doc:
|
|
502
|
+
from .coverage import verify_coverage_report
|
|
503
|
+
|
|
504
|
+
verify_coverage_report(doc["coverage_report"], claims_by_id)
|