affaan-m/ECC · error · ContractError

CV effect must fail closed on anchor loss

Error message

CV effect {event.get('effect')} must fail closed on anchor loss

What it means

ContractError (f-string) raised when the anchor's lost_policy is not exactly 'disable_effect_until_track_recovers'. The contract mandates fail-closed behavior: when the subject track is lost, the CV effect must be disabled until the track recovers, rather than continuing or guessing.

Solutions

  1. Set lost_policy to exactly "disable_effect_until_track_recovers".
  2. Remove custom loss-handling fields and rely on the contract's single supported policy.
  3. Update recipe templates/generators to hardcode the required policy string.
  4. Catch ContractError and log the offending policy value for the recipe author.

Example fix

// before
anchor = {..., "lost_policy": "keep_last_position"}
// after
anchor = {..., "lost_policy": "disable_effect_until_track_recovers"}
Defensive patterns

Strategy: validation

Validate before calling

LOST_POLICY = "disable_effect_until_track_recovers"
if anchor.get("lost_policy") != LOST_POLICY:
    raise ValueError(f"lost_policy must be {LOST_POLICY!r}")

Type guard

def fails_closed_on_anchor_loss(anchor) -> bool:
    return isinstance(anchor, dict) and anchor.get("lost_policy") == "disable_effect_until_track_recovers"

Try / catch

try:
    validate_effect_recipe(recipe, reference_durations)
except ContractError as exc:
    if "fail closed on anchor loss" in str(exc):
        anchor["lost_policy"] = "disable_effect_until_track_recovers"
    else:
        raise

Prevention

When it happens

Trigger: Calling validate_effect_recipe with an anchor whose lost_policy is e.g. "keep_last_known", "continue", "ignore", "drop_event", or missing/null.

Common situations: Copying anchor configs from systems with lenient loss handling; assuming any descriptive policy string is valid; older drafts of the recipe format that allowed multiple policies.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/2e7ef9dc6f98f09e. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/contract.py:242

            raise ContractError("stochastic schedule is periodic; repeating interval cycle")
    if recipe.get("periodic") is not False:
        raise ContractError("effect recipe must explicitly declare periodic=false")
    for event in events:
        cv_effect = str(event.get("effect", "")).startswith("cv_")
        if cv_effect and event.get("requires_subject_anchor") is not True:
            raise ContractError(f"CV effect {event.get('effect')} must require a subject anchor")
        if event.get("requires_subject_anchor"):
            anchor = event.get("subject_anchor")
            required = {
                "mode", "target", "source_ref_sha256", "evidence_time",
                "source_duration", "lost_policy",
            }
            if not isinstance(anchor, dict) or not required.issubset(anchor):
                raise ContractError(f"CV effect {event.get('effect')} lacks a valid subject anchor")
            if anchor.get("mode") not in {"object_track", "point_track", "segmentation_track"}:
                raise ContractError(f"CV effect {event.get('effect')} has an invalid subject anchor")
            if anchor.get("lost_policy") != "disable_effect_until_track_recovers":
                raise ContractError(f"CV effect {event.get('effect')} must fail closed on anchor loss")
            _validate_media_time(
                anchor.get("evidence_time"), anchor.get("source_duration"),
                label="anchor evidence time",
            )
            if reference_durations is not None:
                digest = anchor.get("source_ref_sha256")
                expected_duration = reference_durations.get(digest) if isinstance(digest, str) else None
                if expected_duration is None or anchor.get("source_duration") != expected_duration:
                    raise ContractError("anchor evidence source duration is not bound to its receipt reference")
    for event in events:
        placement = event.get("placement")
        if not isinstance(placement, dict) or not {"safe_area", "max_coverage", "occlusion_policy"}.issubset(placement):
            raise ContractError(f"effect {event.get('effect')} lacks placement constraints")


def validate_provenance(payload: dict[str, Any]) -> None:
    """Require every declared rule to cite immutable, timestamped evidence."""
    rules = payload.get("rules")

View on GitHub (pinned to 8321021c54)