Part III — System Architecture · Chapter 23
Procedural-Legal Primitives
Procedural-Legal Primitives
The Canon layer composes with — does not replace — a litigation-aware data model. Cryptographic signatures prove that an artifact has not been altered. They say nothing about whether it was privileged, produced, or subject to a hold. That is the substrate's job.
ℹPrerequisites▼
Before reading this chapter, you should be comfortable with: Chapters 5–7, 13 (Hashing, Signatures, Canonicalization, Pipeline). The primitives (emit, emit_dsse) are the culmination of the cryptographic pipeline.
A SHA-256 hash is a statement about bytes. An Ed25519 signature is a statement about who endorsed those bytes at what moment. These are powerful statements. They are also insufficient.
Courts do not deal in bytes. Courts deal in documents, privileges, parties, productions, and holds. A piece of evidence that is cryptographically authentic but inadmissible because it was produced in violation of a privilege assertion is worthless — worse than worthless, because producing it may waive the privilege you were trying to protect. A document that is authenticated but deleted because no one put a legal hold on it may result in a sanction that changes the outcome of the case.
The procedural-legal primitives in schema/85_legal.sql are what bind Meridian-Cannon's cryptographic guarantees to the procedural reality of litigation. They are not optional features. The CLAUDE.md for this repository contains an explicit prohibition: do not remove the procedural-legal primitives. The Canon spec depends on them as substrate. This chapter covers each primitive in turn: what it represents, how the schema implements it, and what litigation consequence follows if it is absent. ## At a glance - Privilege, legal holds, and productions are database-enforced, not application-layer conventions: a trigger or RLS policy that fires unconditionally cannot be bypassed by a force=True flag, a raw psql command, or an ORM that forgets to add a WHERE clause. - RLS is the enforcement mechanism: it attaches a WHERE clause to every query against a table below the application layer, so a session that lacks the correct role sees an empty result set, not an error that could be caught and retried. - Every production carries a hash manifest (hash_manifest_uri) that freezes the identity of produced bytes at the production date, creating a chain-of-custody record that survives later modifications to the underlying documents. - DSSEEnvelope is a first-class primitive alongside Attestation and Seal. The emit() function produces a Seal; the emit_dsse() function (v0.2.0) produces a DSSEEnvelope using the Dead Simple Signing Envelope format, which wraps the attestation payload in a standard PAE (Pre-Authentication Encoding) structure independently verifiable by any DSSE-compatible tool. ## Learning objectives By the end of this chapter you should be able to: 1. Create a privilege assertion for a document, specifying privilege_type, basis, and asserted_by_actor, and verify that a session with a non-privileged role cannot read the asserted chunk via the RLS policy. 2. Implement a database trigger on sources that checks for active legal holds and raises an exception on DELETE, then verify it fires on a held source and does not fire after the hold is released. 3. Explain what FRCP 26(b)(5)(A) requires of a withheld-document log and show how withheld_documents.description_for_log satisfies that requirement without revealing the privileged content. 4. Use pii_tier_rank() to determine whether a specific actor's pii_ceiling permits reading a document at a given evidentiary_pii_tier, and identify the behavior when pii_tier_rank() receives an unrecognized tier string. ## Why these are in the database, not application code Application code can be bypassed. A developer adds a force=True flag. A script issues SQL directly. An API call is misrouted. Any check that lives only in application code can be circumvented — intentionally or otherwise — by anyone with database access. Database constraints cannot be bypassed by application code. A trigger that raises an exception on DELETE fires regardless of who issues the DELETE — a Python worker, a raw psql command, an ORM, or an emergency script run at 2 AM by someone who is certain the hold does not apply. The trigger fires anyway. Row-Level Security policies in Postgres enforce access control at the query layer, below the application. A session that does not have the owner or counsel role cannot read privilege_assertions, period — not because the application refuses to serve those records, but because Postgres never returns them. The RLS policy in 99_rls.sql is unconditional:
CREATE POLICY pa_role ON privilege_assertions FOR SELECT
USING (current_actor_role() IN ('owner', 'counsel'));
◆ Going Deeper — FORCE ROW LEVEL SECURITY.
PostgreSQL RLS policies apply to all users except the table owner and superusers, who bypass them by default. In a single-operator deployment where the application role and the superuser are controlled by the same person, RLS is a boundary against application-layer bypasses, not against the database operator themselves. To enforce policies even against the table owner, run:
ALTER TABLE privilege_assertions FORCE ROW LEVEL SECURITY; ALTER TABLE legal_holds FORCE ROW LEVEL SECURITY; ALTER TABLE privilege_log FORCE ROW LEVEL SECURITY;Without
FORCE ROW LEVEL SECURITY, a session connecting as the table owner role can read any row regardless of the policy. For evidence that carries legal privilege, this command is not optional. In a production Meridian-Cannon deployment, confirmFORCE ROW LEVEL SECURITYis set on every sensitive table before any privileged document is ingested. --- Any session whosecurrent_actor_role()is notownerorcounselsees an empty result set fromprivilege_assertions, as if the table contained no rows. That session cannot produce a privileged document because it cannot discover that the document is privileged, and it cannot query the unprivileged chunk text because the chunks RLS policy checks for active privilege assertions:
AND NOT EXISTS (
SELECT 1 FROM privilege_assertions pa
WHERE pa.resource_type = 'chunk' AND pa.resource_id = chunks.id
AND NOT pa.waived
AND current_actor_role() NOT IN ('owner', 'counsel')
)
This is enforcement at the query layer, not the application layer. No application code needs to check for privilege before querying chunks; the database enforces it. Application code that does check is adding a second layer of defense, which is welcome — but the first layer is not optional.
Legal constraints that must be reliably enforced belong at the database layer. Application code is where convenience lives. The database is where invariants live.
Privilege assertions
Attorney-client privilege protects candid communications between client and counsel from disclosure to adversaries. Waiving it is easy: disclosing a privileged document to an opposing party is typically a waiver for everything related to that communication. Inadvertent waiver is a catastrophic outcome.
The privilege_assertions table is the mechanism that prevents inadvertent
disclosure. A privilege assertion is a record that a specific resource —
document, chunk, email, message, recording, transcript — is privileged under
a specific doctrine, asserted by a specific actor, with a specific legal
basis.
The schema carries everything needed for a privilege log:
CREATE TABLE privilege_assertions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
matter_id uuid REFERENCES matters(id),
resource_type text NOT NULL CHECK (resource_type IN (
'document', 'chunk', 'email', 'message',
'recording', 'transcript', 'note',
'social_post', 'activity_event'
)),
resource_id uuid NOT NULL,
privilege_type text NOT NULL CHECK (privilege_type IN (
'attorney_client', 'work_product',
'expert_consulting', 'joint_defense',
'common_interest', 'clergy', 'spousal',
-- ... and others
)),
basis text NOT NULL, -- statute / rule / case cite
asserted_at timestamptz NOT NULL DEFAULT now(),
asserted_by_actor uuid REFERENCES actors(id),
waived boolean NOT NULL DEFAULT false,
waived_at timestamptz,
waived_to_party_id uuid REFERENCES parties(id),
waiver_basis text
);
The waived column is load-bearing. Privilege can be intentionally waived — for example, when a party chooses to disclose attorney advice in support of a good-faith defense. The waiver_basis field records why. The waived_to_party_id records who received the disclosure. A waiver to opposing counsel in one case does not constitute waiver to the world; this schema supports that distinction by recording the specific party. The partial index privilege_active_idx covers only non-waived assertions:
CREATE INDEX privilege_active_idx ON privilege_assertions(resource_type, resource_id)
WHERE NOT waived;
This is the index the chunks RLS policy scans. It is intentionally narrow: waived assertions are not checked because they no longer restrict access. Unwaived assertions are. The index makes this check fast regardless of how many historical (waived) assertions accumulate.
The privilege_type check constraint carries every privilege recognized under Wisconsin and federal practice: attorney_client, work_product, expert_consulting, joint_defense, common_interest, hipaa_protected, minor_child_welfare, and others. This is not an exhaustive list for all jurisdictions; it is the vocabulary for this matter type. If a privilege type is not in the constraint, it cannot be inserted — which prevents a paralegal from asserting "attorney_vibes" as a privilege type on a document they want to hide from opposing counsel. --- > ▼ Why It Matters. > > In the running case, Isabel's attorney reviews the DHS casefile and makes > annotations in the system — annotations that reflect her legal strategy. > Those annotations are notes linked to the casefile via privilege assertions > with privilege_type='work_product'. When opposing counsel requests > production of the casefile, the production queue includes only the casefile > itself, not Isabel's attorney's annotations. Without the privilege_assertions > table, and without the RLS policy enforcing it, the production worker would > query chunks for all content related to the casefile and include everything > in the production bundle — the casefile and the strategy notes both. > > The harm of inadvertent production goes beyond the disclosure itself. > Under FRE 502(b), inadvertent disclosure of privileged material can waive > the privilege unless the holder took "reasonable steps to prevent > disclosure." A privilege_assertions table with RLS enforcement is a > documented, database-layer reasonable step. It is harder to argue > inadvertence when the database physically prevented the production worker > from seeing the document in the first place. --- > § For the Record — FRE 502(b). > > "When made in a federal proceeding or to a federal office or agency, the > disclosure does not operate as a waiver in a federal or state proceeding > if: (1) the disclosure is inadvertent; (2) the holder of the privilege or > protection took reasonable steps to prevent disclosure; and (3) the holder > promptly took reasonable steps to rectify the error, including (if > applicable) following Federal Rule of Civil Procedure 26(b)(5)(B)." > > Rule 502(b) creates a safe harbor for inadvertent disclosure, but only if > the holder took reasonable steps. A privilege_assertions table with > database-layer enforcement, combined with a withheld_documents log for each > production, is the kind of systematic documentation that satisfies the > "reasonable steps" standard. --- ## Legal holds Spoliation is the destruction, alteration, or concealment of evidence after a party has, or reasonably should have, anticipated litigation. In federal courts the consequence ranges from adverse-inference instructions to default judgment. Wisconsin circuit courts apply the same analysis under Wis. Stat. § 804.12(6). The legal_holds table is the database-layer enforcement of the duty to
preserve:
CREATE TABLE legal_holds (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
matter_id uuid NOT NULL REFERENCES matters(id),
hold_name text NOT NULL,
scope_description text NOT NULL,
scope_query jsonb,
imposed_at date NOT NULL,
released_at date,
imposed_by_actor uuid REFERENCES actors(id),
notes text
);
The scope_description field — required, not nullable — records what the hold covers in plain language: "all communications with the Harrow County DHS between 2022-01-01 and 2024-12-31." The scope_query jsonb carries a structured version of that scope that application code can use to evaluate whether a given resource falls within the hold. Together they constitute the hold notice. A legal hold is imposed at a date (imposed_at), not a timestamp. The date is what matters legally — the question is whether the hold was in place on a specific calendar date, not at a specific minute. The released_at date records
when the hold ended, if it has ended. The partial index covers only active
holds:
CREATE INDEX legal_holds_matter_idx ON legal_holds(matter_id)
WHERE released_at IS NULL;
The CLAUDE.md for this repository says the trigger that prevents deletion of
held sources "raises an exception" at the database layer. The schema's
legal_holds table defines the hold; the enforcement mechanism should be a trigger on the sources table that checks for active holds before allowing a DELETE. That trigger is the production enforcement point. Application code that checks for holds before issuing a DELETE is adding a second layer — correct practice — but the trigger is the layer that cannot be bypassed. What the hold enforces: once a legal hold is in place for a matter, no source row associated with that matter can be deleted while the hold is active. The hold does not prevent updates (a parsed_at timestamp being set, for instance); it prevents deletion. Deletion is the action most associated with spoliation. --- > ☉ In the Wild — Zubulake v. UBS Warburg. > > Between 2003 and 2004, Judge Shira Scheindlin of the Southern District of > New York issued five opinions in Zubulake v. UBS Warburg that collectively > established the modern framework for electronic discovery and the duty to > preserve. In Zubulake IV (220 F.R.D. 212, S.D.N.Y. 2003), Judge Scheindlin > held that the duty to preserve evidence "arises when the party has notice > that the evidence is relevant to litigation or when a party should have > known that the evidence may be relevant to future litigation." > > UBS failed to preserve backup tapes containing emails relevant to Laura > Zubulake's gender-discrimination claim. The court found that the duty to > preserve had attached before the tapes were overwritten in the normal > course of UBS's routine backup-rotation policy. The sanction in Zubulake V > (229 F.R.D. 422, S.D.N.Y. 2004): an adverse-inference instruction. The jury > was told to presume that the lost emails would have supported Zubulake's > claims. UBS ultimately settled. > > A legal_holds table with a trigger preventing deletion would have made > this impossible — not because it would have changed the underlying conduct, > but because it would have made the routine backup-rotation policy unable to > touch materials within the hold's scope. The backup system would have > encountered an exception on the DELETE, logged the attempt, and left the > tapes in place. The duty to preserve, codified at the database layer, > would have survived the routine. > > Zubulake is taught in every evidence course. The lesson is usually framed > as "implement a litigation hold." The engineering lesson is: implement it > at the database layer, not the policy layer. --- > ✻ Try This. > > In your database, insert a legal hold for the TPR matter: imposed_at > today, scope_description "all communications and documents related to
matter 2024JC000099, Harrow County, WI." Then attempt to DELETE a source row that is associated with that matter. If your database has a trigger enforcing the hold, you should receive an exception. If you do not receive an exception, the enforcement is missing — the hold exists in the table but is not enforced. Document which case you are in. The existence of the hold table without the trigger is a false sense of security.
Productions
A production is the formal disclosure of documents to another party — not an export, but a legal event with a date, a Bates range, a format, and a recipient. The production record is frozen at the moment of production. The snapshot of what was sent does not change afterward, even if the underlying documents are revised, re-redacted, or destroyed.
CREATE TABLE productions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
matter_id uuid NOT NULL REFERENCES matters(id),
direction text NOT NULL CHECK (direction IN ('outbound', 'inbound')),
produced_by_party_id uuid REFERENCES parties(id),
produced_to_party_id uuid REFERENCES parties(id),
production_date date NOT NULL,
bates_prefix text,
bates_start int,
bates_end int,
format text CHECK (format IN (
'native_only', 'image_only',
'native_image_text_load', 'concordance_dat_opt',
'relativity_dat', 'edrm_xml', 'pdf_bundle', 'mixed'
)),
hash_manifest_uri text,
frozen_at timestamptz NOT NULL DEFAULT now()
);
The direction column reflects the two-sided nature of production. Outbound: you produce to opposing counsel. Inbound: the DHS produces to you. Both are tracked in the same table because both are legally significant events. The direction column controls which queries they appear in. The hash_manifest_uri points to a file listing every document in the production with its SHA-256 hash. This is the production's own chain-of-custody record: if opposing counsel later claims a document was altered after production, the hash manifest settles it. The production date plus the hash manifest is evidence that these specific bytes, with these specific hashes, were disclosed on this specific date. Bates numbers — bates_prefix, bates_start, bates_end — are the traditional pagination scheme for productions. SMITH000001 through SMITH003471. The production record defines the range; individual documents in the production have their Bates number assigned in production_documents (defined in schema/30_documents.sql). The range in the productions table is for reference; the document-level assignment is authoritative. The production_recipients table handles the case where a production goes to multiple parties — the court receives a copy, opposing counsel receives a copy, and a third-party mediator receives a copy. Three recipients, one production, three rows in production_recipients. The delivery method (efile, email, mail, hand) and acknowledgment reference are tracked per recipient. --- > ◆ Going Deeper — privilege_assertions vs. productions: independent controls. > > A common misconception: privilege_assertions and productions interact such > that a privileged document can never be produced. This is wrong on two > levels. > > First, privilege can be waived for production. A party can choose to produce > a privileged document — for example, to support an advice-of-counsel defense, > which waives attorney-client privilege over all communications related to > that advice. The privilege_assertions.waived flag records this. A waived > assertion does not block production. > > Second, a non-privileged document can be withheld from production for other > reasons — irrelevance, proportionality, a protective order, or a court > ruling. The withheld_documents table tracks documents excluded from a > specific production along with the reason; the reason need not be privilege. > > These are two independent dimensions: > > | | Privileged | Not privileged | > |-----------------------|---------------------|------------------------| > | Produced | Waived privilege | Standard production | > | Withheld | Standard privilege | Court order / other | > > Any cell in this table is a legally valid state. The schema supports all > four. Application logic that collapses "privileged" into "not produced" is > wrong; it blocks the common and legally important case of producing a > document while waiving its privilege. --- ## PII tiers Not every document has the same sensitivity. A court docket entry is public record; it can be shown to anyone. A medical record is protected under HIPAA and Wisconsin ch. 51; it requires specific consent for disclosure. A note made by Isabel's attorney reflecting legal strategy is work product; it cannot be produced at all except by waiver. The PII tier system in Meridian-Cannon maps this range to a six-level vocabulary, defined in actors.pii_ceiling and mirrored in sources.default_pii_tier and chunks.pii_tier:
public — court dockets, filed pleadings, open records responses
low — names, addresses, public contact information
internal — case notes not yet determined to be privileged
sensitive — medical records, financial records, child welfare records
privileged — attorney-client communications
work_product — attorney mental impressions, strategy
The ordering is enforced by pii_tier_rank() in schema/10_core.sql:
CREATE OR REPLACE FUNCTION pii_tier_rank(tier text) RETURNS int AS $$
SELECT CASE tier
WHEN 'public' THEN 0
WHEN 'low' THEN 1
WHEN 'internal' THEN 2
WHEN 'sensitive' THEN 3
WHEN 'privileged' THEN 4
WHEN 'work_product' THEN 5
ELSE NULL
END
$$ LANGUAGE sql IMMUTABLE;
The RLS policies use this function to compare an actor's pii_ceiling against a document's evidentiary_pii_tier. A paralegal with pii_ceiling='internal' has rank 2; a sensitive document has rank 3. The RLS policy pii_tier_rank(pii_tier) <= pii_tier_rank(current_actor_pii_ceiling()) evaluates to 3 <= 2 which is false, so the row is filtered out. The paralegal does not see the medical record. The sources table carries default_pii_tier and default_access_role_floor. These set the starting tier for records ingested from a given source. An imessage source set to default_pii_tier='low' means iMessages default to low sensitivity. A ccap source (court records) set to default_pii_tier='public' means court records default to public. Workers that ingest from these sources inherit the default; records can be elevated (not lowered) post-ingest. The tier system is not just an access control mechanism; it controls what enters productions. A production worker that assembles a document set queries against evidentiary_pii_tier to determine which documents can be produced to which party. A document at work_product cannot be produced to opposing counsel absent a specific court order overriding the tier. ## Evidentiary roles and RBAC The sources table carries a default_evidentiary_role for records from
that source:
transmission_evidence — the communication happened
content_evidence — what the communication said
native_capture — a raw, unmodified capture
public_record — filed with a court or government body
expert_input — provided by a retained expert
demonstrative — created for courtroom presentation
party_admission_candidate — statements by a party that may be admissions
strategy_workproduct_candidate — attorney work product
These roles map to hearsay exceptions, authentication requirements, and
admissibility doctrine. An iMessage export from Isabel's phone is
content_evidence — it is what she and the caseworker said. The CCAP docket entry is public_record — self-authenticating under FRE 902(5). A voicemail recording is native_capture — the authentication requirement is a foundation establishing whose voice it is. The evidentiary role determines how the record is handled at trial preparation: which witnesses need to lay foundation, which exceptions to hearsay apply, what the production format should be. Recording this in the database, on the source row, means these determinations travel with the record through every downstream process — chunking, embedding, attestation, production. The system RBAC roles — owner, counsel, paralegal, expert, family, opposing_counsel, court_clerk, system — are defined in actors.role. The RLS policies in 99_rls.sql map these roles to table visibility. The owner sees everything in their matter. Counsel sees everything except sources flagged with a stricter access_role_floor. Paralegal sees most records but not privileged ones or work product. Expert sees only what an explicit ACL grant allows. Family is the most restricted non-expert role. Opposing counsel sees nothing in the primary tables — they have access only through a production portal that queries the productions schema directly. ## Withheld documents and the privilege log Federal discovery rules require that a party who withholds a document on privilege grounds must log the withholding. FRCP 26(b)(5)(A) requires that the party "describe the nature of the documents, communications, or tangible things not produced or disclosed — and do so in a manner that, without revealing information itself privileged or protected, will enable other parties to assess the claim." The withheld_documents table materializes this log for each production:
CREATE TABLE withheld_documents (
production_id uuid NOT NULL REFERENCES productions(id),
document_id uuid NOT NULL REFERENCES documents(id),
privilege_assertion_id uuid NOT NULL REFERENCES privilege_assertions(id),
description_for_log text NOT NULL,
date_of_document date,
authors text[],
recipients text[],
subject text,
UNIQUE (production_id, document_id)
);
The description_for_log is the entry in the privilege log that opposing counsel sees — a description of the document sufficient to assess the privilege claim, without revealing the privileged content. The privilege_assertion_id links to the specific assertion that justifies the withholding. The UNIQUE (production_id, document_id) constraint ensures a document appears in the log at most once per production; a document cannot be both listed and withheld simultaneously. For every production, the outgoing bundle has two parts: the documents produced, and the privilege log of documents withheld. Both are frozen at the production date. The privilege log is itself a produced document; it discloses the existence of privileged materials without disclosing their content. --- > ✻ Try This. > > Pick a source in your database that contains sensitive material. Insert a > privilege assertion for one of its associated documents with > privilege_type='attorney_client' and a basis citing the relevant rule > (e.g., "Wis. Stat. § 905.03"). Then create a production record for the > matter. Insert a withheld_documents row linking the production to the > document and the privilege assertion. Write the description_for_log entry > as if you were a paralegal preparing the privilege log: describe the > document's date, authors, recipients, and subject without revealing its > content. > > Now open a new database session with a role that is not owner or counsel. > Query SELECT * FROM privilege_assertions. How many rows do you see? Query > SELECT content FROM chunks WHERE document_id = '<the privileged doc id>'. > What does the query return? --- ## The running case Isabel's matter — 2024JC000099, Harrow County Circuit Court — involves evidence distributed across six personal-data systems: iMessage, Gmail, Apple Privacy Export (maps, health records), court dockets, Harrow County DHS records (produced under Wisconsin Open Records Law), and recorded phone calls. Each system maps to a different evidentiary role and PII tier: - iMessage: content_evidence, low by default, elevated to sensitive for messages that include references to medical appointments. - Gmail: content_evidence, internal by default. - Apple Health records: native_capture, sensitive — HIPAA-protected. - Court dockets: public_record, public. - DHS records: content_evidence, sensitive — Wis. ch. 48. - Phone recordings: native_capture, internal — one-party consent under Wis. Stat. § 968.31. The attorney's annotation notes, created after reviewing the DHS records, are strategy_workproduct_candidate, elevated to work_product tier at the moment they are linked to a privilege_assertion with privilege_type='work_product'. When opposing counsel serves a discovery request for all communications between Isabel and the DHS, the production worker builds the production by querying for documents linked to the DHS source, at tier sensitive or below. The work-product annotations are not in the query result — the RLS policy filters them out before the worker sees them. The production is assembled from what the database returns, not from a checklist that the attorney must manually review. The withheld documents log lists the attorney's notes: one entry per note, with description_for_log identifying the document type (attorney work product notes, dated, re: DHS records review) without revealing the strategic content. This log accompanies the production and satisfies the FRCP 26(b)(5)(A) disclosure requirement. The legal hold for the matter — imposed on the date litigation was reasonably anticipated, which the attorney has documented as the date the DHS petition was filed — covers all sources associated with matter_id. No source row for this matter can be deleted while the hold is active. Isabel's phone is upgraded; her old iMessage backup is archived. The archive process issues a DELETE on the old backup source. The trigger fires, finds the active hold, and raises an exception. The backup is not deleted. The hold is working. --- > § For the Record — Wis. Stat. § 905.03(2) (attorney-client privilege). > > "A client has a privilege to refuse to disclose and to prevent any other > person from disclosing confidential communications made for the purpose of > facilitating the rendition of professional legal services to the client, > as follows: (a) Between the client or the client's representative and > the client's lawyer or the client's lawyer's representative; (b) Between > the client's lawyer and the client's lawyer's representative; (c) By the > client or the client's representative or the client's lawyer or a > representative of either to a lawyer representing another in a matter of > common interest; (d) Between representatives of the client or between the > client and a representative of the client; or (e) Among lawyers and their > representatives representing the same client." > > The schema's privilege_type='attorney_client' and > privilege_type='common_interest' map directly to subparagraphs (a) and > (c) of this statute. The shared_with_counsel_id and shared_with_expert_id > fields in privilege_assertions track the specific relationships that > trigger each protection. --- ## DSSEEnvelope and emit_dsse() (v0.2.0) In v0.1.1, the only emission path was emit(), which produces a Seal: an Ed25519 signature over the JCS-canonicalized attestation object, stored as fields on the seals table row. In v0.2.0, a second emission path is available: emit_dsse(), which produces a DSSEEnvelope. DSSE (Dead Simple Signing Envelope) is an open standard for signing arbitrary payloads. It wraps the attestation in a PAE (Pre-Authentication Encoding) structure:
PAE("application/vnd.meridian+json", <attestation-json-bytes>)
The Ed25519 signature is computed over the PAE bytes, not directly over the JSON. This allows any DSSE-compatible tool — independent of Meridian-Cannon — to verify the signature given only the envelope and the signer's public key.
Both emission paths compute the same chain_hash: SHA-256(JCS(attestation)). This is intentional. A verifier that wants to confirm field integrity without implementing PAE can recompute the chain hash from the JCS-canonicalized object and compare it to the value in the envelope's payload. The PAE verification step is then the only additional operation required to confirm the signature. Three-step verification for a DSSEEnvelope: 1. Decode the envelope. Extract the payload bytes and the signature. Decode the payload as JSON. 2. Chain hash check. Compute SHA-256(JCS(payload)) and compare to payload.seal.chain_hash. A mismatch means the payload was altered after the hash was computed. 3. PAE verify. Reconstruct PAE("application/vnd.meridian+json", payload_bytes) and verify the Ed25519 signature against the signer's public key (fetched from MERIDIAN_PUBLIC_KEY_URL or the seals table). Steps 2 and 3 are independent: step 2 detects field-level tampering without a public key; step 3 confirms the issuer's identity. A recipient without network access can perform step 2 offline against a locally cached chain hash, and perform step 3 later when the public key becomes available.
emit() produces a legacy Seal — an Ed25519 signature over the JCS-canonicalized attestation stored as fields in the seals table; emit_dsse() produces a DSSEEnvelope wrapping the same payload in a PAE structure independently verifiable by any DSSE-compatible tool. - Both emission paths compute chain_hash identically as SHA-256(JCS(attestation with seal excluded)), so a verifier can confirm field integrity with a hash recompute before touching the signature. - The three-step DSSE verification sequence is: (1) decode envelope and extract payload bytes, (2) recompute chain_hash from payload to detect field-level tampering, (3) verify Ed25519 over PAE(payload_type, payload_bytes) to confirm issuer identity. - A "procedural primitive" is an atomic, composable unit — like emit() or emit_dsse() — that can be tested in isolation, produces a deterministic output from a deterministic input, and does not depend on global mutable state. - The custodian field must be a stable institutional identifier (e.g., "acme-corp-2026") rather than a personal name so that key-rotation continuity and audit trail attribution survive personnel changes. emit() function continues to produce Seal-only output for backwards compatibility. emit_dsse() is the recommended path for any output that will be distributed outside the system boundary (to opposing counsel, to a court, to a third-party verifier). Both functions write to the seals table; emit_dsse() additionally writes the envelope bytes to seals.dsse_envelope. ## Exercises ### Warm-up 1. Open schema/85_legal.sql. The privilege_assertions table includes privilege_type='minor_child_welfare'. Under what Wisconsin statute does this privilege arise? Who holds it — the child, the parent, the state? 2. Open schema/99_rls.sql and locate the policy for withheld_documents. Which roles can see withheld_documents rows? Which cannot? Is this the right policy for a system where a paralegal assembles the production bundle? ### Core 3. Insert a legal hold with imposed_at set to the date Isabel's TPR petition was filed. Then attempt to delete a source row associated with the matter. If your database does not have a DELETE trigger enforcing the hold, write one. Verify that after the trigger is in place, the DELETE raises an exception and the row remains. 4. A consulting expert (not testifying) is retained by counsel. Under FRE 26(b)(4)(D), the expert's communications and work product are protected from discovery absent exceptional circumstances. The privilege_assertions table includes privilege_type='expert_consulting'. Insert a privilege assertion of this type for a set of expert-generated notes. Verify that a session with role='paralegal' cannot see the notes. 5. Walk through the full production workflow for one document: (a) assert attorney-client privilege; (b) create a production; (c) insert the document into withheld_documents with an appropriate description_for_log; (d) verify that the productions table row is frozen and the withheld_documents row has the correct constraint (cannot appear in both produced and withheld for the same production). ### Stretch 6. The pii_tier_rank() function returns NULL for unrecognized tier strings. What does NULL <= 3 evaluate to in SQL? (It is not false.) What does this mean for the RLS policies that use pii_tier_rank()? Is there a document that would pass the RLS check it should fail? Write a test case that demonstrates the behavior, then patch the function if needed. 7. Implement the database trigger that enforces legal holds on DELETE of sources rows. The trigger should: (a) check for active holds (released_at IS NULL) on the matter_id associated with the source; (b) if an active hold exists, raise an exception with a message that includes the hold ID and hold name; (c) if no active hold exists, allow the DELETE. Write a test that verifies the trigger fires on a held source and does not fire on a released hold. ## Build-your-own prompt For your capstone matter: audit your current privilege assertions. Run SELECT resource_type, resource_id, privilege_type, waived, asserted_at FROM privilege_assertions WHERE matter_id = '<your matter>' ORDER BY asserted_at. For each non-waived assertion: is the corresponding record excluded from your most recent production? Is it listed in withheld_documents for that production? If it is neither produced nor listed as withheld, you have a privilege-log gap — a document that should appear in the log but does not. That gap is a discovery compliance defect. ## Further reading - schema/85_legal.sql — privilege_assertions, redactions, withheld_documents, legal_holds. - schema/99_rls.sql — RLS policies enforcing privilege and PII tiers. - schema/10_core.sql — actors, pii_ceiling, pii_tier_rank(). - schema/20_provenance.sql — productions, production_recipients.
- Zubulake v. UBS Warburg, 220 F.R.D. 212 (S.D.N.Y. 2003) (Zubulake IV); 229 F.R.D. 422 (S.D.N.Y. 2004) (Zubulake V).
- FRCP 26(b)(5)(A) — privilege log requirements.
- FRE 502 — attorney-client privilege and work product; limitations on waiver.
- Wis. Stat. § 905.03 — Wisconsin attorney-client privilege statute.
- Wis. Stat. § 804.12(6) — Wisconsin sanctions for failure to preserve.
- The Sedona Conference, Commentary on Legal Holds, 2nd ed. (2022). Available at thesedonaconference.org. The standard reference for hold implementation requirements.
- Scheindlin, Shira A. and Rabkin, Jeffrey, "Electronic Discovery in Federal Civil Litigation: Is Rule 34 Up to the Task?" (2002) — the law review article that preceded and informed the Zubulake opinions.
Next: Chapter 17 — The Time-Aware Relationship Graph.