Dossier · Internal docs
Internal · Domain Model

Dossier — Domain Model

The Schema is the spine — DataSources write to it (inbound), Bindings read from it (outbound). Cases own the data, Filings are snapshots submitted to court.

DataSource → [mapping] → Schema key → [Binding] → Form field
                              ↓
                         Entry on Case → Filing (snapshot)

Domain Concepts

Engine Layer (document assembly)

# Name What it is
1 Schema Named collection of data point definitions. Has ui config for shaping the client app per law type.
2 Form Recursive. Leaf (PDF + fields), composite (children), or both.
3 Field A fillable spot on a PDF — text box, checkbox, dropdown.
4 Binding Routes schema keys → form fields or $child targets.
5 Validation Expression-based correctness rule.
6 DataSource Reusable recipe for getting external data into the schema.

Runtime Layer (case management)

# Name What it is
7 Case A client matter. Owns data (entries), contacts, tasks, notes, events, billing, history, attachments. Has references (Case #, Docket #) and key dates.
8 Filing An envelope submitted (or to be submitted) to court. Carries forms[] + attachments[]. Status: draft → filed → accepted/rejected. Snapshot is replayed from entries up to snapshot_at.
9 Entry A batch of data point changes on a case. One event = one entry with N values. Append-only, source-tagged, optionally tied to an attachment.
10 Contact Person/org reusable across cases. Linked with a role (Debtor, Attorney, Trustee).
11 Task To-do with assignee and deadline. Shows in Calendar.
12 Note Freeform text on a case. Author + timestamp.
13 Event Calendar event — hearing, deadline, meeting. Case-level or firm-wide.
14 ActivityEntry Auto-generated audit event (data change, import, status change, filing). Stored in the activity table.
15 Attachment Uploaded supporting document (ID scan, pay stub, certificate).
16 BillingEntry Fee or payment on a case.

File Structure

domain_tools/                       — form processing scripts, prompts, and pipeline docs (in git)

domains/                            — NOT in git (IP)
├── bankruptcy/
│   ├── schemas/
│   │   ├── individual.json         — ~1,100 keys
│   │   └── nonindividual.json      — ~640 keys
│   ├── data-sources/
│   │   └── credit-report.json      — MISMO XML → schema key mapping
│   └── federal/
│       ├── forms/                  — 70 leaf forms (PDF + JSON per directory)
│       └── groups/                 — 16 composite forms (chapter packages, schedules, etc.)

Groups are composite forms stored separately from leaf forms because they have no PDF. A group like chapter-7-ind.json references leaf forms and other groups as children. In the database and type system, a group is just a Form with children and no filePath. There are 19 composite forms (chapter packages, petition variants, schedule groups, means-test packages, etc.).


Schema

The single source of truth for what data points exist in a domain. Every key has a type, label, hint, and group. Forms bind to schema keys. DataSources map to schema keys.

interface Schema {
  id: string
  name: string              // "bankruptcy.individual"
  description: string | null
  entries: SchemaEntry[]
}

interface SchemaEntry {
  key: string               // "debtor1.first_name", "creditors.secured[].name"
  type: 'text' | 'money' | 'date' | 'boolean' | 'enum' | 'number'
  label: string
  hint?: string
  group?: string            // UI grouping: "Debtor 1 — Identity"
  options?: string[]        // for enum type
}

One schema per domain variant. Bankruptcy has two:

Schema Keys Overlap
bankruptcy.individual ~1,100 12 shared (case., attorney.)
bankruptcy.nonindividual ~640 Same 12

The overlap is only administrative keys. All substantive data (debtors, income, property, creditors, entity, officers) is completely separate.


Form

A form can be a leaf (has PDF with fields), a composite (has children), or both. Recursive — composites contain composites.

interface Form {
  id: string
  tenantId: string | null
  schemaId: string | null       // which vocabulary
  number: string | null         // "101" for leaf forms
  name: string
  description: string | null
  version: number
  status: string
  filePath: string | null       // PDF path (leaf only)
  effectiveDate: string | null
  pages: number                 // 0 for composites
  fields: FormField[]           // empty for composites
  children: ChildEntry[]        // empty for leaves
  bindings: Binding[]
  validations: Validation[]
}

Leaf form example

Has a PDF, fields extracted from it, bindings mapping schema keys to PDF field names.

{
  "id": "F0001010-BA01-4000-8000-000000000000",
  "name": "Form 101 — Voluntary Petition",
  "schemaId": "S0000001-BA01-4000-8000-000000000001",
  "filePath": "b101/b101_2024-06-22.pdf",
  "pages": 9,
  "fields": [{ "key": "Debtor1.First name", "type": "text", "page": 1 }],
  "children": [],
  "bindings": [
    { "source": "debtor1.first_name", "targets": ["Debtor1.First name"] }
  ]
}

Composite form example (group)

Has children, no PDF. Bindings route schema keys to children.

{
  "id": "BB000106-BA01-4000-8000-5C4ED0000001",
  "name": "The Schedules (Individual)",
  "schemaId": "S0000001-BA01-4000-8000-000000000001",
  "filePath": null,
  "pages": 0,
  "fields": [],
  "children": [
    { "id": "F0010600-...", "key": "b106sum", "position": 1 },
    { "id": "F0001190-...", "key": "b119", "position": 3, "condition": "case.has_petition_preparer == true" }
  ],
  "bindings": [
    { "source": "debtor1.first_name", "targets": ["$b106sum.Debtor 1", "$b106ab.Debtor 1"] }
  ]
}

Leaf vs composite bindings

Source Targets Self-reference
Leaf schema key PDF field names on the same form $.field
Composite schema key or $child.field $child.field addresses not applicable

Field

A fillable spot on a PDF.

interface FormField {
  key: string                 // AcroForm field name
  type: 'text' | 'checkbox' | 'dropdown' | 'radio' | 'signature' | 'optionList'
  page: number
  rect: { x: number, y: number, w: number, h: number } | null
  options: string[] | null
  label: string | null
  hint: string | null
  needsReview: boolean        // true if field name is generic/auto-generated
  source: 'acro' | 'llm' | 'manual'
}

Binding

Maps a source expression to one or more target addresses.

interface Binding {
  source: string        // schema key or expression
  targets: string[]     // field names or $child addresses
  condition?: string    // boolean expression
  note?: string         // developer documentation
}

Array syntax

[] in a source/target means "each item in this array":

{
  "source": "creditors.secured[].name",
  "targets": ["Creditors Name"],
  "repeating": { "pattern": "Creditors Name_{n}", "startIndex": 2, "maxRows": 8 }
}

The repeating property (leaf forms only) tells the PDF filler how to map array items to numbered AcroForm field names.

Cross-form array sync

At the parent level, array bindings sync data between sibling forms:

{ "source": "$schedules.creditors.secured[].name", "targets": ["$plan.plan_secured_treatments.maintain_cure[].creditor"] }

One binding per target array. The user decides which creditors go in which treatment section — the binding keeps shared fields (names, amounts) in sync.


Validation

interface Validation {
  expression: string          // "case.district != ''"
  severity: 'error' | 'warning'
  description: string         // "District is required"
}

DataSource

Reusable recipe for getting external data into the schema. Platform-defined or tenant-created. Stored as JSON config files in domains/<domain>/data-sources/.

interface DataSource {
  id: string
  tenantId: string | null     // null = platform-provided
  name: string                // "Experian Credit Report"
  type: string                // "credit-report", "case-mgmt", "csv", "document"
  schemaId: string            // scoped to a schema
  config: unknown             // parse rules, field mappings, API config
}

DataSource mappings are inbound (external field → schema key). Bindings are outbound (schema key → form field). Both use the same key vocabulary — one key picker, one autocomplete, same [] array syntax.

Data flow

External data (XML, API, CSV, PDF)
    ↓
DataSource (parse + map rules)
    ↓
Schema keys (the shared vocabulary)
    ↓
Entry on Case (batch of key=value with source + timestamp + confirmed)
    ↓
Binding engine (resolves confirmed entries → form fields)
    ↓
Filled PDF

Case

A client matter. Owns all data and sub-resources.

interface Case {
  id: string
  tenantId: string
  schemaId: string            // which vocabulary drives this case
  formId: string              // the initial form package
  name: string                // "Garcia, Maria"
  status: string              // Intake, Prep, In Review, Ready, Filed, Active, Discharged, Closed
  references: Record<string, string>  // {"Case #": "26-12345", "Docket #": "...", "District": "NDIL"}
  dates: Record<string, string>       // {"Filed On": "2026-03-25", "341 Meeting": "2026-04-14"}
}

Filing

An envelope submitted (or to be submitted) to court. When status transitions to filed, filed_at and snapshot_at are stamped to NOW(); the merged values as of snapshot_at form the filing's frozen snapshot (replayed on read — there is no stored JSONB snapshot blob).

interface Filing {
  id: string
  caseId: string              // belongs to a case
  name: string                // "Ch.7 Petition Package"
  status: 'draft' | 'filed' | 'accepted' | 'rejected'
  courtReference: string | null   // ECF #
  filedAt: string | null
  snapshotAt: string | null   // timestamp cutoff for replay
  forms: FilingForm[]         // child form_versions, captured at envelope creation
  attachments: FilingAttachmentSummary[]
}

filings (envelope) ↔ filing_forms (which forms are in the envelope) ↔ filing_attachments (which uploaded attachments accompany it). Amendments and conversions create new filings. The original filing's snapshot is immutable — replay against snapshot_at always returns the same values.


Entry

A batch of data point changes on a case. One event (save, import, sync) = one entry with N values.

interface Entry {
  id: string
  caseId: string              // belongs to a case (not a filing)
  source: string              // "manual", "credit-report", "csv"
  dataSourceId: string | null
  timestamp: string
  confirmed: boolean          // false = pending review
  values: Record<string, unknown>
}

How entries work

  • Lawyer types 5 fields, clicks save → 1 entry with 5 values, source = "manual", auto-confirmed
  • Credit report is pulled → 1 entry with 80 values, source = "credit-report", pending confirmation
  • Lawyer corrects a creditor name → 1 entry with 1 value, overrides the credit report entry for that key

Current state = merge all entries by timestamp (latest wins per key). Only confirmed entries are used for PDF generation.

Case history = auto-logged events (data changes, imports, status changes, filings). Separate from entries — entries are data, history is audit.


Child Entry

interface ChildEntry {
  id: string              // UUID of the child form
  key: string             // alias used in expressions: "b106ab", "petition"
  position: number        // ordering
  condition?: string      // boolean expression controlling inclusion
}

No type field — a child is always a form (which may itself be leaf or composite).


Expression Syntax

All source, condition, and target values are parsed by the expression engine.

Reference resolution

Pattern Meaning Example
$child.field Child form's field $b106ab.undefined_155
$.field Current form's own field $.line55
group.key Schema key (dotted) debtor1.first_name
collection[].field Array item field creditors.secured[].name
bare word Literal or function true, 42, CONCAT
'...' String literal 'installments'

Disambiguation

  • Starts with $ → form field reference
  • Has a . but no $ → schema key
  • No . and no $ → literal value or function name

Operators

+, -, *, /, ==, !=, >, <, >=, <=, &&, ||, !

Functions (22)

Category Functions
String CONCAT, UPPER, LOWER, TRIM, LEFT, RIGHT, LEN, SUBSTITUTE
Math SUM, MAX, MIN, COUNT, ROUND, ABS
Logical IF, AND, OR, NOT, IN
Date TODAY, YEAR, MONTH, DAY

Scoping Rules

Context $child.field $.field schema key
Composite binding source yes no yes
Composite binding target yes no no
Child condition yes (siblings visible) no yes
Leaf binding / validation no yes yes

Ancestry rule: bindings can reference any descendant. Never up, never sideways — only down. Cross-form bindings live on the common ancestor.


IDs

JSON files on disk have no id field. The seed script assigns randomUUID() at load time. Groups reference children by key (the form's directory name or group filename), not by UUID. The seed builds a key → id lookup and wires up the references when inserting into the database.

Filenames: -ind / -nin always last suffix (e.g., petition-ch7-ind.json).


Database Tables

Engine

Table Purpose
schemas Data vocabularies (entries JSONB, ui JSONB, tags)
forms All forms — leaf and composite (fields, bindings, validations as JSONB)
form_children Parent → child composition (key, position, condition)
data_sources Import recipe configs (type, schema_id, config JSONB)

Runtime

Table Purpose
cases Client matters (schema_id, status, references JSONB, dates JSONB)
case_forms Working set of forms attached to a case (state: scratch / drafting / queued / filed)
filings Envelopes submitted to court (case_id, status, filed_at, snapshot_at, court_reference)
filing_forms Forms inside a filing envelope (filing_id, form_id, form_version)
filing_attachments Attachments inside a filing envelope (filing_id, attachment_id, required)
entries Batch data changes on a case (case_id, source, data_source_id, values JSONB, confirmed)

Case Management

Table Purpose
contacts People/orgs reusable across cases
case_contacts Case ↔ contact link with role
tasks To-dos (case_id, title, done, assignee_id, deadline)
notes Freeform text (case_id, text, author_id)
events Calendar events (tenant_id, case_id, title, date, type)
activity Auto audit trail (case_id, type, text, detail)
attachments Uploaded files (case_id, name, file_path, category)
billing_entries Fees + payments (case_id, amount, type, method, date)

Platform

Table Purpose
tenants Firms (name, address, phone, website)
users Accounts (tenant_id, email, role)
Source: docs/domain.md