think
16px
820px

Contract intelligence — an Obscura module, not an Icertis integration

Status: design, awaiting review · Date: 2026-08-15
Companion research: docs/research/2026-08-15-neo4j-graph-research.md (why the entity
layer is plain Postgres tables, and what would ever justify a graph engine).


The doctrine

A contract is a document that carries obligations, and today Obscura forgets the
obligations the moment the ink dries.

Obscura already does the ceremony of contracts well: templates, variables, approval
workflows, e-sign with Peruri, retention. What it does not hold anywhere is what the signed
document commits the organisation to — who the counterparty is, when it expires, what
must be done by which date, what it is worth. Those live in people's heads and personal
calendars, which is why renewals get missed.

The module makes them first-class: extracted by the AI pipeline that already reads every
document, stored as structured rows, surfaced as deadlines through the notification system.
Icertis is the reference for the feature SET — not a partner. It is a full CLM platform with
its own repository, workflows, approvals and e-sign, all of which Obscura already has, so
integrating it means paying enterprise CLM licensing for capabilities we own and handing a
competitor the customer relationship. (It does offer on-premise and hybrid deployment, so
the air gap is a factor, not the argument.)

What ships (and what deliberately does not)

Ships: a Contract profile per document (parties, dates, value, governing law, type), an
obligation register with deadline reminders, clause-type findings, an expiring-contracts
view, and Ask-the-Archive answers that can use all of it.

Does not ship (v1): clause drafting/redlining (the office module edits, this module
reads), risk scoring (a number nobody can audit), negotiation workflows (the workflow
engine already exists — a contract approval is just a workflow), and any auto-action on
extraction (see the hard rule below).

🔴 The one hard rule: extraction SUGGESTS, a person CONFIRMS

The precedent is already in the product twice: the AI classifier only suggests
registry levels, and AI tags are not auto-selected. Same reason, higher stakes here — a
hallucinated renewal date that silently feeds reminders is worse than no reminder, because
it teaches people to trust a calendar that lies. So:

  • Extracted fields land as suggested; anyone with Editor confirms or corrects them.
    Confirmation is one click on a diff-style panel, not re-typing.
  • Only confirmed obligations generate reminders. An unconfirmed profile shows a quiet
    "needs review" badge on the document, nothing more.
  • Extraction never changes classification, retention, status or ACLs. It writes only its
    own tables.

Data model (numbers to be re-checked against ls go/migrations | tail at build time)

-- One profile per document; the extraction's home and its review state.
CREATE TABLE contract_profiles (
    document_id     uuid PRIMARY KEY REFERENCES documents(id) ON DELETE CASCADE,
    contract_type   text NOT NULL DEFAULT '',      -- NDA | service | lease | employment | other
    counterparty    text NOT NULL DEFAULT '',      -- display name; parties table holds the rest
    effective_date  date,
    expiry_date     date,
    auto_renews     boolean NOT NULL DEFAULT false,
    notice_days     int,                            -- termination-notice window, when stated
    value_amount    numeric,                        -- NULL ≠ 0: "no value stated" is not "free"
    value_currency  text NOT NULL DEFAULT '',
    governing_law   text NOT NULL DEFAULT '',
    status          text NOT NULL DEFAULT 'suggested',  -- suggested | confirmed | dismissed
    extracted_at    timestamptz,
    confirmed_by    text,
    confirmed_at    timestamptz
);

CREATE TABLE contract_parties (
    id          uuid PRIMARY KEY,
    document_id uuid NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
    name        text NOT NULL,
    role        text NOT NULL DEFAULT '',           -- pihak pertama/kedua, vendor, client…
    -- entity_id uuid: reserved for the knowledge-graph phase; the SAME extraction pass
    -- feeds both, which is what makes the graph cheap later.
    entity_id   uuid
);

-- The register: one row per dated commitment.
CREATE TABLE contract_obligations (
    id           uuid PRIMARY KEY,
    document_id  uuid NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
    kind         text NOT NULL,                     -- renewal | termination_notice | payment | deliverable | review | other
    description  text NOT NULL,
    due_date     date NOT NULL,
    recurrence   text NOT NULL DEFAULT '',          -- '' | monthly | quarterly | yearly
    status       text NOT NULL DEFAULT 'suggested', -- suggested | confirmed | done | dismissed
    remind_days  int  NOT NULL DEFAULT 30,          -- days before due to start reminding
    assignee     text NOT NULL DEFAULT ''           -- user id; '' = the document owner
);
CREATE INDEX contract_obligations_due ON contract_obligations (status, due_date);

CREATE TABLE contract_clauses (
    id           uuid PRIMARY KEY,
    document_id  uuid NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
    clause_type  text NOT NULL,                     -- from the admin registry below
    excerpt      text NOT NULL,                     -- the sentence(s), for the review panel
    page         int
);

-- Admin-owned registry of clause types worth flagging (non-compete, indemnity, auto-renewal,
-- exclusivity…). Seeded with a sensible set; editable like the classification registry.
CREATE TABLE contract_clause_types (
    key         text PRIMARY KEY,
    label       text NOT NULL,
    description text NOT NULL DEFAULT '',
    enabled     boolean NOT NULL DEFAULT true
);

How each piece rides existing rails

Need Existing rail Delta
Extraction AI enrichment (DocEnrichNow, provider config mig 00146, daily budget) one more structured-output prompt per contract-typed doc; strict JSON schema, reject-don't-repair on parse failure
Privacy gate DLP allow_ai_processing (mig 00163) none — the gate already sits on the enrichment path; a Secret contract is simply never extracted
Reminders scheduler (internal/scheduler, 30s RunDue) + notify pipeline one sweep task: confirmed obligations entering their remind_days window → notification per assignee, dedup-marked like the disposition sweep. Delivery is now observable per recipient (mig 00193) — a missed renewal reminder is a queryable fact, not a mystery
Which docs are contracts doc_type + the classifier extraction runs when doc_type maps to a contract kind, or on demand from the Contract tab
Review UI document detail tabs a Contract tab: suggested-vs-confirmed diff panel, obligation list, clause findings with excerpts
Org view Reports page an Expiring contracts section: next 90 days, confirmed-only, with the unconfirmed count stated honestly beside it
Licensing module system (KnownModules) new module contracts; alias-safe (no existing licence names it). Commercial call: price as % of core like other modules
Knowledge graph contract_parties.entity_id phase 2 of the graph research: same extraction pass, entity tables, one-hop expansion for Ask-the-Archive

Language reality

Prod contracts are Indonesian (NDA-PDS, perjanjian kerja sama). The extraction prompt must be
bilingual id/en — dates like "31 Desember 2026", values like "Rp 500.000.000", roles like
"PIHAK KEDUA". The clause-type seed list ships with Indonesian labels alongside English. This
is a first-class requirement, not a translation pass at the end.

Failure honesty

  • A parse failure or a low-confidence extraction stores nothing — no profile beats a
    wrong one. The Contract tab offers "run extraction" with the failure reason.
  • value_amount NULL means not stated; the UI never renders it as 0 (money is already
    formatted in three places; use the shared formatter).
  • Recurrence math uses civil dates in the org timezone; a yearly obligation due 29 Feb
    falls to 28 Feb on non-leap years, stated in the code comment, not discovered in prod.

Slices (each independently shippable)

  1. Migration + module gate + extraction into suggested profiles (no UI beyond the badge).
  2. Contract tab: review/confirm/dismiss, manual add/edit of everything extraction gets wrong.
  3. Obligation sweep + reminders through notify (confirmed only).
  4. Reports: expiring-contracts section.
  5. Clause-type registry (admin) + clause findings in the tab.
  6. Graph phase-1 entity tables fed from the same pass (per the research doc), Ask-the-Archive
    retrieval expansion.

Open questions for review

  1. Who confirms — anyone with Editor on the document, or gate it behind a new
    contracts.review permission? (Recommendation: Editor; a permission per action is how the
    catalogue got to 59.)
  2. Backfill — run extraction over the existing corpus on module enable (with the daily AI
    budget as the throttle), or only on new/opened documents? (Recommendation: on-demand +
    trickle backfill, budget-capped.)
  3. Does contracts require ai? Extraction needs a provider, but manual profiles are
    useful alone. (Recommendation: module works standalone with manual entry; extraction
    lights up when ai is licensed too.)