Legal Hold
Table of contents
Summary
Legal Hold is a formal instruction to keep a specific record intact — overriding any normal deletion schedule — until the legal matter that triggered it is resolved. When a lawsuit is anticipated, a regulator opens an investigation, or a team needs to freeze evidence, the normal “delete after N years” rule stops applying. The hold records the shift: who issued it, against which record, why, and under what authority. A hold is either Active (preservation in effect) or Released (lifted, permanently — it cannot be reactivated). Both placing and releasing are attributed, timestamped, and never altered afterward, so the full arc of any preservation obligation can be reconstructed from the records alone. Multiple holds can cover the same record independently — a record under both an internal investigation and a government investigation has two separate obligations, and releasing one leaves the other in force, which is exactly what multi-party legal situations require. The pattern does not itself block deletion; it records that an obligation exists, and the system that actually deletes records is responsible for checking for Active holds first. This is the mechanism behind litigation-hold management, breach-response preservation, and any setting where destroying a record during an active legal obligation exposes the organization to penalties.
Intent
When litigation is reasonably anticipated, when a regulatory body opens an investigation, when an audit freeze is ordered, or when a breach response team needs to preserve forensic evidence, the normal retention clock stops being the governing rule. The obligation shifts from keep this record for N years to keep this record until the legal matter resolves, regardless of what the retention schedule says. That shift is a Legal Hold.
Legal Hold is the enforcement record for that shift. A compliance officer, legal counsel, or automated case management integration places a hold (a legally mandated preservation order — prevents deletion of records potentially relevant to litigation) on a record, naming who placed it, why, and under what legal authority. The record is now preserved. When the matter closes — the litigation settles, the investigation ends, the audit concludes — the hold is released. The record is again governed by its retention window. The full arc, from placement to release, is carried in the hold records themselves: every attribution field, every timestamp, every reason. The atom guarantees the chain is present and immutable by specification. Cryptographic protection of those records against post-hoc modification — the bar for court-admissible evidence in spoliation litigation (destruction or concealment of evidence relevant to a legal proceeding) — is added by composition with Tamper Evidence; this atom does not provide it alone.
The pattern is structurally distinct from Retention Window in a load-bearing way: Retention Window answers how long must this record be kept under normal operation; Legal Hold answers this record may not be purged under any circumstances until this hold is explicitly released. The two coexist as composing peers — a record under both a retention window and a legal hold must satisfy both: it cannot be purged before retention_until, and it cannot be purged while any Active hold remains. Neither atom enforces the other’s constraint; the composition (Regulated Record Retention & Defensible Deletion) wires the gate that checks both before permitting purge. This keeps each atom freestanding.
The atom records holds; it does not prevent purge directly. Preventing purge is enforced by the composition that wires Legal Hold with the system’s purge surface. This is deliberate: the atom specifies what a hold is and what its records must prove; the deployment decides which purge surfaces check for Active holds before acting. A Legal Hold atom that intercepts purge internally would need to know about storage layers, retention records, and purge mechanisms — absorbing concepts that belong to composing patterns and breaking freestanding status.
Multiple concurrent holds on the same record are structurally independent. This is not a simplification — it is the correct model for real multi-party legal situations. A record under both an internal forensic investigation hold and a state attorney general investigation hold has two independent preservation obligations. If the internal team closes its investigation and releases its hold, the AG’s preservation obligation is unaffected. A system that released both holds when one was released would create spoliation risk. Each hold has its own hold_id, its own lifecycle, and its own release event. Whether a record is currently held — meaning at least one Active hold covers it — is a query result over the hold records, not a separate state on the hold.
This is a freestanding (can be specified without naming any other pattern) concept in the EOS (Essence of Software — Daniel Jackson’s framework for specifying software concepts as freestanding, composable units) sense. It carries its own state (the hold record set), its own actions (place, release, read), and its own invariants (hold immutability, two-state exclusivity, terminal absorption, concurrent independence, release attribution, store durability). Composing patterns add the purge gate, access control, case management integration, and cross-record batch holds.
Structure
Store instance model
The Legal Hold atom operates against a named store instance. A Store Name identifies the instance; multiple instances coexist in real systems — one per organization, jurisdiction, or business unit, depending on deployment topology. Hold Id values are unique within a store instance; uniqueness across instances is a composing concept. Record Ref is an opaque reference scoped to the host system — the same Record Ref may be held by multiple simultaneous holds within the same store instance. Calls implicitly target a single routed instance; instance selection is handled at the deployment-routing layer, not defined by this atom.
Identity model
Each hold has an opaque, immutable, system-generated Hold Id — assigned on Place, never reused, never reassigned within the store instance. It must be a non-empty string sortable in lexicographic byte-order; this property is required for deterministic Read ordering. The id is the hold’s identity; the record reference, placing actor, reason, and timestamps are properties of the hold, not its identity.
Record Ref is an opaque reference to the record being held. Set on Place, immutable. The atom does not validate that the record exists or is currently retained — Record Ref is the caller’s responsibility. Two holds over the same record have distinct Hold Ids; each is its own audit record with its own lifecycle.
Placed By is an opaque reference to the actor placing the hold. Set on Place, immutable. It is the attribution anchor for the preservation decision; empty or whitespace-only values are rejected at placement.
Case Ref is an optional opaque reference to the legal matter, investigation, or audit under which the hold is placed (for example, a case management system identifier or a docket number). Set on Place, immutable. Its absence is valid — holds may be placed before a formal case is opened, or the case reference system may be external. If supplied, it must contain at least one non-whitespace character.
Inputs
- Place calls from legal counsel, compliance officers, case management integrations, or automated preservation workflows, each carrying a Record Ref, a placing actor (Placed By), a Reason, an optional Case Ref, and an optional explicit Placed At timestamp.
- Release calls documenting that the legal obligation has ended, carrying the Hold Id, the releasing actor (Released By), a required Reason, and an optional explicit Released At timestamp.
- Read queries from legal teams, compliance dashboards, audit processes, and litigation support workflows.
Actions
For optional parameters in both Place and Release, “supplied” means provided as a parseable value of the declared type. Null, missing, and empty (or whitespace-only) values are equivalent to “not supplied,” and the action’s documented default applies.
-
Place — (Projected contract:
place(record_ref, placed_by, reason, case_ref?, placed_at?) → hold_id | rejected(invalid-request | storage-failure)) — place a preservation hold on the named record. Assigns a fresh Hold Id, records Record Ref, Placed By, Hold Reason, Case Ref (if supplied), and Placed At (wall clock if not supplied; must not be in the future). The hold enters Active state. Record Ref, Placed By, and Reason must each contain at least one non-whitespace character; Case Ref, if supplied, must also contain at least one non-whitespace character — any violation is Invalid Request. Storage Failure if the store write fails after all preconditions pass; no Hold Id is issued and no record enters the store. -
Release — (Projected contract:
release(hold_id, released_by, reason, released_at?) → released | rejected(invalid-request | not-known | already-released | storage-failure)) — document the end of the preservation obligation and transition the hold to Released. Records Released By, Release Reason, and Released At (wall clock if not supplied; must not be in the future — releasing a hold in the future is not meaningful); all are immutable after the transition. The Hold Id parameter must itself contain at least one non-whitespace character (Invalid Request); a null, empty, or whitespace-only Hold Id is malformed and rejected before any existence check is performed. Released By and Reason must each contain at least one non-whitespace character (Invalid Request). The resolved Released At — whether caller-supplied or wall-clock-defaulted — must be ≥ the hold’s Placed At; a value less than Placed At is Invalid Request regardless of how it was derived (this enforces Invariant 6 against clock-skew artifacts as well as caller-supplied backdated values). Releasing a hold on a record does not affect any other hold on the same record. Storage Failure leaves the hold in Active state; the caller must retry. -
Read — (Projected contract:
read(query) → ordered_sequence_of_holds | rejected(invalid-query)) — return holds matching the Query, ordered by Placed At ascending, then by Hold Id ascending in lexicographic byte-order as a stable tiebreaker. Implementations must assign Hold Id values in a format where string byte-order sort produces a total order (e.g., ULID — Universally Unique Lexicographically Sortable Identifier, UUID v7 — version 7 of the Universally Unique Identifier, which is time-ordered, or zero-padded integer string). The supported filter axes are exactly: Hold Id, Record Ref, Placed By, Case Ref, State, and time ranges on Placed At or Released At. Any combination of supported axes is valid. A query supplying only a Hold Id returns at most one hold. A well-formed query matching no holds returns an empty sequence, not a rejection. A query with no filters returns every hold in the store. A time range filter on Released At returns only holds that carry a Released At field — i.e., Released holds. Active holds carry no Released At field and are implicitly excluded from results whenever a Released At filter is present, regardless of whether a State filter is also supplied. A query{released_at: {after: X}}with no state filter returns Released holds wherereleased_at > X; Active holds are not included. A query{state: Active, released_at: {after: X}}returns an empty sequence by the same rule. A Case Ref filter value matches only holds where Case Ref is set and equals the value; holds without Case Ref are excluded by any positive Case Ref filter (a “find holds that have no Case Ref” predicate is not in the spec; if a deployment needs it, the composing layer adds it). The query{record_ref: X, state: Active}returns every Active hold covering a given record — this is the operational check for whether a record is currently held.Malformed-query rules (Invalid Query): a Hold Id, Record Ref, Placed By, or Case Ref filter value that is null, empty, or whitespace-only is Invalid Query (the filter axes exist; the values are malformed). A State filter value that is not one of {Active, Released} is Invalid Query. A time range with end before start is Invalid Query. A query carrying an unrecognized filter key — any key outside the supported axes named above — is Invalid Query; an unrecognized key is rejected rather than silently ignored, because silent ignore would return a result set inconsistent with the caller’s intent.
Outputs
- For Place: a fresh Hold Id, or a rejection.
- For Release: the outcome token
released, or a rejection. - For Read: a (possibly empty) ordered sequence of holds. Each hold carries its full field set. Fields present on every hold (Active or Released): Hold Id, Record Ref, Placed By, Hold Reason, Placed At, State. Optional field set at placement (independent of state): Case Ref (present if supplied at Place, absent otherwise; immutable thereafter). State-specific fields: Released By, Release Reason, Released At are present on Released holds only. A Released hold carries all placement fields (including Case Ref if it was supplied) and all release fields simultaneously.
State
Each hold is in exactly one state:
- Active — the preservation obligation for Record Ref is in effect. Any composition wiring this atom to a purge surface must treat an Active hold as blocking purge eligibility; the atom records the obligation but does not enforce it internally. The hold carries Hold Id, Record Ref, Placed By, Hold Reason, Placed At, and Case Ref (if supplied). May only be released (transitioning to Released) or read.
- Released — the preservation obligation has ended. Carries Released By, Release Reason, and Released At (all immutable from the moment Release completes), plus all placement fields. Terminal; no further transitions.
Valid transitions — writes only; every committed transition stamps its timestamp from the receiving node’s wall clock:
| action | from | to | guard | stamps | result |
|---|---|---|---|---|---|
| Place | (no record) | Active | Placed At (if supplied) not in the future | fresh Hold Id; Record Ref; Placed By; Hold Reason; Case Ref if supplied; Placed At | the new Hold Id |
| Release | Active | Released | resolved Released At not future ∧ ≥ Placed At | Released By; Release Reason; Released At | released |
No other transitions exist. A hold cannot be re-activated after release; a new preservation obligation requires a new Place call producing a new Hold Id. Release on an already-Released hold is rejected Already Released, writing nothing; concurrent holds on one record are independent (Invariant 4), so releasing one never transitions another.
Flow
- Litigation trigger. Counsel determines that records relating to Project Alpha are subject to litigation hold. Calls
place(record_ref: "doc-alpha-0012", placed_by: "counsel_morgan", reason: "Litigation hold — Smith v. Acme Corp., SDNY 2026-cv-4421 — all Project Alpha records", case_ref: "matter-2026-smith-acme")→hold_id: "hold-001". The hold enters Active. - Second independent hold. The state AG separately issues a preservation demand covering the same record. Compliance calls
place(record_ref: "doc-alpha-0012", placed_by: "compliance_lee", reason: "NY AG Civil Investigative Demand — Case INV-2026-0089", case_ref: "ag-inv-2026-0089")→hold_id: "hold-002". Two independent Active holds now cover the record. - Internal matter closes. Smith v. Acme Corp. settles. Counsel calls
release("hold-001", released_by: "counsel_morgan", reason: "Matter settled with prejudice — May 10 2026")→released. Hold-001 is now Released. Hold-002 remains Active; the record is still held. - AG investigation closes. AG closes the investigation.
release("hold-002", released_by: "compliance_lee", reason: "NY AG CID withdrawn — May 28 2026")→released. No Active holds remain on the record; the composing layer resumes normal retention governance. - Audit query. A later audit queries
read({record_ref: "doc-alpha-0012"})and sees both holds with full placement and release attribution. The record’s complete legal hold history is recoverable without recourse to external systems.
Decision points
-
At Place — Record Ref, Placed By, and Reason must each contain at least one non-whitespace character; Case Ref, if supplied, must also contain at least one non-whitespace character; Placed At, if supplied, must not be in the future (checked against the receiving node’s wall clock). Any violation is Invalid Request. Storage Failure if the store write fails; no Hold Id is issued, no record enters the store.
-
At Release — the Hold Id parameter is checked first: if null, empty, or whitespace-only, the call is Invalid Request (the caller passed garbage, not a reference to a missing hold). If Hold Id is well-formed, the store is consulted: Not Known if no hold with this id exists; Already Released if the hold is in Released state. If neither, attribution and temporal checks apply: Released By and Reason must each contain at least one non-whitespace character (Invalid Request); the resolved Released At — caller-supplied or wall-clock-defaulted — must not be in the future (the future-bound applies only when caller-supplied, because a wall-clock default is “now” by construction) and must be ≥ the hold’s Placed At. The ≥ Placed At bound applies to the resolved Released At regardless of how it was derived; this enforces Invariant 6 against clock-skew artifacts as well as caller-supplied backdated values. A violation is Invalid Request. Storage Failure leaves the hold in Active; the caller must retry. Rejection priority: malformed Hold Id (Invalid Request) → Not Known → Already Released → attribution/temporal (Invalid Request) → Storage Failure.
-
At Read — every supplied filter value must be well-formed for its axis. A Hold Id, Record Ref, Placed By, or Case Ref filter value that is null, empty, or whitespace-only is Invalid Query. A State filter value not in {Active, Released} is Invalid Query. A time range with end before start is Invalid Query. An unrecognized filter key — any key outside the supported axes — is Invalid Query; the spec rejects rather than ignores unknown keys. A Released At filter implicitly excludes Active holds, which carry no Released At field, regardless of whether a State filter is also present. A Case Ref filter excludes holds without Case Ref. A well-formed query matching no holds returns an empty sequence.
Behavior
- Holds are durable on success. Once Place returns a Hold Id, the hold is in the store and will appear in subsequent reads.
- Hold placement is not idempotent. Two Place calls for the same Record Ref, Placed By, and Reason create two independent holds with distinct Hold Ids.
- Concurrent holds are independent. Multiple Active holds on the same Record Ref do not interact. Releasing hold A leaves hold B unaffected. The aggregate “is this record held?” question is answered by querying
{record_ref: X, state: Active}and checking whether the result is non-empty; the atom does not maintain a separate aggregate state. - Released state is terminal and auditable. A Released hold carries the full placement and release record. It is the audit evidence of the complete preservation arc — when the hold was placed, by whom, why, and when it was lifted. Releasing a hold does not remove its record from the store.
- The atom does not enforce the purge gate. Whether a held record is actually prevented from being purged is enforced at the composing layer (Retention Window + Legal Hold composition). The atom records that a preservation obligation exists; the composition enforces it at the purge surface.
- Reads are repeatable; the hold store is monotonic. The hold store only grows — Place adds records, Release transitions them. An unfiltered read at
t2 > t1returns every hold visible att1plus any added in between. State-filtered reads are not monotonic: a hold visible understate: Activeatt1may appear understate: Releasedatt2if released in between.
Feedback
- After Place — a new Active hold record exists; Hold Id, Record Ref, Placed By, Hold Reason, Placed At, and Case Ref (if supplied) are set and immutable.
- After Release — the hold is now Released; Released By, Release Reason, and Released At are set and immutable. All placement fields are unchanged.
Each rejected action produces an observable refusal naming the failed precondition.
Invariants
-
Invariant 1 — Hold immutability. After a successful Place, the fields Hold Id, Record Ref, Placed By, Hold Reason, Placed At, and Case Ref never change, regardless of any subsequent action.
-
Invariant 2 — Membership exclusivity. Every hold known to the store is in exactly one of {Active, Released} at all times.
-
Invariant 3 — Terminal absorption. Once a hold transitions to Released, no action transitions it further. The atom has no re-activate surface; a new preservation need requires a new Place.
-
Invariant 4 — Concurrent holds are independent. Releasing hold H on record R does not change the state of any other hold H′ on the same record R. The Active/Released state of each hold is determined solely by whether Release has been called on that specific Hold Id.
-
Invariant 5 — Release attribution is complete. Every Released hold carries Released By and Release Reason each containing at least one non-whitespace character, and a Released At timestamp that is set. An anonymous release, an unexplained release, a whitespace-only attribution string, or a release with no timestamp is a conformance failure — each defeats the audit trail that legal proceedings depend on.
-
Invariant 6 — Temporal ordering. For every Released hold, Released At ≥ Placed At. A hold cannot be documented as released before it was placed. The constraint applies to the value persisted in the record, regardless of whether Released At was caller-supplied or wall-clock-defaulted; the Release Decision point enforces this against the resolved value before the transition is committed.
-
Invariant 7 — Placement attribution is complete. Every hold, Active or Released, carries Hold Id, Record Ref, Placed By, and Hold Reason each containing at least one non-whitespace character, and a Placed At timestamp that is set. Invariant 1 guarantees these fields are immutable; this invariant guarantees they are never blank or unset. An anonymous placement, a whitespace-only reason, or a missing timestamp is a conformance failure — it defeats the chain of custody a court requires to establish when and why the preservation obligation was recognized.
-
Invariant 8 — Hold store durability. No hold record is removed from the store. The total hold count is monotonically non-decreasing. A Hold Id returned by a successful Place is durably persisted; a Storage Failure rejection guarantees no partial record was written. Released holds are retained as audit evidence; deleting a Released hold would destroy the proof that the preservation obligation was honored and lawfully lifted.
Examples
Happy path — litigation hold through release
See Flow section. A complete hold arc is walked there: placement by counsel, second independent hold by compliance, release of the first, continued preservation under the second, eventual release of the second, and a later audit query recovering the full history.
Rejection path — release attempted twice
After hold-001 is Released, counsel’s paralegal system retries: release("hold-001", released_by: "system_retry", reason: "automated retry") → rejected(already-released). The hold record is unchanged. The paralegal system detects the rejection and suppresses the retry.
Rejection path — place with empty reason
place(record_ref: "doc-0099", placed_by: "compliance_chen", reason: " ") → rejected(invalid-request). Whitespace-only reason is treated as empty. No hold is created.
Rejection path — release with future timestamp
release("hold-007", released_by: "counsel_kim", reason: "case closed", released_at: "2027-01-01T00:00:00Z") → rejected(invalid-request). A release documented as occurring in the future is not operationally meaningful; the atom records the present obligation, not a future intent.
Regulated adversarial scenarios
Regulator audit — HHS OCR HIPAA investigation
HHS (US Department of Health and Human Services — the federal agency that enforces HIPAA) Office for Civil Rights (OCR) opens an investigation into a reported breach of PHI (Protected Health Information — individually identifiable health data covered by HIPAA, the Health Insurance Portability and Accountability Act, the US federal law governing healthcare data privacy). It issues a preservation demand to the covered entity for all records relating to the incident. The compliance team calls Place for each record in scope; each placement carries case_ref: "ocr-hipaa-inv-2026-0334". Two months later, OCR requests the preservation record: query read({case_ref: "ocr-hipaa-inv-2026-0334", state: Active}) returns every Active hold placed under this investigation. Every hold carries Placed By and Hold Reason strings each with at least one non-whitespace character, and a Placed At timestamp that is set — immutable by Invariants 1 and 7. OCR confirms that preservation was initiated and that each hold is still Active. The covered entity has a documentable, auditable preservation response; no recourse to developer testimony is needed.
Spoliation challenge — federal litigation
Opposing counsel in federal litigation argues that the defendant destroyed documents after the duty to preserve was triggered under FRCP (Federal Rules of Civil Procedure — the rules governing civil lawsuits in US federal courts) Rule 37(e). Defendant’s counsel queries read({record_ref: "doc-contract-077"}) for all holds ever placed on the disputed document. The query returns the hold placed on the record, with placed_at: 2026-02-14 — the date the preservation obligation was recognized. The opposing party claims the document was destroyed on 2026-02-10. The hold record shows Placed At postdating the destruction. If a corresponding purge record from Retention Window shows purged_at: 2026-02-10 and no Active hold existed at that time, the records faithfully document the chronology — the hold was placed after the purge. If the document was never purged and remains in the store, the hold records confirm ongoing preservation. Either way, the court has the complete record; the atom does not manufacture a defense but it does not hide the facts either.
Concurrent hold integrity — dual regulatory investigation
A financial institution is simultaneously under a DOJ (US Department of Justice) criminal investigation and an SEC (US Securities and Exchange Commission — the federal regulator of securities markets) civil enforcement action. Both issue preservation demands covering the same set of trading records. The compliance team places holds under both matters:
hold-doj-001throughhold-doj-240:case_ref: "doj-crim-2026-0011"hold-sec-001throughhold-sec-240:case_ref: "sec-enf-2026-0087"
The DOJ investigation closes first. The compliance team releases all hold-doj-* holds. Query read({record_ref: "trade-record-0042", state: Active}) returns hold-sec-042 — the SEC hold remains Active. Invariant 4 guarantees that releasing the DOJ holds had no effect on the SEC holds. The institution correctly continues to preserve the records under SEC obligation. An auditor reviewing the hold records sees the full picture: two independent regulatory demands, one resolved, one active, no record destroyed while any Active hold covered it.
Generation acceptance
Any implementation derived from this atom must produce records and a runtime surface that pass the following checks from the records alone, without recourse to source code, runbooks, or developer narration:
-
Hold completeness check. For a set of Hold Ids known to have been issued, confirm that
read({hold_id: X})returns each of them across all states. No issued Hold Id may be absent from the store. -
Placement attribution check — both states. For every hold in the store, in any state: confirm Placed By, Hold Reason, Record Ref, and Hold Id each contain at least one non-whitespace character, and confirm Placed At is set (present, not null). This applies equally to Active and Released holds — Invariant 7 covers both. A hold in either state with a blank attribution string or a missing Placed At is a conformance failure under Invariant 7.
-
Release attribution check. For every Released hold: confirm Released By and Release Reason each contain at least one non-whitespace character, confirm Released At is set, and confirm Released At ≥ Placed At (Invariant 6). A Released hold with a blank attribution string, a missing Released At, or an inverted temporal ordering is a conformance failure under Invariants 5 and 6.
-
Hold independence check. Place two holds on the same Record Ref. Release the first. Confirm that
read({hold_id: second_hold_id})returns a hold still in Active state. Confirm thatread({record_ref: X, state: Active})returns the second hold only. Invariant 4 guarantees independence; this check verifies it. -
Terminal absorption check. Attempt Release against a known Released hold. The call must return
rejected(already-released). Confirm the hold’s fields are unchanged after the attempted release. -
Store monotonicity check. At time
t1, issueread({})(unfiltered) and record the result set S1. Place one new hold and confirm the Place call returned a Hold Id. At timet2 > t1, issueread({})again and record result set S2. Confirm every hold in S1 appears in S2 by Hold Id (no hold is removed). For each hold present in both S1 and S2, confirm the placement fields (Hold Id, Record Ref, Placed By, Hold Reason, Placed At, and Case Ref if it was set in S1) are unchanged in S2 — placement fields are immutable per Invariant 1. Release fields (Released By, Release Reason, Released At) may newly appear on holds released between t1 and t2; their appearance is conformant with the state machine and is not a monotonicity violation. The state of any hold present in both sets may legitimately have transitioned from Active to Released; the reverse transition is a conformance failure. The total hold count in S2 is ≥ the count in S1. Confirms the behavioral guarantee that the hold store is monotonically non-decreasing and that placement fields are immutable, while distinguishing the legitimate Active→Released transition from a violation.
Edge cases and explicit non-goals
-
Hold placed after record is purged. The atom does not prevent placing a hold on a Record Ref for which the underlying record has already been destroyed. The hold is created successfully; the Record Ref is an opaque value the atom does not validate against the storage layer. The hold record faithfully documents that a preservation obligation was recognized after the fact. Legal counsel and the court assess the spoliative implications — the atom records the truth, it does not adjudicate it. Whether post-purge hold placement triggers any remediation belongs to the composing layer and to legal counsel.
-
Multiple concurrent holds and aggregate held status. A record with N Active holds requires N Release calls to fully lift all holds. The atom has no
release_allaction — bulk release of all holds covering a record is a composing-layer operation that calls Place and Release appropriately. The aggregate “is this record currently held?” question is answered byread({record_ref: X, state: Active})returning a non-empty sequence; if the sequence is empty, no Active holds cover the record. -
Place is not idempotent. A prescriber system that retries after a network timeout creates a duplicate hold if the first call succeeded. For at-most-once semantics on hold placement, compose with Duplicate Prevention.
-
Hold on a record that does not exist in the retention system. Record Ref is opaque; the atom does not validate it against a retention store, a document management system, or any other external system. Placing a hold on a non-existent or misspelled Record Ref creates a hold record. The hold is real from this atom’s perspective; the record it names may not be. The composing system is responsible for ensuring Record Ref values are valid. For high-stakes litigation holds, a validation step against the retention store belongs in the composing workflow.
-
Case reference without a formal case. Case Ref is optional precisely because preservation obligations arise before formal litigation is filed — when litigation is “reasonably anticipated” under FRCP, when a regulatory inquiry is received informally, or when an internal investigation is underway without a docket number. Holds without Case Ref are valid; Hold Reason carries the narrative explanation. No amendment mechanism exists; the immutability of Case Ref after placement is load-bearing for legal proceedings. When a formal case reference later attaches to a hold that was placed without one, the correct workflow is to place a new, independent hold carrying the Case Ref, leaving the original hold Active for as long as its underlying preservation obligation persists. The original is not released merely to re-catalog under a case reference — releasing it would write Released At, Released By, and Release Reason for an obligation that has not actually ended, poisoning the audit trail with a release event that an external evaluator could reasonably read as suspicious record manipulation. Invariant 4 (concurrent holds are independent) makes both holds preserve the record in parallel; the new hold’s Hold Reason can narrate the relationship to the original. The original is released only when its preservation obligation genuinely ends.
-
Purge gate enforcement. Whether a record covered by an Active hold is actually prevented from being purged is not enforced by this atom. This is deliberately outside scope: enforcement requires the purge surface to check for Active holds, which requires integrating Legal Hold with the storage or retention layer. That integration is the Regulated Record Retention & Defensible Deletion composition (Legal Hold + Retention Window + Audit Trail). Deployments that query hold records as an advisory check without wiring the gate are non-conforming to the composition’s invariants but conforming to this atom’s invariants.
-
Access control. Who may Place holds, who may Release them, and who may Read them is not defined by this atom. That is the obligation of a composing Permissions pattern. In many deployments, hold placement is restricted to legal counsel or designated compliance officers; unauthorized placement or release of holds is a serious process failure that Permissions governs.
-
Case management and legal matter lifecycle. Tracking the legal matter itself — parties, counsel, status, court, settlement terms, matter type — is out of scope. Case Ref is an opaque pointer into an external case management system. This atom makes no claims about what that system contains.
-
Batch holds across multiple records. One Place call creates one hold on one Record Ref. Bulk holds (all records matching a query, all records in a folder, all records within a date range) are a composing-layer operation. The composing layer iterates the matching records and calls Place for each; the resulting holds are individually releasable. Atomic batch placement — where all records in a batch are held or none are — requires a transaction wrapper in the composing layer.
-
Retention Window interaction. Legal Hold and Retention Window are composing peers. This atom does not import Retention Window semantics; it records preservation obligations. The composition that enforces the purge gate must check both: no purge before
retention_until, and no purge while any Active hold covers the record. -
Tamper-evidence. The atom guarantees immutability by specification; it does not cryptographically prevent a store administrator from altering hold records. For court-admissible evidence of record preservation, compose with Tamper Evidence, which provides cryptographic sealing of the hold records. Tamper-evident hold records are required under several regulatory regimes (SEC Rule 17a-4, 21 CFR (Code of Federal Regulations) Part 11 in regulated clinical contexts).
-
Clock semantics. Placed At and Released At default to the receiving node’s wall clock when not supplied. Placed At must not be in the future — a hold cannot logically be placed in the future. Back-dated Placed At values are accepted; documenting a preservation obligation recognized late is valid and often necessary. Courts scrutinize backdated hold timestamps in spoliation disputes, but the atom records what the caller supplies without interpretation; legal counsel owns the evidentiary consequences. Released At must not be in the future and must be ≥ Placed At (enforced at the Release Decision point). Back-dated Released At values are accepted — documenting a release that was communicated or recognized at an earlier time is valid. Clock skew, timezone normalization, and monotonicity are handled at the deployment layer.
-
Concurrency. Two systems concurrently calling Release on the same Hold Id must be serialized. The first succeeds; the second receives Already Released. Implementations must serialize state transitions on a given Hold Id.
Terms
The canonical concepts this spec refers to. Each [Term] marker in the prose above links to its card here. A card states what the concept is, in plain English, plus its Kind — one of four: Type (a thing or category), Operation (a behavior), Member (a value of an enumerated Type), or, for a named datum, Field (a datum a Type carries — what does it carry?) or Parameter (a value an Operation needs — what does it need?). A card also names the Type it is a Member of / Field of, the Operation it is a Parameter of, and its Role where the domain assigns one. A card carries one Projects line — the concept’s single canonical lowering token, the one place the concrete name stays visible on the page — for every Field, Parameter, and pinned/wire Member. Everything else about casing (each target’s snake / camel / pascal / const / wire form) is derived from that one token by tools/harness/term-adapter.mjs, never hand-written. (annotation.md Terms registry; representational only — it changes no guarantee, invariant, or behavior of the atom above.)
Place
The behavior that records a new preservation hold against a record. It assigns a fresh Hold Id, stamps Placed At, records Record Ref, Placed By, Hold Reason, and Case Ref (if supplied), and returns the Hold Id (or a rejection). The hold enters Active.
Kind: Operation
Release
The behavior that documents the end of a preservation obligation, transitioning an Active hold to Released and recording Released By, Release Reason, and Released At. A Released hold cannot be released again (Already Released); releasing one hold never affects another on the same record (Invariant 4).
Kind: Operation
Read
The read-only behavior that returns the holds matching a Query, ordered by Placed At ascending then Hold Id ascending. It changes nothing. Filters by Hold Id, Record Ref, Placed By, Case Ref, State, or time range are combinable; the query {record_ref: X, state: Active} is the operational “is this record held?” check.
Kind: Operation
Hold Id
The opaque, immutable, system-generated identity of a hold, assigned on Place, never reused or reassigned within the store instance. A non-empty string sortable in lexicographic byte-order — required for deterministic Read ordering. The record, actor, reason, and timestamps are properties of the hold, not its identity.
Kind: Field Field of: the hold record Projects: hold_id
Record Ref
The opaque reference to the record being held. Set on Place, immutable, never validated against any storage layer. Multiple holds may name the same Record Ref; each is an independent hold.
Kind: Field Field of: the hold record Projects: record_ref
Placed By
The opaque reference to the actor placing the hold — the attribution anchor for the preservation decision. Set on Place, immutable; empty or whitespace-only is rejected (Invariant 7).
Kind: Field Field of: the hold record Projects: placed_by
Hold Reason
The required, non-empty narrative for the hold — written from the Reason parameter at Place. Set on Place, immutable (Invariants 1 and 7). Carries the proportionality/scope explanation a court expects.
Kind: Field Field of: the hold record Projects: hold_reason
Placed At
The timestamp the hold was placed — supplied or defaulted to the receiving node’s wall clock; must not be future. Set on Place, immutable. The lower bound for Released At (Invariant 6) and the ordering key for Read.
Kind: Field Field of: the hold record Projects: placed_at
Case Ref
The optional opaque reference to the legal matter, investigation, or audit. Set on Place if supplied (then immutable); its absence is valid. A positive Case Ref filter excludes holds without one.
Kind: Field Field of: the hold record Projects: case_ref
State
The hold’s lifecycle state — Active or Released. Set to Active on Place; transitions once to Released via Release. Every hold is in exactly one state (Invariant 2).
Kind: Field Field of: the hold record Projects: state
Released By
The opaque reference to the actor releasing the hold. Set at Release, immutable; present on Released holds only. Non-null required (Invariant 5).
Kind: Field Field of: the hold record Projects: released_by
Release Reason
The required, non-empty reason for the release — written from the Reason parameter at Release. Set at Release, immutable; present on Released holds only (Invariant 5).
Kind: Field Field of: the hold record Projects: release_reason
Released At
The timestamp the hold was released — supplied or defaulted to wall clock; must not be future and must be ≥ Placed At (Invariant 6). Set at Release, immutable; present on Released holds only.
Kind: Field Field of: the hold record Projects: released_at
Store Name
The identifier of the store instance a hold belongs to. Multiple instances coexist; Hold Ids are unique within an instance, while Record Ref is host-scoped. No action accepts it as a parameter — instance selection is handled at the deployment-routing layer.
Kind: Field Field of: the store instance Projects: store_name
Reason
The required, non-empty reason string Place and Release consume — written into Hold Reason or Release Reason respectively. Not stored under this name; an empty or whitespace-only value is rejected Invalid Request.
Kind: Parameter Parameter of: Place Projects: reason
Query
The selection Read consumes — a filter over Hold Id, Record Ref, Placed By, Case Ref, State, and/or a time range on Placed At or Released At. Supplied per call, not stored; a malformed one is rejected Invalid Query.
Kind: Parameter Parameter of: Read Projects: query
Active
The non-terminal state of a hold whose preservation obligation is in effect. A composition wiring this atom to a purge surface treats an Active hold as blocking purge. May be released or read.
Kind: Member Member of: the hold state Role: Outcome
Released
The terminal state of a hold whose preservation obligation has ended. Carries Released By, Release Reason, and Released At; retained as audit evidence, no further transition (Invariant 3).
Kind: Member Member of: the hold state Role: Outcome
Invalid Request
The refusal Place or Release returns when request fields fail — an empty/whitespace Record Ref, Placed By, Reason, Case Ref, Released By, or Hold Id; a future Placed At; or a Released At that is future or before Placed At.
Kind: Member Member of: the action rejection Role: Outcome Projects: invalid-request
Not Known
The refusal Release returns when the named Hold Id references no hold in this store instance — a lookup miss (a common cause is cross-instance referencing).
Kind: Member Member of: the Release rejection Role: Outcome Projects: not-known
Already Released
The refusal Release returns when the target is already Released — terminal absorption (Invariant 3); a pure guard that writes nothing.
Kind: Member Member of: the Release rejection Role: Outcome Projects: already-released
Storage Failure
The refusal any writing action returns when a durable write fails after preconditions pass. All-or-none: no partial record is observable, and the prior state is unchanged (Invariant 8).
Kind: Member Member of: the action rejection Role: Outcome Projects: storage-failure
Invalid Query
The refusal Read returns when query parameters are malformed — a null/empty/whitespace filter value, a State value outside {Active, Released}, a time range with end before start, or an unrecognized filter key.
Kind: Member Member of: the Read rejection Role: Outcome Projects: invalid-query
Composition notes
Legal Hold is the preservation primitive the library has held open since Retention Window was grounded. Every atom in compliance and every atom in healthcare ultimately composes with it when records must be preserved against destruction:
- Retention Window — the primary composing peer. Legal Hold overrides Retention Window’s purge eligibility: while any Active hold covers a record,
purgemust be rejected regardless of whetherretention_untilhas elapsed. Neither atom enforces the other’s constraint; the gate belongs to the Defensible Retention composition. - Audit Trail — every Place and Release event is an auditable action; Audit Trail provides the tamper-evident, attributed, retention-governed record of every hold lifecycle event.
- Tamper Evidence — seals hold records against post-hoc modification. Court-admissible hold records require cryptographic integrity guarantees beyond this atom’s spec-level immutability.
- Actor Identity — Placed By and Released By are opaque references; Actor Identity provides cryptographic attestation that those references are real, credentialed actors who authorized their respective actions. In regulated contexts (SOX — the Sarbanes-Oxley Act, US law on corporate financial reporting and records integrity; 21 CFR Part 11), hold placement is an electronic record requiring verifiable authorship.
- Permissions — governs who may Place, Release, or Read holds. Legal hold placement is a privileged action in every regulated deployment.
- Duplicate Prevention — for at-most-once semantics on hold placement under retry conditions.
- Medication Order — in healthcare, an active investigation or litigation hold may cover medication order records and their associated clinical observations. Legal Hold composes directly with any record-producing atom when that record’s destruction must be suspended.
- Clinical Observation — same as Medication Order; clinical observations under malpractice litigation or HHS investigation are subject to legal hold.
- Defensible Retention — Legal Hold + Retention Window + Audit Trail, wired to enforce the purge gate. Resolve a Person’s Data Rights — reaches Legal Hold transitively through Defensible Retention: an Active hold is what maps a Defensible Retention
purge_recordblock to theretained(legal-hold)erasure disposition (GDPR Article 17(3)(e)), the operational form of “an erasure request may be declined because a hold preserves the record.”
Standards references
- Federal Rules of Civil Procedure Rule 37(e) — the primary U.S. federal standard for electronic discovery preservation. A party must take reasonable steps to preserve ESI (Electronically Stored Information — digital records subject to legal discovery) once litigation is reasonably anticipated; failure to preserve when an Active hold should have been in place exposes the party to sanctions including adverse inference instructions. The Placed At timestamp and Hold Reason field are the record of when and why the preservation obligation was recognized.
- Federal Rules of Civil Procedure Rule 26(b) — proportionality doctrine for discovery preservation; not all records must be held, only those reasonably expected to be relevant. Hold Reason and Case Ref are the scoping fields that document proportionality.
- Sedona Conference Principles (3rd ed.) — the leading authoritative guidance on electronic discovery preservation obligations. Principle 5: a party is not required to preserve every document; preservation must be proportionate. Principle 6: a party should consider adoption of a litigation hold policy. The Hold Reason field is the policy documentation surface.
- SOX §802 (18 U.S.C. §1519) — criminal obstruction-of-justice provision for destruction of documents subject to federal investigation or proceedings. An Active Legal Hold covering the relevant records is the structural defense against §802 exposure.
- SEC Rule 17a-4(f) — requires broker-dealers to preserve records in non-rewriteable, non-erasable format, accessible to regulators on demand. Legal Hold composes with Tamper Evidence to meet this standard; the hold record itself is a regulated record under 17a-4.
- HIPAA §164.530(j) — documentation retention requirements; HHS investigations trigger preservation obligations over the PHI and administrative records involved. Legal Hold is the preservation mechanism.
- HIPAA Breach Notification Rule (45 CFR §164.400–414) — breach investigations generate preservation obligations over the records relating to the incident. Case Ref references the OCR investigation case identifier.
- GDPR (EU General Data Protection Regulation — the European Union’s data-privacy law) Article 17(3)(e) — erasure right (right to be forgotten) does not apply when processing is necessary for the establishment, exercise, or defence of legal claims. An Active Legal Hold is the operational record that establishes the legal-claim exception to erasure. See Resolve a Person’s Data Rights, whose
retained(legal-hold)disposition is the records-alone form of this exception. - 21 CFR Part 11 — electronic records and signatures in FDA-regulated contexts. Preservation holds on regulated records (clinical trial data, manufacturing batch records) must be attributable and non-alterable. Composes with Actor Identity and Tamper Evidence.
- E-SIGN Act (Electronic Signatures in Global and National Commerce Act) / UETA (Uniform Electronic Transactions Act) — electronic hold records carry the same legal force as paper hold notices where these acts apply.
- ISO 15489-1 (Records management) — the International Organization for Standardization’s standard for records-management practice; hold is the operational mechanism for Section 9.7 (suspension of disposition). The two-state (Active/Released) model maps directly to ISO 15489’s hold lifecycle.
Status
grounded on Final Critique 4 — 2026-05-20 (formal layer complete 2026-06-04 — TLA+ model legal-hold.tla + buggy twin verified; see Lineage §Formal model. Cleared grounded (English) — formal layer pending. Invariant 6 (released_at ≥ placed_at) GAP closed 2026-06-04 — now covered by Inv_TemporalOrdering in the extended two-clock model. Coverage cross-check 2026-06-04 — see tools/harness/coverage/legal-hold.md.) — foundation round (Pass 1 + 2 + 3 author-led), Refinement round 1 (human), and two AI-conducted adversarial rounds complete: Refinement round 2 (Sonnet, batched with Consent and Soft Delete) and Refinement round 3 (Opus single-atom, Torvalds X2 posture). All nine GRID (the nine-node structural-completeness framework: Intent, System, Friction, Flow, Decision, Feedback, State, Behavior, Proof) nodes resolved; all concepts independent; all surfaced adversarial gaps closed in-pattern or named as explicit out-of-scope. Under the unified methodology (3×3 baseline rounds with per-round Pass 1/2/3 numbering + Final Critique starting at Round 4), this pattern’s Refinement round 3 (Opus single-atom, Torvalds X2 posture) is retro-labeled Final Critique 4; the original round-naming in the Lineage notes below is preserved as historical record.
Lineage notes
Regulated atom. Conventions — Regulated adversarial scenarios and Generation acceptance — inherited from the methodology directly (pressure-testing.md), baked in from the first draft. Retention Window and Actor Identity are the reference shapes for regulated compliance atoms.
Pass 1 — Structural completeness (GRID). Three findings, all closed in-pattern.
-
Store instance model absent. Initial draft referenced “the store” without defining instance topology. Parallel finding to Medication Order and every other atom that composes against a store. Fixed: Store instance model subsection added, mirroring Retention Window.
hold_iduniqueness scoped to instance;record_refnoted as globally scoped; instance selection named as deployment-routing concern. -
Outputs section under-specified. Initial draft listed only
hold_idandreleasedwithout enumerating which fields are present on which state of hold record. Fixed: Outputs now explicitly names core fields present on every hold and state-specific fields (release fields present only on Released holds), parallel to Medication Order’s cumulative field listing. -
readordering and filter semantics not defined. Thereadaction had no stated ordering, no specification of what constitutes a well-formed vs. malformed query, and no statement of what happens on an empty result. Fixed:readaction description updated to specifyplaced_atascending ordering, enumerate valid filter axes, nameinvalid-queryconditions, and state that a well-formed query matching no holds returns an empty sequence.
All nine GRID nodes resolved.
Pass 2 — Conceptual independence (EOS). Clean. Three extraction candidates evaluated; all kept in-pattern.
-
case_refas over-absorption candidate. Couldcase_refimply that case management belongs in-atom? Evaluated:case_refis an opaque advisory reference — the atom does not interpret it, does not import case management lifecycle semantics, and does not name any case management pattern in its specification. Parallel toclinical_evidence_refin Medication Order. The atom records that a hold is associated with a case; it does not model the case. Clean. -
Two-state machine as over-simplification hiding a richer lifecycle. Could Legal Hold have intermediate states — e.g., Pending (hold requested, not yet confirmed), Disputed (opposing party contests the hold scope), Suspended (hold temporarily narrowed)? Evaluated: these are workflow-layer concerns. A Pending state would require an approval surface, which belongs to Approval Step (roadmap atom #4). A Disputed state would require legal adjudication tracking, which belongs to a case management composing pattern. A Suspended state would require a second reversibility mechanism identical to hold mechanics — a hold on the hold. None of these is freestanding relative to Legal Hold; each imports concerns from other atoms or the composing layer. The two-state machine (Active → Released) is correct EOS design for this atom. Extraction is not warranted.
-
Hold independence invariant as a hidden composition rule. Could “concurrent holds are independent” mean that the atom is secretly composing two or more holds into an aggregate held state? Evaluated: no. Each hold is its own record with its own lifecycle. Independence is a constraint on the atom’s behavior (releasing hold H does not affect hold H′), not a composition. The aggregate held query
{record_ref: X, state: Active}is a read operation on the hold set, not a separate state. Clean.
Refinement round 1 — 2026-05-13. Six findings across the three passes, all closed in-pattern.
-
readordering tiebreaker absent. Batchplacecalls occurring in the same instant produce holds with identicalplaced_atvalues; ordering between them was non-deterministic. An audit tool issuing the same query at two different times could receive results in different order, breaking deterministic replay. Fixed:readaction now specifiesplaced_atascending, thenhold_idascending as a stable tiebreaker. -
releasedid not enforcereleased_at ≥ placed_atat the action level. Invariant 6 claimed the ordering bound but the Decision point only checked thatreleased_atis not in the future — a caller supplying a backdatedreleased_atearlier thanplaced_atcould write an incoherent Released hold. Fixed: Decision point updated to requirereleased_at ≥ placed_at;invalid-requestif violated. Clock semantics edge case updated to explicitly state backdatedreleased_atis accepted (no lower bound beyondplaced_at). -
released_attime range filter onstate: Activequeries was unaddressed. A well-formed query{state: Active, released_at: {after: X}}was ambiguous — it could be read asinvalid-queryor as an empty result. The general rule (well-formed query matching no holds → empty sequence) applied, but the specific case of filtering on a field that Active holds don’t carry was not called out. Fixed:readaction now explicitly states this case returns an empty sequence. -
Flow step 4 stated composing-layer semantics. “The record returns to normal retention governance” is a claim about what Retention Window and the purge gate do — not what this atom does. The atom only knows that no Active holds remain. Fixed: tightened to “the composing layer resumes normal retention governance.”
-
Non-idempotency cross-reference to Duplicate Prevention in Behavior section. The Behavior bullet named Duplicate Prevention inline; per CLAUDE.md conventions, cross-atom references belong in Edge cases, not the spec’s Behavior section. The same reference already existed in the Edge cases section. Fixed: removed the cross-reference from the Behavior bullet; Edge cases coverage is complete.
Pass 3 — Adversarial scrutiny (Linus mode) — foundation round. Six findings, all closed in-pattern.
-
Hold placed after record purged — spec was silent. The initial draft said nothing about
placeagainst arecord_refwhose underlying record has already been destroyed. A reader could assume the atom validatesrecord_refagainst the retention store (it does not) or that such a hold is rejected (it is not). This is a real and consequential scenario in spoliation litigation — placing a hold after destruction does not undo the destruction, and hiding this from the spec would be dishonest. Fixed: Edge case Hold placed after record is purged added, explicitly stating that the hold is created,record_refis not validated against the storage layer, and legal counsel assesses the spoliative implications. The atom records the truth; it does not adjudicate. -
releaseof a hold on a record with other Active holds — consequence not stated. The initial draft said nothing about what happens to sibling holds. Invariant 4 covered independence but the action description did not explicitly state it. A reader of thereleaseaction alone would not know whether releasing a hold affects others. Fixed:releaseaction description updated — “Releasing a hold on a record does not affect any other hold on the same record.” -
released_atfuture-timestamp restriction not specified.placed_atfuture restriction was stated (parallel toordered_atin Medication Order), butreleased_atwas silent. A release documented as occurring in the future is not operationally meaningful and could create misleading audit records. Fixed:releaseaction description and Decision point updated to statereleased_at, if supplied, must not be in the future. -
Temporal ordering between
placed_atandreleased_atnot guaranteed by any invariant. A Released hold withreleased_at < placed_atviolates basic temporal coherence and would produce a nonsensical audit record. No invariant named this. Fixed: Invariant 6 added — “For every Released hold,released_at ≥ placed_at.” Generation acceptance check 3 verifies it. -
Purge gate enforcement presented ambiguously. The initial draft implied the atom enforces the purge gate (“the record may not be purged while this hold is Active”). This is accurate as an obligation but misleading as an implementation claim — the atom records the obligation; the composition enforces it. An implementor reading the spec could incorrectly conclude the atom itself intercepts purge calls. Fixed: Behavior section updated — “The atom does not enforce the purge gate” — with explicit rationale; Intent extended to explain why enforcement belongs to the composition. Edge case Purge gate enforcement added naming the conformance distinction.
-
placenon-idempotency not stated. Same class of finding asorderin Medication Order andplace_under_retentionin Retention Window. A legal system that retriesplaceafter a network timeout creates a duplicate hold; both are Active against the record. Fixed: Edge caseplaceis not idempotent added, pointing to Duplicate Prevention for at-most-once semantics.
Refinement round 2 — AI-conducted adversarial round (Sonnet, batched with Consent and Soft Delete). Surfaced no findings beyond those already closed in Refinement round 1. Recorded as a clean round for reproducibility per the methodology requirement that AI rounds be named and dated; the batched-attention caveat motivated the subsequent single-atom Opus round documented below.
Refinement round 3 — AI-conducted adversarial round (Opus single-atom, Torvalds X2 posture) — 2026-05-13. Reviewer: Claude Opus, low-patience adversarial posture, full pass-question set from pressure-testing.md applied, no recourse to prior rationales. Thirteen findings surfaced, all closed in-pattern. The pass exposed two classes of defect the cooperative rounds and the batched Sonnet round missed: (a) fixes from Refinement round 1 that carried their own gaps — the released_at ≥ placed_at enforcement applied only to caller-supplied values, leaving the wall-clock-defaulted case unenforced — and (b) terminological and categorization drift that quietly contradicts the spec’s own validation rules.
-
Invariant 6 enforcement gap on defaulted
released_at. Refinement round 1 added thereleased_at ≥ placed_atDecision-point check but qualified it with “if supplied.” A wall-clock-defaultedreleased_aton a clock-skewed node could yield a value <placed_at, writing a Released hold that violates Invariant 6 while the action accepts it. Fixed:releaseaction description and Decision point now require the bound be enforced against the resolvedreleased_at, regardless of whether it was caller-supplied or defaulted. Invariant 6 statement extended to note that the constraint applies to the persisted value and that enforcement is at the Decision point against the resolved value. -
“Non-empty” terminology inconsistent with the validation rule. Invariants 5 and 7 said “non-empty”; Decision points and action signatures rejected whitespace-only strings under “at least one non-whitespace character.” An implementer reading the invariants alone would have accepted whitespace-only attribution strings. Fixed: invariants 5 and 7 rewritten to say “at least one non-whitespace character” explicitly, matching the Decision points.
-
Type confusion — “non-empty” applied to timestamps. The same invariants listed
released_atandplaced_atamong “non-empty” fields. Timestamps are not strings. Fixed: invariants split by field type — strings require “at least one non-whitespace character”; timestamps require “set.” -
Intent overclaimed verifiability without naming Tamper Evidence. The Intent paragraph asserted “verify from the records alone” without qualifying that cryptographic protection against post-hoc modification is provided by Tamper Evidence composition. The Edge case “Tamper-evidence” contradicted the Intent. Fixed: Intent paragraph rewritten to claim only that the chain is present in the records and immutable by specification, and to name Tamper Evidence as the composition that adds cryptographic protection for court-admissible evidence.
-
Status mis-categorized as
unresolved. Perpressure-testing.md, patterns that have completed human refinement but not the AI round declarepartially resolved;unresolvedis the label for “nothing done.” Fixed: with this Opus round complete and all findings closed, Status advances togrounded — 2026-05-13per the same methodology section. -
AI adversarial round (Sonnet) not recorded in Lineage notes. The methodology requires AI rounds be recorded as such, named with the model, for reproducibility. The prior Sonnet round was named only in Status. Fixed: Refinement round 2 entry added above, recording the Sonnet round as a clean AI round and naming the batched-attention caveat that motivated the Opus follow-up.
-
case_refmis-categorized as a “state-specific” field in Outputs.case_refis optional and independent of state — present on Active or Released holds if supplied at placement. Grouping it withreleased_*fields under “state-specific” misrepresented the field’s nature. Fixed: Outputs separates “optional fields set at placement” (case_ref) from “state-specific fields” (the release fields), and notes that Released holds carry placement fields includingcase_refif it was supplied. -
Misleading “amendment” advice poisoned the audit trail. The Edge case “Case reference without a formal case” advised that
case_refis “added retroactively by placing a new hold with the case reference and releasing the original.” Releasing the original writesreleased_at,released_by,release_reasonfor an obligation that has not ended — an audit signal an external evaluator could reasonably read as suspicious record manipulation. Fixed: edge case rewritten. The correct workflow is to place a new independent hold with thecase_refand leave the original Active for as long as its preservation obligation persists; Invariant 4 makes both holds preserve in parallel. The original is released only when its preservation obligation genuinely ends. -
releaseDecision point silent on malformedhold_id. A null, empty, or whitespace-onlyhold_idparameter would have fallen intonot-known(no hold has that id), telling the caller the wrong thing. Fixed:releaseaction and Decision point now checkhold_idsyntactic validity first; a malformedhold_idisinvalid-requestwith priority over the existence check. Rejection priority list updated. -
Generation acceptance check 6 — “identical field values” assertion was over-strong. The check asserted that every hold in S1 (the unfiltered read at t1) appears in S2 (at t2) with identical field values. A legitimate
releasebetween t1 and t2 would add release fields to a hold present in both sets, failing the check on a conforming system. Fixed: check 6 rewritten. The placement fields must be unchanged (Invariant 1); release fields may newly appear on holds released in the interval (legitimate state machine transition); the count in S2 is ≥ S1. The reverse transition (Released → Active) is named as the actual conformance violation. -
“Supplied” semantics undefined for optional parameters.
placed_at?andreleased_at?said “wall clock if not supplied” but did not say what counts as “not supplied” — absent, null, empty all plausible. Fixed: a definition statement added before the action list. For optional parameters, “supplied” means provided as a parseable value of the declared type; null, missing, and empty (or whitespace-only) values are equivalent to “not supplied” and the default applies. -
readDecision point ambiguous on null/empty filter parameters. The original wording — “a syntactically invalidhold_id(non-null, non-empty)” — left a reader unable to tell whether null or empty filter values were rejected or accepted as degenerate filters. Fixed:readaction and Decision point now state explicitly that null, empty, or whitespace-only filter values onhold_id,record_ref,placed_by, andcase_refareinvalid-query. Strict over silent — silent-ignore would return a result set inconsistent with the caller’s intent. -
Unknown filter keys in
readnot specified, andcase_reffilter on absent-case_ref holds not specified. The spec listed supported filter axes but did not say what happened if a caller passed an unknown axis name, or how a positivecase_reffilter handled holds withoutcase_ref. Fixed:readaction and Decision point now reject unknown filter keys asinvalid-query(strict rather than silent-ignore) and state that a positivecase_reffilter excludes holds withoutcase_ref. A “find holds with nocase_ref” predicate is named as out-of-scope; deployments needing it add it in the composing layer.
Scheduled rescan: 2026-05-20 — clean.
Formal-layer vote — 2026-06-03: YES (model pending). Invariant 4 (concurrent holds independent — releasing one does not affect another on the same record) is an interleaving isolation claim; Invariant 6 adds temporal ordering. Load-bearing temporal/ordering/safety claims a derived formal model would verify; none exists yet, so the pattern is downgraded to grounded (English) — formal layer pending until the model is authored and verifies (findings flow back into this English spec per the conflict protocol). Vote per pressure-testing.md §Formal models — The formal-layer vote.
Formal model — 2026-06-03: TLA+ authored and verified; pattern promoted to grounded. Derived model legal-hold.tla + config legal-hold.cfg, checked by tla-checker via tools/harness/check.mjs. What it checks: up to MaxH = 3 holds over one record, each {none, Active, Released}; the load-bearing Invariant 4 (concurrent-hold independence — releasing one hold does not affect another) via a ghost releasedByOwn[k] (TRUE only if hold k was the release target): Inv_HoldIndependence == ∀ k : holds[k] = Released ⇒ releasedByOwn[k]. Exhaustive: 27 states, holds. Buggy twin legal-hold-buggy.tla cascades release to every Active hold (conflating per-hold release with record-level release); rejected at 12 states (place 2 holds, release one → the other is Released without its own action). Out of model scope: id discipline, attribution fields, the purge gate (the Defensible Retention composition — see compositions/defensible-retention.tla). Conflict-protocol outcome: none — the model corroborates the English; canonical English unchanged.
Formal model extension — 2026-06-04: Invariant 6 GAP closed; Inv_TemporalOrdering added. Extended legal-hold.tla with a global monotonically-advancing bounded clock now (a Naturals counter in 0..MaxClock, advanced by a Tick action; no Sequences module — per harness README) and two per-hold ghost timestamp arrays: placedAt[k] (stamped with now on Place) and releasedAt[k] (stamped with now on Release). New invariant: Inv_TemporalOrdering == ∀ k ∈ 1..MaxH : holds[k] = "Released" ⇒ releasedAt[k] ≥ placedAt[k]. Because now only advances monotonically, the correct model holds exhaustively — release always stamps a tick ≥ the placement tick. Inv_HoldIndependence is retained and still checked in Safety. Config updated: MaxH = 2, MaxClock = 3; Init assigns all five variables. State count: 370 states (MaxH=2, MaxClock=3); raised to MaxClock=4 → 811 states, invariants still hold. The state count grows with MaxClock because more absolute timestamp combinations exist — this is inherent to a timestamp-stamped model; no new behavioral patterns (ordering violations) emerge at higher bounds, confirming the temporal-ordering claim is fully exercised at MaxClock=3. Buggy twin legal-hold-buggy.tla updated: Release stamps releasedAt[k] = 0 (backwards/skewed clock) instead of now; any hold placed at tick > 0 yields releasedAt < placedAt, violating Inv_TemporalOrdering. Rejected at 18 states (Tick → Place → Release sequence). The Inv 4 cascade-release hazard is retained as a second isolated twin, legal-hold-buggy-cascade.tla (cascade release flips every Active hold to Released but marks releasedByOwn only for the target; rejected at 25 states on Inv_HoldIndependence, with Inv_TemporalOrdering holding so the failure is isolated). The two isolated twins — legal-hold-buggy.tla (Inv 6) and legal-hold-buggy-cascade.tla (Inv 4) — give each load-bearing invariant its own reachable, checker-rejected counterexample rather than letting the shorter violation mask the longer; both are auto-discovered and required-to-reject by tools/harness/audit.mjs. Conflict-protocol outcome: none — the extension corroborates the English; released_at ≥ placed_at as enforced at the release Decision point is exactly what the model verifies.
Showcase pass — 2026-06-29. Representational-only annotation/legibility pass; no guarantee, invariant, number, formula, signature, or rejection taxonomy changed. (a) Four-kind [Term] annotation applied across the body and a ## Terms registry added before Composition notes (23 terms): 3 Operations (Place, Release, Read); 11 Fields (Hold Id, Record Ref, Placed By, Hold Reason, Placed At, Case Ref, State, Released By, Release Reason, Released At, Store Name — the hold record carries no Type card, mirroring the plain-noun “hold record” referent); 2 Parameters (Reason, Query); 7 Members (Active, Released states; Invalid Request, Not Known, Already Released, Storage Failure, Invalid Query rejections). The one labeled projected-contract signature per Operation, the release success token released, concrete example invocations and their returns, the non-existent release_all, peer-atom tokens (retention_until, purge, purged_at, purge_record, retained(legal-hold)), model filenames, and dated registers stay backticked as survivors. Wall-clock timestamps are stamped from the receiving node’s clock (this atom is not in the seam-injected-clock family — no [Now] term). (b) Summary/blockquote merge — ## Summary moved to the top (after TOC, before Intent), the descriptive top blockquote folded out after confirming each claim is carried by Summary/Intent/State/Invariants; no also-known-as line existed, so none was invented. (c) Lineage collapsed into a <details markdown="block"> block. (d) prose cut #1 — the densest Summary run-on split into one-idea sentences, lossless. (e) prose cut #5 — the State “Valid transitions” list rendered as a transition table (action / from / to / guard / stamps / result), with terminal-absorption and concurrent-hold-independence semantics kept in the prose beside it. Re-verified, not re-grounded: Status stays at grounded on Final Critique 4 — 2026-05-20. Gates: lint clean (O-term resolver — every marker resolves and every card is used); term-adapter derives cleanly (23 terms); 8 invariants preserved; .tla/.cfg untouched — harness re-run green: legal-hold.tla PASS, legal-hold-buggy.tla --buggy rejected (Inv 6), legal-hold-buggy-cascade.tla --buggy rejected (Inv 4).