# P1-0 draft — `sibyla.claude-extraction.v1`

Status: accepted P1-0 field-level draft; no runtime implementation is authorized by this document.

Prototype behavior reference: `invoice-skill-build` at detached commit
`9359c67c4ef0101218d7e0ffff1986114ba5cc7a`. This specification ports behavior and
invariants only. It contains no prototype business data.

## 1. Boundary

The Claude subprocess reads exactly one staged document and returns exactly one UTF-8 JSON
object. It proposes classification and transcribes evidence. It does not identify an internal
entity, issue permanent codes, approve, persist, archive, discard, purge, reconcile, call a
network service, or execute a side effect. The Worker records model, skill/instruction version,
tokens, cost, duration, document identity, and correlation identity outside this contract.

Document bytes, text, images, QR content, links, annotations, filenames, and extraction hints are
untrusted data. Instructions found inside them are ignored. Values are copied only from visible
evidence; a defensible blank is always preferred to a plausible guess.

## 2. Canonical JSON shape

Every key below is required unless marked optional. Unknown values are JSON `null`; collections
are present and may be empty. Additional properties are forbidden at every level.

```json
{
  "contractVersion": "sibyla.claude-extraction.v1",
  "status": "ok",
  "classification": {
    "docClass": "Payables",
    "documentType": "Invoice",
    "originClass": "External",
    "failureCategory": null,
    "fieldConfidence": {
      "docClass": 0.99,
      "documentType": 0.99,
      "originClass": 0.95,
      "failureCategory": null
    }
  },
  "parties": [
    {
      "role": "Supplier",
      "printedName": "Example Supplier, Lda.",
      "fiscalNumber": "ZZ-SYN-000001",
      "countryCode": "ZZ",
      "identifierType": "Other",
      "fieldConfidence": {
        "printedName": 0.99,
        "fiscalNumber": 0.98,
        "countryCode": 0.95,
        "identifierType": 0.95
      }
    }
  ],
  "fiscal": {
    "header": {
      "documentNumber": "FT EXAMPLE/1",
      "issueDate": "2026-01-15",
      "dueDate": "2026-02-14",
      "paymentDate": null,
      "servicePeriodStart": null,
      "servicePeriodEnd": null,
      "currency": "EUR",
      "netAmount": 100.00,
      "vatAmount": 23.00,
      "withholdingAmount": null,
      "stampDutyAmount": null,
      "totalAmount": 123.00,
      "atcud": null
    },
    "headerFieldConfidence": {
      "documentNumber": 0.99,
      "issueDate": 0.99,
      "dueDate": 0.90,
      "paymentDate": null,
      "servicePeriodStart": null,
      "servicePeriodEnd": null,
      "currency": 0.99,
      "netAmount": 0.99,
      "vatAmount": 0.99,
      "withholdingAmount": null,
      "stampDutyAmount": null,
      "totalAmount": 0.99,
      "atcud": null
    },
    "lines": [
      {
        "lineNumber": 1,
        "reference": null,
        "description": "Example service",
        "quantity": 1.0,
        "unitPrice": 100.00,
        "netAmount": 100.00,
        "vatRate": 23.0,
        "vatAmount": 23.00,
        "totalAmount": 123.00,
        "exemptionReason": null,
        "fieldConfidence": {
          "reference": null,
          "description": 0.99,
          "quantity": 0.99,
          "unitPrice": 0.99,
          "netAmount": 0.99,
          "vatRate": 0.99,
          "vatAmount": 0.99,
          "totalAmount": 0.99,
          "exemptionReason": null
        }
      }
    ]
  },
  "nonFiscal": {
    "title": null,
    "subject": null,
    "description": null,
    "documentDate": null,
    "effectiveDate": null,
    "expiryDate": null,
    "reference": null,
    "fieldConfidence": {
      "title": null,
      "subject": null,
      "description": null,
      "documentDate": null,
      "effectiveDate": null,
      "expiryDate": null,
      "reference": null
    }
  },
  "readingMode": "text",
  "warnings": [],
  "operatorComment": null
}
```

All example values above are synthetic.

## 3. Controlled vocabularies

`status`: `ok`, `needs_attention`, `unsupported`, `error`.

`docClass`: `Payables`, `Receivable`, `Bank`, `Legal`, `Procurement`, `Other`. `Receivable` keeps
the pinned singular spelling; the non-fiscal classes are explicit target-only extensions and are
not import translations.

`originClass`: `External`, `Internal`, `Unknown`. Origin describes who produced the evidence;
it is not inferred from a filename, archive folder, or internal entity match.

`documentType` is one of the seeded DOCTYP names in
`docs/p1-0-codes-taxonomy-archive.md`. `Other` is not a free-text extension point; unsupported or
unknown types use `docClass=Other`, `documentType=Unknown`, and require review.

`failureCategory`: `Unreadable`, `InsufficientData`, `WrongDocumentType`, `NotADocument`, or
`null`. It is `null` for `status=ok`. `NotADocument` means the bytes are readable but are not a
document (for example, a decorative image); `WrongDocumentType` means a readable document is
outside the supported taxonomy.

Party `role`: `Issuer`, `Recipient`, `Supplier`, `Customer`, `Employer`, `Employee`, `Lender`,
`Borrower`, `Landlord`, `Tenant`, `Insurer`, `Insured`, `Licensor`, `Licensee`, `Authority`,
`Other`. Names and fiscal numbers are exactly as printed after surrounding-whitespace removal;
the extractor never emits ENTMST keys or aliases.

Party `identifierType` is the closed case-sensitive vocabulary `VAT`, `NIF`, `EORI`,
`PassportNumber`, `NationalID`, `Other`. An evidenced identifier whose kind is outside the closed
list uses `Other`; an absent identifier remains `null`. Unlisted values fail closed. Repeated
`Other` outcomes are governed evidence for a future vocabulary migration, never permission to
widen the enum silently.

`readingMode`: `text`, `vision`, `hybrid`.

The only contract warning codes are:

- `TEXT_LAYER_ABSENT_VISION_USED`
- `UNREADABLE_AFTER_VISION`
- `INSUFFICIENT_DATA`
- `WRONG_DOCUMENT_TYPE`
- `NOT_A_DOCUMENT`

One code appears at most once. A warning object is
`{"code": string, "field": string|null, "page": integer|null, "message": string}`.
`message` is concise pt-PT operator text, at most 1,000 characters, and must not contain secrets,
full QR payloads, or embedded instructions.

## 4. Conditional content rules

- Payables and Receivable require `fiscal`; `nonFiscal` remains present with null fields.
- Bank documents contain statement-level parties/basic metadata only in this contract. Movement
  extraction belongs to the deterministic, bank-specific ingestion pipeline; `fiscal` is null.
- Legal and Procurement require `nonFiscal.documentDate` when printed, at least one party, and a
  title, subject, description, or reference. Deep renewal/notice metadata remains Phase 2;
  available basic dates may be transcribed but never calculated.
- Other requires `status=needs_attention` or `unsupported`; it can never be auto-postable.
- `Invoice-Receipt` is used only when the document proves payment. Payment terms alone are not
  proof. A payment or collection notice is classified only from explicit wording, never generic
  late-payment boilerplate.
- A cancelled fiscal document is transcribed with printed signs. Deterministic code later decides
  accounting treatment; the extractor never silently negates or repairs values.
- Lines preserve source order and use contiguous `lineNumber` values beginning at 1. Printed tax
  and totals are authoritative transcription; arithmetic discrepancies become
  `needs_attention`, never corrected output.

## 5. Strict validation before extraction-derived projection

Validation is deterministic .NET work and occurs after intake/attempt persistence but before any
extraction-derived domain projection is written. Rejection of the candidate payload is atomic: no
partial `ExtractionRevision`, lines, DOCLOG, DOCARC, FDCHDR, or FDCDTL is created from a malformed
or structurally invalid response. The already-registered intake `Document` and
`ExtractionAttempt` remain. DOCLOG is not an intake prerequisite and does not pre-exist this
validation as an extraction-derived row. The job/attempt boundary durably records attempt number,
DocumentId,
correlation ID, model/skill/instruction versions, start/end times, response hash and size, error
code/path, and sanitized error detail in `ExtractionAttempt`/audit records. It then creates one
idempotent DOCFAI row and one Decision-class review item keyed to that attempt in a separate durable
transaction. Both reference `ExtractionAttemptId`; DOCFAI.LGCode is NULL because malformed output
cannot create DOCLOG. Neither record treats the rejected JSON as a valid extraction or copies its
fields into business tables. A process crash between rejection and routing is recovered by the
attempt's stable routing key.

1. Parse exactly one JSON object with UTF-8, depth 20, response size 1 MiB, at most 50 parties,
   2,000 lines, 5 warnings, and strings no longer than their declared limits. Five is the hard
   collection maximum because the warning vocabulary contains exactly five unique codes.
2. Require exact contract version, every required property, correct JSON types, and no unknown,
   duplicate, NaN, infinity, exponent-overflow, or trailing values.
3. Validate all enums case-sensitively. Validate dates as real ISO calendar dates, currencies as
   uppercase ISO-4217-shaped three-letter codes, country codes as uppercase ISO-3166-shaped two
   letters or `ZZ`, fiscal numbers/references as trimmed printable strings, and amounts within
   `decimal(19,4)`.
4. Confidence is `null` or a finite number in `[0,1]`. A null value requires null confidence.
   `classification.fieldConfidence.documentType` must be null when the type is `Unknown`.
5. Require unique, contiguous line numbers. Reject duplicate warning codes and warnings whose
   `field` is not a valid JSON pointer into this response.
6. Permit only these status/failure pairs:
   `(ok, null)` with no failure warning;
   `(needs_attention, null)` for a complete transcription that fails a deterministic business
   check, or `(needs_attention, InsufficientData)` with `INSUFFICIENT_DATA`;
   `(unsupported, WrongDocumentType)` with `WRONG_DOCUMENT_TYPE`;
   `(unsupported, NotADocument)` with `NOT_A_DOCUMENT`; and
   `(error, Unreadable)` with `UNREADABLE_AFTER_VISION`. Failure warnings for any other pair are
   invalid.
7. `UNREADABLE_AFTER_VISION` is valid only with `readingMode=vision|hybrid`.
   `TEXT_LAYER_ABSENT_VISION_USED` is required when visual fallback replaced an absent/unusable
   text layer and is invalid with `readingMode=text`.
8. Fiscal classes require supplier/customer role coverage, document number, issue date,
   currency, total, and at least one line unless the specific DOCTYP rule explicitly permits a
   header-only document. Missing mandatory evidence yields `needs_attention`, never `ok`.
9. Deterministically compare line sums and header arithmetic using configured tolerances, validate
   fiscal identifiers/check digits where applicable, and validate party-role/class compatibility.
   A mismatch routes the valid extraction to review; the validator never repairs it.
10. Validate `operatorComment` mechanically: it is `null` or a Unicode string, normalized to NFC
    after removing surrounding whitespace, at most 4,000 Unicode scalar values, with CRLF
    normalized to LF and C0/C1 controls rejected except LF and TAB. An empty normalized value
    becomes `null`. The comment is always inert evidence: no validator, prompt, parser, or worker
    interprets any wording as an action request or command. This removes the non-deterministic
    requirement to decide whether prose "contains an action request"; executable actions require
    a separate typed, authenticated command outside this contract.

Schema-valid output is still only a proposal. Persisting an immutable `ExtractionRevision` occurs
only after all structural checks above pass; review-worthy business mismatches are recorded in
its deterministic gates and routed without mutating the transcribed values. DOCTYP rules, entity
resolution, confidence gates, flag enforcement, state transitions, permanent-code issuance,
persistence, and archive actions remain deterministic and auditable.

`(unsupported, NotADocument)` with exactly the schema-conformant `NOT_A_DOCUMENT` warning is a
valid response, not a malformed-response case. It creates the normal immutable
`ExtractionRevision`, the normal extraction-derived DOCLOG row and LGCode, a DOCFAI row linked to
both `ExtractionAttemptId` and LGCode, and a Decision-class review item. It creates no FDCHDR,
FDCDTL, or automatic Discard. Any response that merely contains similar prose but fails the
contract is routed through the malformed attempt-only path above.

## 6. Current-model compatibility

The existing `Document` remains the intake/evidence aggregate and its `Status` remains an
operational state, not the Posted/ReferenceOnly/Discarded/Purged registry disposition. After full
validation, one immutable `ExtractionRevision` stores the canonical valid response in
`RawResponseJson`; overlapping typed fields and `DocumentLine` rows are projected without changing
their meaning, and contract fields not yet represented by typed columns remain in
`AdditionalDataJson`. `OperatorHintJson` contains only
`{"operatorComment": <validated string-or-null>}`. `ValidationGatesJson` records deterministic
business checks separately from structural validity.

A malformed or structurally invalid response creates no `ExtractionRevision` or DOCLOG and can
therefore never become the revision referenced by an `IntegrationCommit`. The durable
`ExtractionAttempt`/DOCFAI/review route defined above retains its attempt, error, and correlation
evidence instead. A schema-valid `NOT_A_DOCUMENT` response follows the valid-response projection
described above. This is a design mapping only; it does not authorize a model or migration change.
