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) |
docs/domain.md