Status: experimental C9-H implementation contract
Issue: #597 (slice C9-H), parent #570
Authority: advisory only; it deletes, expires, compacts, rewrites, dispatches, verifies, accepts, integrates, pushes and merges nothing.
GitHub Actions artifacts, logs and caches have finite retention; the dedicated
ledger branch (C9-G) does not expire.
#597 requires that “essential ResultManifest/VerificationResult references
survive Actions-run expiration” and that Actions artifacts are “never the only
copy of evidence required for later interpretation/replay”. This slice states
that as a policy and as checks. Code: idkmesh/github_ledger_retention.py.
The numbers in the policy (minimum age, size thresholds) are IDKMesh policy values. They are not claims about what GitHub itself retains or enforces.
| Class | Kinds | Rule |
|---|---|---|
| Durable | ledger_record, admission_record, candidate_reference, result_manifest, verification_result, human_decision_record |
retained for the life of the ledger; never expired, compacted or rewritten |
| Transient | actions_artifact, actions_log, actions_cache, provider_transcript, raw_candidate_bundle |
may expire, only under the rules below |
Any other kind is unclassified and is retained (fail closed).
check_run_retention(state, policy) takes a RecoveredRunState
(C9-E) and returns a RetentionReport whose
findings are empty only when the run carries what its state requires. The
requirements follow the Product Spine invariants:
| State reached | Required reference |
|---|---|
candidate_observed and later |
candidate_reference |
normalized and later |
result_manifest |
evidence_ready, awaiting_human_decision, decided |
at least one verification_result |
decided |
human_decision_record |
cancelled and admission_blocked runs require nothing further, but any
reference they carry is still checked. Failures and cancellations are never a
finding: negative evidence stays visible.
Every carried reference is checked, so that the compact copy is enough once the raw one is gone:
| Finding | Meaning |
|---|---|
malformed_digest |
the digest is not sha256: plus 64 lowercase hex characters |
unlocatable_reference |
a kind in require_storage_path (default: result_manifest, verification_result, human_decision_record) has a digest but no repository-relative path |
unsafe_path |
the path fails the same safe-path rules as the C14-C durable evidence link (no absolute, empty, ./.. segments, backslashes, control characters) |
transient_locator |
the path contains a transient runner segment (default _temp, _work, _actions, actions-artifacts, artifacts, runner, tmp, cache), so it points into a location that does not outlive the runner |
transient_locator is a path-shape heuristic, not proof of where a file lives;
the segment list is part of the policy. A digest or reference is not a verdict.
Findings carry the slot name and a fixed message and never echo the offending
value.
plan_artifact_expiry(artifacts, referenced_digests=, durable_digests=, now=, policy=, active_run_ids=)
returns one ExpiryDecision per raw artifact. It never expires anything; the
caller owns deletion. now is passed in (no hidden clock) and must be timezone
aware. The first matching rule wins:
| Decision | Condition |
|---|---|
retain_durable_kind |
the kind is durable |
retain_unclassified |
the kind is not a known transient kind |
retain_active_run |
its run_id is in active_run_ids (still being observed) |
retain_sole_copy |
its digest is referenced by the ledger and is not in durable_digests |
retain_young |
younger than transient_min_age_days (default 30), or created in the future |
may_expire |
none of the above |
durable_digests is the subset of referenced digests the caller has confirmed
present in the durable store. Confirming that is the caller’s job; this module
only refuses to let a raw artifact be the sole copy of referenced evidence.
assess_ledger_size(total_bytes, record_count, policy) compares ledger size with
ledger_warn_bytes (default 100 MiB) and ledger_limit_bytes (default 500 MiB)
and returns ok, warn or exceeded with the only remedy the policy offers:
none, plan_new_shard, or operator_decision_new_shard. Deleting, compacting
or rewriting ledger history is never a remedy; C9-C’s linear, never-rewritten
history and #597’s “no history rewrite to hide failed/cancelled attempts” stand.
Sharding itself is not implemented here.
RetentionPolicy.from_mapping is strict: schema_version must be
github-ledger-retention-policy-v0.1, unknown keys are refused, integers are
bounded (bool is not an integer), require_storage_path may name only durable
kinds, and the warn threshold must be below the limit. There is no policy file on
disk yet; the defaults are in code.
This slice does not delete or expire any artifact, call the GitHub API, read a clock, confirm that a path actually exists in the ledger branch, implement ledger sharding, or grant any authority. It also does not prove provider-side evidence retention beyond the compact references held in ledger records. Wiring these checks into a scheduled workflow is future work and would need its own authority review.