qp-vault models knowledge as a living system. Documents are not static files; they have lifecycles with creation, approval, activation, supersession, expiration, and archival.
DRAFT ----> REVIEW ----> ACTIVE ----> SUPERSEDED ----> ARCHIVED
| | |
| | EXPIRED ----> ACTIVE (re-activate)
| | | |
+---> ARCHIVED +---> ARCHIVED +---> ARCHIVED
| From | To | Description |
|---|---|---|
| DRAFT | REVIEW | Submit for review |
| DRAFT | ACTIVE | Skip review (emergency publish) |
| DRAFT | ARCHIVED | Abandon draft |
| REVIEW | ACTIVE | Approve |
| REVIEW | DRAFT | Send back for revision |
| REVIEW | ARCHIVED | Reject permanently |
| ACTIVE | SUPERSEDED | Replaced by newer version |
| ACTIVE | EXPIRED | Past valid_until date |
| ACTIVE | ARCHIVED | Retire manually |
| SUPERSEDED | ARCHIVED | Archive superseded version |
| EXPIRED | ACTIVE | Re-activate (extend validity) |
| EXPIRED | ARCHIVED | Archive expired version |
| ARCHIVED | (none) | Terminal state |
from qp_vault import Vault, Lifecycle
vault = Vault("./knowledge")
# Create a draft
policy = vault.add(
"Security policy for remote access...",
name="security-policy-v2.md",
trust_tier="canonical",
lifecycle="draft",
)
# Move through review
vault.transition(policy.id, "review", reason="Ready for security team review")
vault.transition(policy.id, "active")Invalid transitions raise LifecycleError:
# ACTIVE -> DRAFT is not allowed
try:
vault.transition(active_resource.id, "draft")
except LifecycleError as e:
print(e) # "Cannot transition from active to draft. Allowed: superseded, expired, archived"When a newer version replaces an older one:
v1 = vault.add("Policy v1", name="policy-v1.md", trust_tier="canonical")
v2 = vault.add("Policy v2 with PQ crypto", name="policy-v2.md", trust_tier="canonical")
# Supersede: v1 -> SUPERSEDED, linked to v2
old, new = vault.supersede(v1.id, v2.id)
assert old.lifecycle == "superseded"
assert old.superseded_by == v2.id
assert new.supersedes == v1.idWalk the full version history:
chain = vault.chain(v1.id)
# Returns: [v1, v2, v3, ...] in chronological order
for version in chain:
print(f"{version.name} [{version.lifecycle.value}]")Chains are walked both directions (via supersedes and superseded_by pointers) and have cycle protection (max 1000 links).
Resources can have time windows during which they are considered authoritative:
from datetime import date
vault.add(
"Q4 2025 budget allocation",
name="budget-q4-2025.md",
trust_tier="canonical",
valid_from=date(2025, 10, 1),
valid_until=date(2025, 12, 31),
)Query what was active at a specific date:
# "What was our budget policy on November 15, 2025?"
results = vault.search("budget allocation", as_of=date(2025, 11, 15))Find resources about to expire:
# What's expiring in the next 90 days?
expiring = vault.expiring(days=90)
for r in expiring:
print(f"{r.name} expires {r.valid_until}")Resources with valid_until in the past are automatically transitioned to EXPIRED when check_expirations() runs:
expired = await vault._lifecycle.check_expirations()Every lifecycle transition emits a LIFECYCLE_TRANSITION VaultEvent:
{
"event_type": "lifecycle_transition",
"resource_id": "abc-123",
"details": {
"from": "draft",
"to": "active",
"reason": "Approved by security team"
}
}Supersession emits both LIFECYCLE_TRANSITION (for the state change) and SUPERSEDE (for the pointer linkage).