Operator Lifecycle — Incident Substrate Model
Document: operator_lifecycle.md
Path: /docs/Incident_Substrate_Model/operator_lifecycle.md
Revision: RTT/1 · Canon Edition
Status: Authoritative
Issued: 2026-05-20
Trilogy Position#
This document is the third and final member of the ISM canon trilogy:
| # | Document | Scope |
|---|---|---|
| 1 | operator_grammar.md |
Operator contracts: IN/OUT/PRE/POST/GUARD/YIELDS/FAULTS |
| 2 | substrate_errors.md |
Fault token registry: conditions, severities, handler obligations |
| 3 | operator_lifecycle.md |
Record lifespan: creation, versioning, lineage, archival, retention, expiry, purge |
All three documents share a single normative boundary. Where they conflict,
the resolution order is: operator_grammar.md > substrate_errors.md >
operator_lifecycle.md. No document in the trilogy is subordinate to any
document outside it.
Preamble#
operator_grammar.md specifies what operators do. substrate_errors.md
specifies what operators emit when they fail. This document specifies
what happens to the records those operators produce — across their entire
lifespan, from the moment a signal is accepted by incident.ingest through
to the verified, irrevocable deletion of all associated data.
The lifecycle is the substrate's contract with time. Every record is finite. Every record's history is immutable. Every record's deletion is receipted.
1. Lifecycle Phase Model#
An IncidentRecord passes through exactly seven lifecycle phases. Phases
are distinct from record states (defined in operator_grammar.md Section 7)
— states describe what an operator may do to a record now; phases describe
where in its total lifespan a record currently sits.
Phase 1: CREATION Record identity is minted; signal is bound.
Phase 2: VERSIONING Mutable sub-objects accumulate versioned snapshots.
Phase 3: LINEAGE Record is positioned within the incident graph.
Phase 4: ARCHIVAL Record transitions to a terminal state and is sealed.
Phase 5: RETENTION Sealed record is held for a policy-defined window.
Phase 6: EXPIRY Retention window closes; record enters deletion queue.
Phase 7: PURGE All record data is irreversibly destroyed; receipt issued.
Phase transitions are monotonically forward — a record cannot move backward through phases. Phase 3 (LINEAGE) is the only phase that may overlap with Phase 2 (VERSIONING); all other phase boundaries are discrete.
CREATION → VERSIONING → ARCHIVAL → RETENTION → EXPIRY → PURGE
↕
LINEAGE
Phases 1–3 correspond to IncidentRecord.state ∈ {INGESTED, CLASSIFIED, SURFACE_MAPPED, PLAN_DERIVED, PENDING_APPROVAL, APPROVED, EXECUTING, HOLD}.
Phases 4–7 correspond to terminal states {RESOLVED, FAULTED} and beyond.
2. Phase 1 — Record Creation#
2.1 Identity Model#
Every IncidentRecord is assigned a globally unique identity at creation
time. Identity is never reused, even after purge. The identity model comprises
four fields that together form the record's immutable fingerprint:
RecordIdentity {
record_id : UUID -- RFC 4122 v4; substrate-assigned at ingest
signal_id : UUID -- Emitter-assigned; carried from incident.ingest IN(...)
source : String -- Emitter identity; carried from incident.ingest IN(...)
ingested_at : Timestamp -- Substrate-side acceptance time (UTC ISO-8601, ms precision)
}
record_id is the primary key for all substrate operations. signal_id is
the deduplication key for the emitter. The combination (signal_id, source)
is unique within the substrate — the same emitter may not submit the same
signal twice with different outcomes.
RecordIdentity is written once at creation and is thereafter immutable.
No operator, configuration change, or administrative action may alter any
field of RecordIdentity after the record exits Phase 1.
2.2 Creation Gate#
Record creation is governed by the following ordered gate sequence, evaluated
by incident.ingest before any write:
Gate 1: Emitter authorization check → fault: UNAUTHORIZED_EMITTER
Gate 2: Payload size check → fault: PAYLOAD_TOO_LARGE
Gate 3: Content-type support check → fault: UNSUPPORTED_CONTENT_TYPE
Gate 4: Signal structural validity check → fault: MALFORMED_SIGNAL
Gate 5: Deduplication check (signal_id) → yields: status == DUPLICATE (no new record)
Gate 6: Timestamp plausibility check → fault: MALFORMED_SIGNAL
[WRITE] IncidentRecord created with state = INGESTED
Gates are evaluated strictly in order. Failure at any gate aborts the sequence; later gates are not evaluated. Passing all six gates is the necessary and sufficient condition for record creation.
A gate-5 DUPLICATE result is not a fault — it is a successful idempotent
response. The existing record_id is returned to the caller unchanged.
No new record is created. No state transition occurs on the existing record.
2.3 Creation Atomicity#
Record creation is atomic. Either all of the following are written in a single substrate transaction, or none are:
RecordIdentityblock- Initial
RecordState = INGESTED ingested_attimestamp- Normalized signal envelope (source, content_type, emitted_at)
- Raw payload reference (payload is stored separately; the record holds a content-addressed reference, not the payload bytes inline)
If the transaction fails after gate 6, the substrate MUST NOT emit
IngestionStatus == ACCEPTED. The signal must be re-submitted by the emitter.
2.4 Raw Payload Handling#
The raw payload is stored in the substrate's content-addressed blob store,
keyed by SHA-256(raw_payload). The IncidentRecord holds the hash as a
reference. This has two consequences:
- Deduplication at the blob layer: Identical payloads from different signals share one blob. Purge of one record does not purge the blob if other records reference it.
- Zero inline storage: The
IncidentRecordschema never embeds payload bytes. All access to raw payload goes through the blob store reference.
Payload blobs are subject to independent retention policy (see Phase 5). A payload blob is eligible for blob-layer purge only when zero live records reference it AND the last referencing record has completed Phase 7.
3. Phase 2 — Record Versioning#
3.1 Versioned Sub-Object Model#
Three sub-objects on an IncidentRecord accumulate versioned snapshots
as the record progresses through its operational states. Each version is
immutable once written. Versions are indexed monotonically from 1.
VersionedSubObjects {
classification_versions : List<ClassificationVersion> -- appended by incident.classify
surface_map_versions : List<SurfaceMapVersion> -- appended by incident.map_surface_area
plan_versions : List<PlanVersion> -- appended by incident.derive_rectification_steps
}
The current version of each sub-object is always the highest-indexed
entry. The canonical version at any given past timestamp is the highest-
indexed entry whose effective_at is ≤ that timestamp.
No version may be deleted, overwritten, or reordered. Version indices are gaps-prohibited: index N+1 may not be written before index N is committed.
3.2 ClassificationVersion#
ClassificationVersion {
version : UInt -- 1-based, monotonically increasing
classifier_id : String
category : IncidentCategory
subcategory : String?
confidence : Float[0.0,1.0]
rationale : String
effective_at : Timestamp
superseded_at : Timestamp? -- null if this is the current version
}
When a new classification version is written, the prior version's
superseded_at is set to the new version's effective_at. This creates
a non-overlapping, gapless temporal chain.
Classification downgrade protection: A new classification version whose
confidence is more than 0.20 below the prior version's confidence MUST
carry an uncertainty_flag_id reference pointing to a previously created
CLASSIFICATION_AMBIGUOUS uncertainty flag on the same record. Downgrading
without a supporting uncertainty flag is a lifecycle constraint violation.
3.3 SurfaceMapVersion#
SurfaceMapVersion {
version : UInt
surface_map_id : UUID
scanner_id : String
surface_count : UInt
surface_snapshot_hash : Hash
mapped_at : Timestamp
superseded_at : Timestamp?
}
A new SurfaceMapVersion may only be written when the record's state is
CLASSIFIED or SURFACE_MAPPED. Writing a new surface map version while
the record is in PLAN_DERIVED, PENDING_APPROVAL, or APPROVED is
prohibited — the plan is derived against a specific surface map, and
surface re-mapping during the approval window would silently invalidate
the plan.
If a new surface map is genuinely required after planning has begun, the
approval must first be rejected (returning the record to PLAN_DERIVED),
then the record must transition back through reclassification to reach
a state where surface re-mapping is permitted.
3.4 PlanVersion#
PlanVersion {
version : UInt
plan_id : UUID
planner_id : String
surface_map_id : UUID -- the surface map this plan was derived against
step_count : UInt
derived_at : Timestamp
superseded_at : Timestamp?
approved_at : Timestamp? -- null until approval resolves
approved_by : List<ApproverRef>?
approval_policy : ApprovalPolicy?
}
A plan version is sealed (becomes immutable beyond approved_at and
approved_by fields) when incident.request_operator_approval is
invoked. Once PENDING_APPROVAL is entered, the plan's steps list
is frozen — no field of any RectificationStep within it may change.
If an approval is rejected, the current plan version's superseded_at
is set and the record returns to PLAN_DERIVED. A new plan version must
be derived before a new approval can be requested.
3.5 Version Chain Integrity Hash#
At each version write, the substrate computes and stores a chain_hash
over the concatenation of all prior version hashes in that sub-object's
chain plus the new version's canonical serialization:
chain_hash[N] = SHA-256(chain_hash[N-1] || serialize(version[N]))
chain_hash[1] = SHA-256(serialize(version[1]))
This produces a tamper-evident chain. Any modification to a historical version invalidates all subsequent chain hashes, which the substrate MUST verify on every read. A chain hash failure is a substrate integrity fault and MUST trigger immediate record quarantine and operator alert — it cannot be resolved by retry.
3.6 ExecutionRecord Chain#
incident.execute.* operators emit ExecutionRecord entries that are
appended to the record's execution log. The execution log follows the same
chain hash model as versioned sub-objects. Each ExecutionRecord contains:
ExecutionRecord {
execution_id : UUID
record_id : UUID
plan_id : UUID
step_index : UInt
operator_ref : String
target_ref : String
status : ExecutionStatus
dry_run : Bool
executed_at : Timestamp
fault_code : String? -- null on success; fault token on failure
fault_detail : String?
chain_hash : Hash -- integrity chain over all prior ExecutionRecords
}
ExecutionRecord entries are append-only and immutable. They are retained
independently of the parent IncidentRecord — an ExecutionRecord is never
purged before its parent record, and its retention window is always ≥ the
parent's retention window.
4. Phase 3 — Lineage#
4.1 The Incident Lineage Graph#
The ISM substrate maintains a directed acyclic graph (DAG) of IncidentRecord
relationships. Every record is a node; relationships are typed edges. The graph
is append-only — edges are never removed, even after purge (the purge receipt
replaces the node, but edge topology is preserved in the lineage index).
4.2 Relationship Types#
RelationshipType {
SPAWNED_FROM -- child record was created to handle a FAULTED parent's unresolved surfaces
RECLASSIFIED_AS -- record was superseded by a new record with a corrected classification
SPLIT_INTO -- record's surface scope was decomposed into multiple child records
MERGED_FROM -- record consolidates surfaces from multiple prior records
LINKED_TO -- advisory non-causal relationship; used for correlated incidents
}
Each relationship is represented as an edge:
LineageEdge {
edge_id : UUID
source_record_id : UUID
target_record_id : UUID
relationship : RelationshipType
created_at : Timestamp
created_by : String -- operator or agent that established the relationship
rationale : String
}
4.3 Lineage Registration#
Lineage edges are registered at the point of the incident.ingest call
that creates the child record, not retroactively. The ingesting agent MUST
supply parent_record_id and relationship_type in the optional lineage
block of incident.ingest IN(...) to establish the edge at creation time.
If lineage is not declared at ingest time, it may be registered as a
LINKED_TO edge by a subsequent administrative operation, but causal
edge types (SPAWNED_FROM, SPLIT_INTO, MERGED_FROM) are only valid
when declared at the child record's creation moment.
4.4 SPAWNED_FROM Semantics#
SPAWNED_FROM is the primary lineage edge type in the ISM. It is used
when a FAULTED record cannot be recovered in place and a new record must
be created to re-attempt remediation. The following constraints apply:
- The parent record MUST be in state
FAULTEDbefore aSPAWNED_FROMchild may be ingested. - The child record MUST reference the parent's
record_idin its lineage block. - The child's
SurfaceMapMUST be a proper subset of the parent'sSurfaceMapat the point of fault — it may not introduce new surfaces not present in the parent's surface history. - The parent record's
ExecutionRecordchain is cross-referenced in the child's lineage metadata, giving the child's operators full visibility into what the parent attempted.
4.5 Lineage Depth Limit#
The lineage graph enforces a maximum depth of MAX_LINEAGE_DEPTH (default: 5)
for any single SPAWNED_FROM chain. A record at depth 5 may not spawn
further children via SPAWNED_FROM. At this depth, mandatory human
escalation is required — the incident surface is considered persistently
unresolvable by automated means and must be handled entirely out-of-band.
LINKED_TO edges are exempt from depth limits — they are advisory only
and do not represent causal descent.
4.6 Graph Traversal Rules#
- The lineage graph may be queried at any time, including during active execution.
- Traversal results include nodes in all lifecycle phases — including purged nodes, represented by their purge receipts.
- The graph is read-only from the perspective of all
incident.*operators. Only lifecycle management processes (archival, lineage registration) may write edges. - Cycles are a substrate integrity fault. The substrate MUST reject any edge that would create a cycle in the lineage graph.
5. Phase 4 — Archival#
5.1 Archival Trigger#
Archival is triggered automatically when an IncidentRecord enters either
terminal state:
state == RESOLVED → archival triggered immediately
state == FAULTED → archival triggered after FAULT_ARCHIVAL_DELAY (default: 24h)
to allow operators time to inspect the live record
FAULTED records are not archived immediately because operators routinely
need to read the live record's execution chain and surface map during
incident post-mortems. The delay window is configurable but the minimum
is 1 hour; zero-delay archival of faulted records is not permitted.
5.2 Archival Seal#
Before archival, the substrate computes the Archival Seal — a single hash over the complete, serialized record at the moment of archival. The Archival Seal is the authoritative integrity reference for the archived record.
ArchivalSeal {
seal_id : UUID
record_id : UUID
sealed_at : Timestamp
terminal_state : RecordState -- RESOLVED | FAULTED
record_hash : Hash -- SHA-256 over canonical record serialization
chain_hashes : Map<String,Hash> -- final chain_hash for each versioned sub-object
execution_count : UInt -- total ExecutionRecord entries at seal time
lineage_edge_count : UInt
}
The ArchivalSeal is written to a dedicated, append-only seal ledger that
is independent of the main record store. The seal is never stored inline
with the record it covers — physical separation ensures that seal data
survives even if the record store is compromised.
5.3 Post-Archival Record State#
After archival, the IncidentRecord in the main store is replaced by an
ArchivedRecordStub:
ArchivedRecordStub {
record_id : UUID
terminal_state : RecordState
sealed_at : Timestamp
seal_id : UUID -- reference to ArchivalSeal in seal ledger
archive_ref : ArchiveRef -- content-addressed location in archive store
retention_class : RetentionClass
expires_at : Timestamp
legal_hold : Bool
}
The stub is the only record-store presence of an archived record. All
detailed data is in the archive store, accessed via archive_ref. The stub
is itself immutable after creation — the only field that may change is
legal_hold (see Phase 5).
5.4 Archive Store#
The archive store is a content-addressed, write-once object store. Each
archived record occupies a single archive object keyed by
SHA-256(canonical_serialization). Archive objects are:
- Write-once: No archive object may be overwritten.
- Read-many: Archive objects may be fetched for audit and lineage review.
- Separately retained: Archive objects follow the retention policy bound
to the
ArchivedRecordStub, not the main store's policy. - Cross-referenced: The
archive_refin the stub is a stable, durable locator — it does not change if the archive store is migrated.
Secret-typed surface entries within archived records are redacted in
the archive object. The surface_ref (the identifier, e.g. vault path or
ARN) is retained for audit purposes. The secret value was never stored
in the record (per the zero-secret-value guarantee in grammar invariant 4),
so no redaction of values is needed — only a marker noting that this surface
type carries a secret reference is retained.
6. Phase 5 — Retention#
6.1 Retention Classes#
Every archived record is assigned a RetentionClass at the time of archival.
Retention class determines the minimum period for which archived record data
must be held before it becomes eligible for expiry.
RetentionClass {
CRITICAL -- Minimum retention: 7 years
-- Applies to: records with IncidentCategory ∈ {SECRET_LEAK,
-- UNAUTHORIZED_ACCESS, DATA_EXPOSURE, SUPPLY_CHAIN}
-- Also applies to: any record with MAX_LINEAGE_DEPTH >= 3
STANDARD -- Minimum retention: 2 years
-- Applies to: records with IncidentCategory ∈ {DEPENDENCY_CVE,
-- MISCONFIGURATION, POLICY_VIOLATION}
-- Also applies to: any FAULTED record not otherwise CRITICAL
LOW -- Minimum retention: 90 days
-- Applies to: records with IncidentCategory == UNKNOWN where
-- terminal_state == RESOLVED and lineage depth == 0
-- Also applies to: dry-run-only records (all steps dry_run == true)
}
RetentionClass is computed deterministically from the record's
classification history and lineage at seal time. The class is always the
maximum class implied by any single qualifying rule — a record satisfying
both STANDARD and CRITICAL criteria is assigned CRITICAL.
RetentionClass is immutable once assigned. A record cannot be downgraded
to a lower class after archival, even if the initial classification is later
determined to have been incorrect.
6.2 Retention Period Computation#
expires_at = sealed_at + retention_period(retention_class) + jitter
Where:
retention_period(CRITICAL)= 7 years (2557 days)retention_period(STANDARD)= 2 years (730 days)retention_period(LOW)= 90 daysjitter= deterministic pseudo-random offset in range [0, 24h], keyed onrecord_id. Jitter prevents expiry thundering-herd when large batches of records are archived simultaneously.
expires_at is written into the ArchivedRecordStub at archival time and
is thereafter immutable — except for legal hold extension (see 6.3).
6.3 Legal Hold#
A legal hold prevents expiry and purge regardless of expires_at. Legal
holds are set and cleared exclusively by substrate administrators via the
Legal Hold Management Interface — no incident.* operator may set or clear
a legal hold.
LegalHold {
hold_id : UUID
record_id : UUID
placed_at : Timestamp
placed_by : String -- administrator identity
authority : String -- reference to legal/compliance authority (e.g. case number)
review_due_at : Timestamp -- when the hold must be reviewed for continued necessity
}
A record under legal hold:
- Does not enter Phase 6 (EXPIRY) regardless of
expires_at. - Must be reviewed at
review_due_at— the substrate MUST emit a notification to the placing administrator atreview_due_at - 30 days. - If
review_due_atpasses without a review action, the substrate MUST alert and extendreview_due_atby 90 days automatically, indefinitely. It does not lift the hold automatically.
Legal holds are fully audited — every placement, review, extension, and
removal is written to the Legal Hold Audit Ledger, which has its own
independent retention period of MAX(record_retention, 10 years).
6.4 Payload Blob Retention#
Raw payload blobs stored in the content-addressed blob store are retained for the lifetime of the longest-retaining referencing record. When the last record referencing a blob completes Phase 7, the blob becomes eligible for blob-layer purge at the next blob GC cycle.
Blob GC cycles run no more frequently than once per 24 hours and no less frequently than once per 7 days.
6.5 ExecutionRecord Retention#
ExecutionRecord entries are retained for:
MAX(parent_record_retention, EXECUTION_RECORD_MINIMUM_RETENTION)
Where EXECUTION_RECORD_MINIMUM_RETENTION = 2 years (730 days), regardless
of the parent record's class. A LOW-class parent record may be purged after
90 days, but its ExecutionRecord entries are held for the full 2 years.
This ensures that execution audit trails outlive the records that generated them for all non-CRITICAL incidents, providing a minimum audit window sufficient for most regulatory frameworks.
7. Phase 6 — Expiry#
7.1 Expiry Eligibility#
An ArchivedRecordStub becomes expiry-eligible when all of the following
are true:
substrate_clock.now() >= expires_at
legal_hold == false
all child records in lineage graph are in Phase 4 or later
(a parent record may not expire while a live child record references it)
no open FollowupTicket references this record_id with status != CLOSED
The child-record constraint means lineage roots expire last — a parent
incident cannot be purged while any of its SPAWNED_FROM descendants are
still active. This preserves the full causal context for active incidents.
7.2 Expiry Notice Window#
When a record becomes expiry-eligible, the substrate enters a mandatory expiry notice window of 72 hours before the record is placed into the purge queue. During this window:
- The
ArchivedRecordStubis markedexpiry_pending: true. - A notification is sent to the substrate's designated expiry notification channel (configurable endpoint; default: substrate operations mailbox).
- Any substrate administrator may extend the expiry window by up to 30 days via the Expiry Extension Interface. Extensions are limited to 3 per record without a legal hold being placed.
- After 3 extensions without a legal hold, the record enters the purge queue automatically at the next eligible window.
7.3 Expiry Notice Payload#
ExpiryNotice {
notice_id : UUID
record_id : UUID
terminal_state : RecordState
retention_class : RetentionClass
sealed_at : Timestamp
expires_at : Timestamp
purge_eligible_at : Timestamp -- expires_at + 72h notice window
lineage_summary : String -- human-readable: depth, child count, relationship types
followup_summary : String -- count of CLOSED FollowupTickets on this record
}
Expiry notices are stored in the Expiry Notice Ledger for the duration of the record's legal hold audit trail (minimum 10 years). They are never purged with the record they describe.
8. Phase 7 — Purge#
8.1 Purge Mechanics#
Purge is the irreversible, verified destruction of all primary record data. It proceeds in the following strict sequence:
Step 1: Pre-purge integrity check
Verify ArchivalSeal(record_id).record_hash against the archive object.
If hash does not match → abort purge; emit substrate integrity fault.
Step 2: ExecutionRecord purge eligibility check
Verify all ExecutionRecords for record_id have retention age >=
EXECUTION_RECORD_MINIMUM_RETENTION.
If not → defer purge; log deferral reason.
Step 3: FollowupTicket closure check
Verify all FollowupTickets referencing record_id are in CLOSED state.
If any are OPEN → abort purge; emit PURGE_BLOCKED_BY_OPEN_FOLLOWUP.
Step 4: Lineage child check
Verify all child records in lineage are in Phase 4 or later.
If any child is in Phase 1–3 → abort purge; log blocking child record_id.
Step 5: Legal hold check
Verify legal_hold == false.
If true → abort purge; emit PURGE_BLOCKED_BY_LEGAL_HOLD.
Step 6: Purge Receipt pre-issuance
Write PurgeReceipt (see 8.2) to Purge Receipt Ledger BEFORE destruction.
If PurgeReceipt write fails → abort entire purge; do not destroy data.
Step 7: Data destruction
7a. Delete archive object from archive store.
7b. Purge all ClassificationVersions, SurfaceMapVersions, PlanVersions
from version store.
7c. Purge ExecutionRecords (if retention-eligible; see Phase 5).
7d. Purge UncertaintyFlags, HoldRecords, ApprovalRequests.
7e. Replace ArchivedRecordStub with PurgeStub (see 8.3).
7f. Remove payload blob reference; trigger blob GC eligibility.
Step 8: Post-purge verification
Verify ArchivedRecordStub has been replaced by PurgeStub.
Verify archive object is no longer retrievable.
Verify PurgeReceipt.purge_completed_at is set.
If any verification fails → emit PURGE_INCOMPLETE; alert operators.
All eight steps are logged to the Purge Audit Log. Step 6 (pre-issuance of the purge receipt) MUST complete before any data destruction in step 7. If the substrate crashes between steps 6 and 7, the purge is idempotent — on recovery, the substrate detects the pre-issued receipt and re-attempts destruction only for sub-steps not yet verified complete.
8.2 Purge Receipt#
PurgeReceipt {
receipt_id : UUID
record_id : UUID -- preserved permanently
terminal_state : RecordState -- preserved permanently
retention_class : RetentionClass
sealed_at : Timestamp
purge_initiated_at : Timestamp
purge_completed_at : Timestamp? -- null until step 8 verification passes
purge_initiated_by : String -- substrate process identity
execution_count : UInt -- total ExecutionRecords that existed
surface_count : UInt -- total surface entries that existed
lineage_edges : UInt -- total lineage edges at purge time
seal_id : UUID -- reference to ArchivalSeal in seal ledger
verification_hash : Hash -- SHA-256 over PurgeReceipt canonical serialization
}
The PurgeReceipt is the permanent, irrevocable record that a given
record_id existed and was destroyed. It contains enough information to
answer audit questions without retaining any of the original incident data.
PurgeReceipt entries are never purged. They are retained permanently
in the Purge Receipt Ledger, which is a separate, independently backed-up
store. The Purge Receipt Ledger's own integrity is verified by a daily
chain-hash audit over all receipts in chronological order.
8.3 Purge Stub#
After step 7e, the ArchivedRecordStub is replaced by a PurgeStub:
PurgeStub {
record_id : UUID
receipt_id : UUID -- reference to PurgeReceipt
purge_completed_at : Timestamp
}
The PurgeStub remains in the main record store permanently, in the same
index position as the original IncidentRecord. Its sole purpose is to
ensure that any system holding a cached record_id receives a meaningful
and accurate response ("this record was purged") rather than a
RECORD_NOT_FOUND error, which would be ambiguous between "never existed"
and "was destroyed."
8.4 Zero-Retention Guarantee for Secret Surfaces#
The ISM provides a specific, additional guarantee for SECRET-typed surface entries: at no point during any lifecycle phase do secret values enter any substrate store. This guarantee is enforced at three layers:
- Grammar layer:
incident.execute.rotate_secretOUT never includes the new secret value (grammar invariant 4,operator_grammar.md). - Archival layer: Archive objects for records with SECRET-typed surfaces
include only the
surface_ref(the identifier/path) — marked with a[SECRET_SURFACE]tag — never any value. - Purge layer: The
PurgeReceiptrecords the count of SECRET-typed surface entries that existed, but not their refs or values.
This three-layer guarantee means that purging a record containing SECRET surfaces achieves complete destruction of all secret-adjacent data — there is no residual secret identifier in any post-purge artifact.
9. Lifecycle Invariants#
The following invariants govern the complete lifecycle. They extend
(and do not contradict) the grammar invariants in operator_grammar.md
Section 10.
-
Phase monotonicity: A record may not move to an earlier lifecycle phase. Archival is irreversible; retention is non-decreasing; purge is terminal.
-
Identity permanence:
RecordIdentityfields are immutable from Phase 1 onward. No lifecycle event, administrative action, or substrate migration may alterrecord_id,signal_id,source, oringested_at. -
Version chain tamper-evidence: Any break in a versioned sub-object's chain hash sequence is a substrate integrity fault. The substrate MUST quarantine the affected record and alert operators. Chain hash failures are never silently corrected.
-
Archival seal precedence: The
ArchivalSealis the authoritative record of what anIncidentRecordcontained at terminal state. If the archive object and the seal disagree, the seal is correct and the archive object is corrupted. -
Retention class immutability: A record's
RetentionClassis assigned once at archival and never reduced. Upward reclassification (e.g., a STANDARD record discovered to fall under CRITICAL criteria) requires a legal hold to be placed immediately and a substrate administrator review. -
Pre-purge receipt requirement: No record data may be destroyed before a
PurgeReceiptis committed to the Purge Receipt Ledger. Destruction without a receipt is a substrate integrity fault. -
Purge stub permanence:
PurgeStubentries are never deleted. The main record store's index is monotonically growing — it only gains entries (as new records) or replaces entries with stubs (on purge); it never shrinks. -
ExecutionRecord longevity floor:
ExecutionRecordentries are never purged beforeEXECUTION_RECORD_MINIMUM_RETENTION(2 years), regardless of the parent record's retention class or purge status. -
Lineage graph permanence: Lineage edges and node references (including purge stub references) are never deleted from the lineage graph. The lineage graph grows monotonically.
-
Legal hold audit completeness: Every legal hold placement, review, extension, and removal is recorded in the Legal Hold Audit Ledger. No legal hold event may occur without a corresponding audit entry. The Legal Hold Audit Ledger is retained for
MAX(record_retention, 10 years). -
Child-before-parent expiry prohibition: A record with live children (Phase 1–3) in the lineage graph may not enter Phase 6 (EXPIRY). Expiry eligibility is re-evaluated whenever a child record transitions to Phase 4.
-
Secret surface zero-retention: No secret value associated with any SECRET-typed surface entry is retained in any substrate store at any lifecycle phase. The zero-retention guarantee for secret values is unconditional and survives purge.
10. Lifecycle Schema Extensions#
Types introduced by this document, extending the registry in
operator_grammar.md Section 8.
RecordIdentity ::= { record_id: UUID, signal_id: UUID,
source: String, ingested_at: Timestamp }
ClassificationVersion ::= { version: UInt, classifier_id: String,
category: IncidentCategory, subcategory: String?,
confidence: Float, rationale: String,
effective_at: Timestamp, superseded_at: Timestamp?,
chain_hash: Hash }
SurfaceMapVersion ::= { version: UInt, surface_map_id: UUID, scanner_id: String,
surface_count: UInt, surface_snapshot_hash: Hash,
mapped_at: Timestamp, superseded_at: Timestamp?,
chain_hash: Hash }
PlanVersion ::= { version: UInt, plan_id: UUID, planner_id: String,
surface_map_id: UUID, step_count: UInt, derived_at: Timestamp,
superseded_at: Timestamp?, approved_at: Timestamp?,
approved_by: List<ApproverRef>?, approval_policy: ApprovalPolicy?,
chain_hash: Hash }
ExecutionRecord ::= { execution_id: UUID, record_id: UUID, plan_id: UUID,
step_index: UInt, operator_ref: String, target_ref: String,
status: ExecutionStatus, dry_run: Bool, executed_at: Timestamp,
fault_code: String?, fault_detail: String?, chain_hash: Hash }
RelationshipType ::= SPAWNED_FROM | RECLASSIFIED_AS | SPLIT_INTO
| MERGED_FROM | LINKED_TO
LineageEdge ::= { edge_id: UUID, source_record_id: UUID,
target_record_id: UUID, relationship: RelationshipType,
created_at: Timestamp, created_by: String, rationale: String }
ArchivalSeal ::= { seal_id: UUID, record_id: UUID, sealed_at: Timestamp,
terminal_state: RecordState, record_hash: Hash,
chain_hashes: Map<String,Hash>, execution_count: UInt,
lineage_edge_count: UInt }
ArchivedRecordStub ::= { record_id: UUID, terminal_state: RecordState,
sealed_at: Timestamp, seal_id: UUID,
archive_ref: ArchiveRef, retention_class: RetentionClass,
expires_at: Timestamp, legal_hold: Bool }
RetentionClass ::= CRITICAL | STANDARD | LOW
LegalHold ::= { hold_id: UUID, record_id: UUID, placed_at: Timestamp,
placed_by: String, authority: String, review_due_at: Timestamp }
ExpiryNotice ::= { notice_id: UUID, record_id: UUID, terminal_state: RecordState,
retention_class: RetentionClass, sealed_at: Timestamp,
expires_at: Timestamp, purge_eligible_at: Timestamp,
lineage_summary: String, followup_summary: String }
PurgeReceipt ::= { receipt_id: UUID, record_id: UUID,
terminal_state: RecordState, retention_class: RetentionClass,
sealed_at: Timestamp, purge_initiated_at: Timestamp,
purge_completed_at: Timestamp?, purge_initiated_by: String,
execution_count: UInt, surface_count: UInt,
lineage_edges: UInt, seal_id: UUID, verification_hash: Hash }
PurgeStub ::= { record_id: UUID, receipt_id: UUID, purge_completed_at: Timestamp }
ArchiveRef ::= Content-addressed locator string in the archive store;
format: "ism-archive://<store-id>/<sha256-hex>"
LifecyclePhase ::= CREATION | VERSIONING | LINEAGE | ARCHIVAL
| RETENTION | EXPIRY | PURGE
11. Lifecycle Constants#
These values are substrate-configured via the ISM configuration layer.
| Constant | Default | Description |
|---|---|---|
FAULT_ARCHIVAL_DELAY |
24 hours | Delay before FAULTED records are archived |
MAX_LINEAGE_DEPTH |
5 | Maximum SPAWNED_FROM chain depth |
RETENTION_CRITICAL |
7 years (2557 days) | Retention period for CRITICAL class records |
RETENTION_STANDARD |
2 years (730 days) | Retention period for STANDARD class records |
RETENTION_LOW |
90 days | Retention period for LOW class records |
EXECUTION_RECORD_MINIMUM_RETENTION |
2 years (730 days) | Minimum retention for ExecutionRecords regardless of parent class |
EXPIRY_NOTICE_WINDOW |
72 hours | Notice window before purge queue entry |
EXPIRY_EXTENSION_MAX |
3 | Maximum extensions without legal hold |
EXPIRY_EXTENSION_PERIOD |
30 days | Duration of each extension |
LEGAL_HOLD_REVIEW_ALERT_LEAD |
30 days | Alert lead time before review_due_at |
LEGAL_HOLD_AUDIT_RETENTION |
MAX(record, 10 years) | Retention for Legal Hold Audit Ledger |
BLOB_GC_MIN_INTERVAL |
24 hours | Minimum interval between blob GC cycles |
BLOB_GC_MAX_INTERVAL |
7 days | Maximum interval between blob GC cycles |
CHAIN_HASH_AUDIT_INTERVAL |
24 hours | Frequency of Purge Receipt Ledger integrity audit |
12. Trilogy Cross-Reference#
| Concept | Primary definition | Referenced here |
|---|---|---|
IncidentRecord.state machine |
operator_grammar.md §7 |
§1 (phase mapping), §5.1 (archival trigger) |
incident.ingest gate sequence |
operator_grammar.md §1 |
§2.2 (creation gate) |
| Grammar Invariant 4 (zero-secret-value) | operator_grammar.md §10 |
§5.4, §8.4 |
ExecutionRecord |
operator_grammar.md §3.6 |
§3.6, §5.5, §8.1 step 7c |
| All fault tokens | substrate_errors.md |
§2.2 (gate faults), §8.1 (purge faults) |
GEN-005 PARTIAL_EXECUTION |
substrate_errors.md §GEN |
§3.6 (faulted execution chain) |
EXE-002 PATH_TRAVERSAL_DETECTED |
substrate_errors.md §EXE |
§4.3 (SPAWNED_FROM child constraints) |
| Substrate constants | operator_grammar.md §9 |
§11 (lifecycle constants extend §9) |
End of lifecycle document.
Canonical source: /docs/Incident_Substrate_Model/operator_lifecycle.md · TriadicFrameworks repository
RTT/1 Canon · ISM Lifecycle v1.0
Trilogy complete: operator_grammar.md · substrate_errors.md · operator_lifecycle.md