Uma Claude Skill que pega um único contrato executado — .docx ou .pdf com camada de texto — e emite um registro JSON aterrado em citações com as cláusulas em que seu CLM realmente baseia chaves: lei aplicável, cap de responsabilidade, indenização, prazo, auto-renovação, gatilhos de rescisão, condições de pagamento, propriedade de IP, prazo de confidencialidade, mais quaisquer campos customizados que você configurar (data residency, MFN, mudança de controle, cessão). Todo valor extraído carrega um trecho verbatim, uma citação {page, char_span} e um score de confiança, então o revisor downstream consegue verificar em segundos em vez de reler o contrato.
Esta página cobre quando rodar, quando explicitamente não rodar, quanto custa e os modos de falha nomeados que você deve dimensionar antes de apontar para um repositório de produção.
Quando usar
Pegue a skill quando você tem uma necessidade de output estruturado contra contratos que já passaram pela barreira de privilégio:
Backfill de dados de CLM. Você herdou um repositório de arquivos planos (Box, SharePoint, drive de rede) e precisa popular campos de metadado no Ironclad ou Agiloft sem queimar um trimestre de um paralegal.
Construção de biblioteca de cláusulas. Você quer toda cláusula de “liability cap” em todo o portfolio para que a clause library reflita o que você realmente concordou, não a posição declarada do playbook.
Diligence. Você tem 48 horas para surfacear cláusulas de mudança de controle, cessão e most-favored-customer no conjunto de contratos de um alvo antes de um deal fechar.
Triagem de renovação. Você precisa flagar todo contrato com auto-renovação nos próximos 90 dias com o campo de notice-period-days populado.
O bundle do artifact vive em apps/web/public/artifacts/clause-extraction-claude-skill/ e entrega:
SKILL.md — a definição da Skill com método, formato de output e pontos de atenção
references/1-clause-taxonomy.md — as cláusulas a extrair por tipo de contrato, com cabeçalhos e sinônimos
references/2-output-schema.json — o JSON Schema contra o qual todo registro valida (pin numa versão)
references/3-citation-format.md — gramática de citação e as regras para fallbacks de “não presente” / “não foi possível extrair”
Quando NÃO usar
A skill é estreita de propósito. Recuse a invocação em qualquer um destes casos.
Drafts privilegiados em negociação ativa. A política de AI da maioria dos times jurídicos (e o template de AI policy que recomendamos) traça uma linha dura em drafts de negociação em andamento — particularmente redlines de outside counsel e attorney work product. Esta skill é para contratos executados ou quase finais que já passaram pela questão de privilégio. Se você não tem certeza se um documento passou, a resposta é não.
Qualquer coisa via vendors de AI não-Tier-A. Rode apenas contra o endpoint Tier-A aprovado pela sua firma (API direta da Anthropic, ou seu tenant enterprise do Claude). Nunca o chatbot de consumidor. Nunca um plugin de browser. Nunca um wrapper de SaaS não-vetado que promete “Claude por baixo dos panos”. Mandar um contrato por um vendor Tier-B é um vetor de leak de privilégio — recuse a invocação em vez de contornar a política de AI. A Skill em si hard-codifica uma allowlist de endpoints; se você está rodando dentro do Claude Code ou Claude.ai com seu tenant enterprise, está bem.
Drafting ou redlining. Esta skill só lê. Para redlining, use a skill separada de contract-redline.
Interpretação jurídica. O output é texto + citação. Se um cap de responsabilidade de 12 meses é “bom o bastante” dado o contexto do deal é um julgamento que fica com a advogada.
Setup
Solte o bundle em ~/.claude/skills/ (Claude Code) ou faça upload do diretório references/ e do SKILL.md para um projeto do Claude.ai.
Substitua os conteúdos de references/1-clause-taxonomy.md com a taxonomia real da sua firma. A taxonomia default tem as cláusulas comuns de MSA; a maioria das firmas adiciona 5-10 campos customizados (data residency por jurisdição, carveouts de mudança de controle, prazo de non-solicit, escopo de MFN).
Pin references/2-output-schema.json numa versão. Bump extractor_version no schema e na Skill em toda mudança de taxonomia para que consumidores downstream detectem drift.
Rode num contrato conhecido — escolha um cujos valores de cláusulas você já tem no CLM. Compare o JSON extraído contra o registro do CLM. Itere nos sinônimos da taxonomia até bater.
Rode em escala. A Skill é por contrato; orquestre o batch no n8n, num loop de shell ou no hook de intake do seu CLM.
O que a skill realmente faz
Quatro passos, em ordem.
Extração de texto com preservação de layout..docx é parseado via XML do docx; .pdf via pdfplumber para que números de página e char spans de bounding-box sobrevivam. Se o PDF não tem camada de texto (imagem escaneada), a Skill aborta com error: "ocr_required" em vez de emitir texto vazio. Rotear PDFs escaneados para OCR é uma preocupação separada upstream; esta Skill não faz OCR, porque produzir silenciosamente uma extração “limpa” e vazia de um scan é pior do que falhar barulhento.
Extração aterrada em citação, um pass por cláusula. Para cada cláusula na taxonomia: encontra parágrafos candidatos via match de heading + sinônimo, passa apenas esses candidatos (não o contrato inteiro) ao Claude com a definição da cláusula, e exige de volta o valor, um trecho verbatim de ≤ 280 chars, a citação {page, char_span} e um score de confiança high | medium | low. Qualquer trecho que não seja byte-idêntico a uma substring dos parágrafos de origem é rejeitado — esse é o guard de alucinação, e é inegociável. Prompts por cláusula (vs um mega-prompt) deixam você re-tentar apenas as falhas, capar os tokens de input de cada call e isolar alucinação a um único campo em vez do registro inteiro.
Validação de schema contra o output-schema.json pinado. Erros de validação caem no array errors do output. A Skill não coerce tipos silenciosamente.
Fallback “não presente”. Quando uma cláusula não é localizada, emite value: null, status: "not_present", note: "Searched headings: [...]". Não adivinhe. Pipelines de backfill de CLM tratam null + status:not_present como confirmado-ausente (arquive o contrato sem esse campo) e null + status:error como precisa-rerun (não arquive). Conflar os dois corrompe dado de CLM ao longo do tempo.
Realidade de custo
No pricing do Claude em 2026 — chame de ~$3/M tokens de input e ~$15/M tokens de output para o modelo cost-effective usado dentro da Skill — o custo é dominado por tokens de input, e tokens de input são dominados pelo comprimento dos parágrafos candidatos (porque a Skill nunca manda o contrato completo, só os parágrafos que casam por cláusula).
Números aproximados por contrato:
Contrato curto (5 páginas, ~3K tokens de input em todos as calls por cláusula, ~500 tokens de output): ~$0.02 por contrato.
MSA padrão (20 páginas, ~12K tokens de input, ~1K tokens de output): ~$0.05 por contrato.
MSA enterprise longo com schedules (60 páginas, ~35K tokens de input, ~2K tokens de output): ~$0.13 por contrato.
Num time típico in-house de mid-market rodando ~200 contratos novos e herdados por mês pelo pipeline, isso são $10–$30/mês em gasto de token. O custo é erro de arredondamento contra uma hora de paralegal. Onde para de ser erro de arredondamento é o projeto de diligence de 50.000 contratos — a $0.05 cada, isso são $2.500, que ainda é barato, mas vale a pena orçar à frente em vez de descobrir na fatura do cartão.
O custo não-token: toda extração com confidence: medium | low (e uma amostra de 10% dos high) precisa de revisão humana. Planeje ~30 segundos por registro em medium e ~2 minutos em low. A Skill é mais rápida que um paralegal, não grátis.
Métrica de sucesso
Duas métricas que vale instrumentar desde o dia um.
Acurácia de extração num conjunto rotulado. Construa um gold set de 50 contratos com extrações manuais. Meça precision e recall por cláusula. Meta: ≥ 95% precision nas cláusulas obrigatórias (governing_law, liability_cap, term_length_months, auto_renewal). Abaixo disso, os falsos positivos envenenam o CLM e os revisores aprendem a ignorar o campo. Recall importa menos — not_present é uma resposta de carga, e uma cláusula perdida vai para revisão humana de qualquer jeito.
Tempo por contrato, end to end. Incluindo o pass de revisão humana nos registros flagados. Meta para um MSA de 20 páginas: abaixo de 4 minutos de wall-clock, vs. 20-30 minutos de extração manual completa. Se você não está vendo 5×, a fila de revisão humana está agressiva demais — aperte os thresholds de confiança.
vs alternativas
vs extração nativa de cláusulas com AI do Ironclad. A extração nativa do Ironclad é excelente se todo contrato que você se importa vive no Ironclad. Ela tropeça quando você está fazendo backfill de fora do Ironclad (o caminho de importação é desajeitado) e quando você quer cláusulas customizadas além do conjunto templated do Ironclad. Esta Skill roda contra qualquer arquivo em disco e usa sua taxonomia. Se você mora inteiramente no Ironclad, use a extração nativa deles; se você está alimentando múltiplos destinos ou fazendo diligence num repositório não-Ironclad, esta Skill é o fit melhor.
vs Kira Systems. Kira é o incumbente enterprise-grade — alta acurácia, biblioteca profunda de templates, caro (seis dígitos), ciclo longo de venda, exige dado de treinamento por cláusula customizada. Se você é uma firma BigLaw fazendo diligence de M&A em escala, Kira paga o preço. Se você é um time legal-ops de 50 pessoas fazendo backfill de alguns milhares de MSAs herdados, Kira é exagero e esta Skill é duas ordens de magnitude mais barata para a acurácia que você precisa.
vs revisão manual por paralegal. A comparação honesta. Um paralegal extraindo 10 cláusulas de um MSA de 20 páginas leva 20-30 minutos e bate ≥ 99% de acurácia nas cláusulas fáceis (lei aplicável, prazo) e ~90% nas difíceis (estrutura de cap de responsabilidade, carveouts de indenização). Esta Skill faz em menos de um minuto a ~$0.05, bate ~95% nas fáceis e ~85% nas difíceis, e roteia o resto para um humano via flag de confiança. O movimento certo para a maioria dos times é híbrido: Skill em todo contrato, paralegal nos registros flagados.
Pontos de atenção
Leak de privilégio via vendor Tier-B. Rotear um documento privilegiado por um endpoint de AI não-aprovado pode renunciar privilégio. Guard: a Skill checa uma allowlist hard-codada de endpoints (api.anthropic.com mais seu tenant enterprise) no startup e se recusa a rodar se o endpoint configurado não está nela. Documente o owner da allowlist na sua AI policy.
Gaps de texto induzidos por OCR em PDFs escaneados. Um PDF de imagem escaneada sem camada de OCR extrai como páginas vazias; sem um guard, a Skill reportaria a maioria das cláusulas not_present e pareceria um run limpo. Guard: o passo 1 detecta páginas com < 50 caracteres extraídos e aborta com ocr_required em vez de emitir um registro enganoso. Roteie o contrato por OCR upstream e re-rode.
Cláusulas alucinadas. Modelos vão inventar prestativamente uma cláusula de “termination for convenience” que não existe se perguntados. Guard: o check de substring byte-idêntica de excerpt no passo 2 — qualquer trecho não literalmente presente nos parágrafos de origem é rejeitado e a cláusula registra status: "error", error: "excerpt_not_grounded". Não há caminho de alucinação high-confidence por construção.
Drift de schema entre versões de contrato. Uma atualização de taxonomia que muda liability_cap de string para um objeto {type, amount, period} quebra silenciosamente todo consumidor downstream. Guard: pin extractor_version em references/2-output-schema.json e bump em toda mudança de taxonomia ou schema. Consumidores downstream dão key na versão, não numa assunção de estabilidade.
Resolução de termos definidos. “As set forth in Schedule A” retorna a referência, não o valor. Guard: a Skill detecta as set forth in / as defined in e emite confidence: medium com note: "cross-reference, manual resolution required". Resolução automática naive é pior do que o flag honesto.
Não é aconselhamento jurídico. Extração é mecânica. Se um cap de 12 meses é aceitável para este deal é julgamento que fica com a advogada.
Stack
Claude — orquestração de extração de texto, extração de cláusulas aterrada em citação, validação de schema
---
name: clause-extraction
description: Extract a fixed set of contract clauses from a single .pdf or .docx and emit citation-grounded JSON with page/span references. Use after intake to backfill CLM metadata, build a clause library, or surface change-of-control / liability terms during diligence.
---
# Clause extraction
## When to invoke
Invoke this skill per contract, after the document has been ingested and you need a structured clause record (governing law, liability cap, term, auto-renewal, indemnification, payment terms, IP ownership, confidentiality term, termination triggers, plus any custom clauses you configure).
Typical callers:
- CLM backfill — populating Ironclad / Agiloft / DealHub metadata for a legacy contract repository
- Diligence — surfacing change-of-control, assignment, MFN clauses on a target company's contract set before deal close
- Clause library — building a corpus of "what we actually agreed to" across a portfolio so the playbook reflects reality
Do NOT invoke this skill for:
- **Privileged drafts in active negotiation** — per AI policy in most legal teams, in-flight negotiation drafts (especially with outside counsel redlines) do not get sent to AI tooling. This skill is for executed or near-final contracts that have already cleared privilege.
- **Anything via non-Tier-A AI vendors.** Run only against the firm-approved Tier-A model endpoint (Anthropic API or your enterprise Claude tenant). A general-purpose chatbot, browser plugin, or unvetted SaaS wrapper is a privilege-leak vector — refuse the invocation rather than route around the AI policy.
- Drafting or redlining clauses (this skill reads only)
- Interpreting legal effect (the output is text + citation; legal judgment stays with counsel)
## Inputs
- Required: `contract_path` — absolute path to a `.pdf` or `.docx`. PDFs must be text-based or pre-OCR'd; scanned-image PDFs without an OCR layer are rejected at step 1.
- Required: `taxonomy` — path to `references/clause-taxonomy.md` (or a custom taxonomy keyed by contract type). Defines the clauses to look for and the expected value type (string, number, boolean, enum).
- Required: `output_schema` — path to `references/output-schema.json`. The JSON Schema the output must validate against. Schema drift across contract versions is the #1 source of downstream pipeline breakage; pinning the schema per run guards against it.
- Optional: `contract_type` — `msa | sow | nda | dpa | order_form`. Selects the clause subset from the taxonomy. Defaults to `msa`.
- Optional: `custom_clauses` — array of additional clause names to look for beyond the taxonomy defaults (e.g. `data_residency_clause`, `most_favored_customer_clause`).
## Reference files
Read these from `references/` before processing. They are templates — replace the placeholder content with your firm's real taxonomy and schema before running on production contracts.
- `references/clause-taxonomy.md` — clause definitions per contract type, with the value type, required/optional flag, and synonym phrases the extraction step matches against
- `references/output-schema.json` — the JSON Schema every emitted record must validate against
- `references/citation-format.md` — citation grammar (page + span anchor) and the rules for "not present" / "could not extract" fallbacks
## Method
Run these steps in order. Do not parallelize — later steps depend on the artifacts produced by earlier ones.
### 1. Text extraction with layout preservation
For `.docx`: parse via the docx XML and emit a flat text stream with paragraph indices and section headings preserved.
For `.pdf`: use a text-layer extractor (pdfplumber or pdfminer.six) that preserves page numbers and bounding-box character spans. If the PDF has no text layer (scanned image), abort with `error: "ocr_required"` rather than silently emitting empty text. Routing a scanned PDF to OCR is a separate upstream concern; this skill does not OCR.
The output of step 1 is a list of `{page, paragraph_index, char_span, text}` records. Every later citation references these coordinates.
### 2. Citation-grounded extraction (one pass per clause)
For each clause in the taxonomy:
1. Locate candidate paragraphs by heading match (e.g. "Governing Law", "Term") and synonym phrase match (e.g. "shall be governed by", "initial term of this Agreement").
2. Pass the candidate paragraphs (not the full contract) to Claude with the clause definition and ask for: the value, the verbatim source excerpt (≤ 280 chars), the `{page, char_span}` citation, and a `confidence` score (`high | medium | low`).
3. **Reject any extracted excerpt that is not byte-identical to a substring of the source paragraphs.** This is the hallucination guard — if the model returns text not actually in the contract, drop the extraction and record `value: null, error: "excerpt_not_grounded"`.
Why one pass per clause and not a single mega-prompt: per-clause prompts let you retry only the failures, cap each call's input tokens (cheaper, faster), and isolate hallucination failures to a single field instead of the whole record.
### 3. Schema validation
Validate the assembled record against `output-schema.json`. If validation fails, emit the validation error in the output's `errors` array. Do not silently coerce types.
### 4. "Not present" fallback
If a clause is not located in step 2 (no candidate paragraphs above confidence threshold), emit `value: null, status: "not_present", note: "Searched headings: [...]; no matching paragraphs found."` Do not guess. "Not present" is a load-bearing answer; CLM backfill pipelines treat `null + status:not_present` differently from `null + error:*`.
## Output format
Always emit a single JSON object per contract. Soft constraints below are enforced by `references/output-schema.json`.
```json
{
"contract_file": "vendor_msa_2026.pdf",
"contract_type": "msa",
"extracted_at": "2026-05-03T14:22:00Z",
"extractor_version": "clause-extraction@2026.05",
"clauses": {
"governing_law": {
"value": "Delaware",
"excerpt": "This Agreement shall be governed by and construed in accordance with the laws of the State of Delaware, without regard to its conflict of laws principles.",
"citation": { "page": 14, "char_span": [1820, 1980] },
"confidence": "high",
"status": "extracted"
},
"liability_cap": {
"value": "12 months fees",
"excerpt": "In no event shall either party's aggregate liability exceed the fees paid by Customer in the twelve (12) months preceding the event giving rise to the claim.",
"citation": { "page": 18, "char_span": [220, 410] },
"confidence": "high",
"status": "extracted"
},
"auto_renewal": {
"value": true,
"renewal_term_months": 12,
"notice_period_days": 90,
"excerpt": "This Agreement shall automatically renew for successive 12-month terms unless either party provides 90 days' written notice of non-renewal.",
"citation": { "page": 3, "char_span": [50, 230] },
"confidence": "high",
"status": "extracted"
},
"most_favored_customer_clause": {
"value": null,
"status": "not_present",
"note": "Searched headings: ['Most Favored', 'MFN', 'Pricing']; no matching paragraphs found."
}
},
"errors": []
}
```
## Watch-outs
- **Privilege leak via Tier-B vendor.** Routing a privileged or attorney-work-product document through a non-approved AI endpoint can waive privilege. Guard: hard-coded allowlist of model endpoints (`ALLOWED_ENDPOINTS = ["api.anthropic.com", "<your-enterprise-tenant>"]`) checked at skill startup. Refuse to run if the configured endpoint is not on the list. Document the allowlist owner in your AI policy.
- **OCR-induced text gaps on scanned PDFs.** If step 1 silently emits empty pages from a scanned image PDF, the skill will report many clauses as `not_present` and look like a clean extraction. Guard: step 1 detects pages with < 50 extracted characters and aborts with `ocr_required` rather than producing a misleading "clean" record.
- **Hallucinated clauses.** Models will helpfully invent a "termination for convenience" clause that doesn't exist if asked. Guard: byte-identical excerpt-substring check in step 2 — any excerpt not literally present in the source paragraphs is rejected. Pair with `confidence: low` flagging for human review on the rest.
- **Schema drift across contract versions.** A taxonomy update that changes `liability_cap` from a string to a structured `{type, amount, period}` silently breaks every downstream consumer. Guard: pin `extractor_version` in the output and bump it on every taxonomy or schema change. Downstream consumers key on version, not on the assumption that the schema is stable.
- **Defined-term resolution.** When a clause says "as set forth in Schedule A" the excerpt is the reference, not the value. Guard: detect the substring "as set forth in" / "as defined in" and emit `confidence: medium, note: "cross-reference, manual resolution required"` rather than treating the reference as the answer.
- **Heading-light contracts.** Contracts without clear section headings (older or short-form) extract less reliably. Guard: when fewer than 60% of expected headings match in step 2, mark the whole record `confidence: medium` and note `"heading_density: low"` so downstream QA routes it to human review.
# Clause taxonomy — TEMPLATE
> Replace this template's contents with your firm's actual clause taxonomy
> per contract type. The clause-extraction skill reads this file on every
> run; without your real taxonomy, extractions will use the generic defaults
> below and miss the clauses your CLM cares about.
The skill keys on `clause_id`. Every clause record in the output JSON uses the `clause_id` as the property name. Adding a clause means: add an entry here AND add the matching property to `output-schema.json`.
## Convention
For each clause:
- `clause_id` — snake_case identifier used as the JSON key
- `value_type` — `string | number | boolean | enum | structured`
- `required` — `true | false` (drives `not_present` vs hard error in validation)
- `headings` — list of section heading strings the locator matches against
- `synonyms` — list of phrase substrings the locator falls back to when no heading matches
- `value_hint` — what the extractor should pull (e.g. "the named jurisdiction state or country", "12-month-fees / 24-month-fees / unlimited / other")
## MSA defaults
### governing_law
- value_type: `string`
- required: true
- headings: `["Governing Law", "Choice of Law", "Applicable Law"]`
- synonyms: `["shall be governed by", "construed in accordance with the laws of"]`
- value_hint: the named jurisdiction (state, province, or country)
### liability_cap
- value_type: `structured` — `{ type: "fees_period" | "fixed_amount" | "unlimited" | "other", amount?: number, period_months?: number, currency?: string }`
- required: true
- headings: `["Limitation of Liability", "Liability Cap", "Cap on Liability"]`
- synonyms: `["aggregate liability shall not exceed", "in no event shall either party's liability exceed"]`
- value_hint: extract the cap amount or formula. Distinguish indirect-damages exclusions (do NOT extract those here) from the cap itself.
### indemnification
- value_type: `structured` — `{ ip_indemnity: boolean, mutual: boolean, carveouts: string[] }`
- required: true
- headings: `["Indemnification", "Indemnity"]`
- synonyms: `["shall defend, indemnify and hold harmless"]`
- value_hint: pull the IP indemnity boolean and the carveouts list (e.g. combinations, modifications, open source).
### term_length_months
- value_type: `number`
- required: true
- headings: `["Term", "Term and Termination"]`
- synonyms: `["initial term of this Agreement", "shall commence on the Effective Date"]`
- value_hint: convert years to months (3-year term → 36).
### auto_renewal
- value_type: `structured` — `{ enabled: boolean, renewal_term_months?: number, notice_period_days?: number }`
- required: true
- headings: `["Renewal", "Term and Termination"]`
- synonyms: `["shall automatically renew", "evergreen", "successive renewal terms"]`
### termination_triggers
- value_type: `structured` — `{ for_convenience: { allowed: boolean, notice_days?: number }, for_cause: { material_breach_cure_days?: number }, for_insolvency: boolean }`
- required: true
- headings: `["Termination"]`
- synonyms: `["may terminate this Agreement", "for material breach"]`
### payment_terms
- value_type: `structured` — `{ net_days: number, currency: string, late_fee_apr?: number }`
- required: true
- headings: `["Payment", "Fees and Payment", "Invoicing"]`
- synonyms: `["payable within", "net thirty (30) days"]`
### ip_ownership
- value_type: `enum` — `vendor | customer | joint | work_for_hire | other`
- required: true
- headings: `["Intellectual Property", "Ownership", "IP Rights"]`
- synonyms: `["all right, title and interest"]`
### confidentiality_term_months
- value_type: `number`
- required: true
- headings: `["Confidentiality", "Non-Disclosure"]`
- synonyms: `["confidentiality obligations shall survive", "for a period of"]`
- value_hint: convert years to months. If trade-secret carveout is "in perpetuity", emit `-1` and set `confidence: medium`.
## NDA defaults
(Replace with your NDA-specific taxonomy. Typical: `term_months`, `survival_period_months`, `permitted_purposes`, `residual_rights`, `return_or_destroy`.)
## DPA defaults
(Replace with your DPA-specific taxonomy. Typical: `data_residency`, `subprocessor_consent`, `audit_rights`, `breach_notification_hours`, `sccs_module_used`.)
## Custom clauses (firm-specific)
Add your firm-specific clauses here. Examples to consider:
- `change_of_control_clause` — boolean + carveouts
- `most_favored_customer_clause` — boolean + scope
- `data_residency_clause` — enum of jurisdictions
- `assignment_restriction` — enum: `no_restriction | consent_required | prohibited`
- `non_solicit_term_months` — number
## Last edited
{YYYY-MM-DD} — bump on every taxonomy change. The extractor records this date in `extractor_version` so downstream consumers can detect schema drift.
# Citation format — TEMPLATE
> The clause-extraction skill emits a citation on every extracted clause so
> the downstream reviewer can verify the extraction in seconds rather than
> re-reading the contract. Without a usable citation grammar, extractions
> are unfalsifiable — and unfalsifiable extractions become silent CLM data
> rot. This file pins the format and the fallback rules.
## Citation grammar
A citation is a structured object, not a string. The skill emits:
```json
{
"page": 14,
"char_span": [1820, 1980]
}
```
- `page` — 1-indexed page number in the source PDF, or paragraph cluster index for `.docx` (since `.docx` has no fixed pagination).
- `char_span` — `[start, end]` character offsets within the page's extracted text, where `start` is inclusive and `end` is exclusive.
The `excerpt` field on the clause record is the verbatim substring at that span. The skill enforces that `page_text[char_span[0]:char_span[1]] == excerpt`. If the assertion fails, the extraction is rejected and the clause is recorded with `status: "error", error: "excerpt_not_grounded"`.
## Why structured, not "p. 14, ¶ 3"
Free-text citations like "p. 14, paragraph 3" cannot be machine-verified. A reviewer cannot click them. A pipeline cannot diff them across re-runs. A regression test cannot assert "the citation moved by exactly N characters when we re-ran extraction after taxonomy v2." Structured citations make every extraction reproducible and reviewable.
## Reviewer UX expectations
Downstream tooling (CLM, review queue, audit log) is expected to render the citation as a deep link into the source PDF page with the excerpt highlighted. Without that affordance, reviewers fall back to ctrl-F on the excerpt — which works, but doubles review time.
Recommended renderer behavior:
- Display the excerpt with the citation page badge inline
- On click, open the source PDF at the cited page with the excerpt highlighted (PDF.js supports `#highlight=<text>`)
- Show `confidence` as a colored chip: high = green, medium = amber, low = red
## "Not present" — the load-bearing answer
When a clause is not located, the citation is omitted and `status: "not_present"` is set with a `note` field documenting the search:
```json
{
"value": null,
"status": "not_present",
"note": "Searched headings: ['Most Favored', 'MFN', 'Pricing']; searched synonyms: ['most favored', 'no less favorable']; no matching paragraphs found."
}
```
This is intentionally explicit. CLM backfill pipelines treat a `null` with `status: "not_present"` as confirmed-absent (file the contract without that field) and a `null` with `status: "error"` as needs-rerun (do not file). Conflating the two corrupts CLM data over time.
## Cross-reference handling
When the matched paragraph is a pointer ("as set forth in Schedule A"), the skill emits:
```json
{
"value": "see Schedule A",
"excerpt": "Liability shall be limited as set forth in Schedule A.",
"citation": { "page": 18, "char_span": [220, 274] },
"confidence": "medium",
"note": "cross-reference; manual resolution required"
}
```
Resolving cross-references is out of scope. The skill could chase the reference into Schedule A, but the failure modes (mis-numbered schedules, amendments overriding the schedule, partially-resolved chains) make naive resolution worse than an honest "needs human" flag.
## Confidence calibration
| Confidence | Meaning | Reviewer action |
|---|---|---|
| `high` | Heading match + synonym match + clean excerpt grounded in source | Spot-check 10% sample; trust the rest |
| `medium` | Synonym match without heading, OR cross-reference, OR low heading density on the contract overall | Review every record |
| `low` | Multiple candidate paragraphs and the model picked one with weak signal, OR excerpt is ≥ 200 chars | Review every record before filing |
The skill MUST NOT emit `high` for a record that did not pass the byte-identical excerpt check. There is no "high-confidence hallucination" case — by construction.
## Last edited
{YYYY-MM-DD}