{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://longbook.app/schema/longbook.schema.json",
  "$comment": "longbook contract revision 45 — generated by scripts/gen-vault-contract.mts, never edited by hand.",
  "title": "longbook financial memory",
  "description": "A longbook vault: every financial commitment the owner asked longbook to remember. Self-describing — any tool can read and safely edit it using this schema and llms.txt.",
  "type": "object",
  "required": [
    "app",
    "version",
    "commitments"
  ],
  "properties": {
    "app": {
      "enum": [
        "longbook"
      ],
      "description": "Which app this file belongs to."
    },
    "version": {
      "type": "integer",
      "const": 3,
      "description": "The file format. Leave it alone — it is not a revision counter for the contents."
    },
    "exported_at": {
      "type": "string",
      "description": "ISO timestamp; present on exports."
    },
    "commitments": {
      "type": "array",
      "description": "The commitments themselves — subscriptions, insurance, EMIs, rent, fees, salaries, one-time spends. The only required list; everything else is optional.",
      "items": {
        "type": "object",
        "required": [
          "id",
          "name",
          "amount",
          "currency",
          "billing_cycle",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "UUID. Stable — preserve it when editing."
          },
          "name": {
            "type": "string",
            "description": "The short name the owner would say — \"Netflix\", \"Car EMI\", \"Term insurance\" — never the statement's own line (\"UPI/4521/NETFLIX.COM\") or a merchant code. A detail worth keeping goes in notes."
          },
          "amount": {
            "type": "number",
            "description": "The price now, in the currency below — what a newly planned charge is written at. Never converted or blended. What any one occurrence cost or will cost is on its row in charges[]; changing this reaches only unpaid planned charges from today on."
          },
          "currency": {
            "type": "string",
            "description": "ISO code, e.g. INR, USD."
          },
          "billing_cycle": {
            "enum": [
              "monthly",
              "yearly",
              "custom",
              "once"
            ],
            "description": "Recurring — monthly, yearly, or custom every N days — or once. Shown as Commitment Type. The rhythm a plan is written to; the dates themselves are rows in charges[]."
          },
          "custom_cycle_days": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Only meaningful when billing_cycle is \"custom\"."
          },
          "tag": {
            "type": [
              "string",
              "null"
            ],
            "description": "What kind of commitment this is — a name from tags[] (kind \"commitment\"), or a new one. One tag per row."
          },
          "details": {
            "type": "object",
            "description": "longbook's own: statement_line, the words a statement printed, written when longbook reads a statement itself. Read it; never write it. What is particular about a commitment — a policy number, a lender, a plan — goes in notes.",
            "additionalProperties": {
              "anyOf": [
                {
                  "type": [
                    "string",
                    "number",
                    "boolean",
                    "null"
                  ]
                },
                {
                  "type": "array",
                  "items": {
                    "type": [
                      "string",
                      "number"
                    ]
                  }
                }
              ]
            }
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "The commitment's three most important details, one per line, each \"Label: value\" — for insurance \"Policy number: …\", \"Policy name: …\", \"Document: documents/…\". The category decides which three; llms.txt lists them. Only what the source states. Never a card number, CVV, expiry, PIN, or credential — a save containing one is refused, not silently accepted."
          },
          "member_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "FK → members[].id — who this commitment is for."
          },
          "account_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "FK → accounts[].id — what it is billed to."
          },
          "status": {
            "enum": [
              "active",
              "paused",
              "cancelled"
            ]
          },
          "free_trial": {
            "photo": {
              "type": [
                "string",
                "null"
              ],
              "description": "The commitment's picture: a file name under the longbook folder's images/ folder, written by the app. Never a URL; it never leaves the folder. Read it, never write it."
            },
            "type": [
              "boolean",
              "null"
            ],
            "description": "True while the owner marks it a free trial. longbook shows a Free trial pill; nothing about the trial is a charge row. A statement that shows a 0.00 line still writes that line as a charge."
          },
          "ends_on": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO date the commitment stops. Nothing is planned past it; a charge on the day itself still lands."
          },
          "direction": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "in",
              "out",
              "transfer",
              null
            ],
            "description": "\"in\" for money that arrives — salary, a refund, interest, a dividend. \"transfer\" for a movement between the owner's own accounts they still want to see — a credit card bill due on the 5th, a standing move to savings. Absent or \"out\" is a charge. Money in and transfers are remembered beside what goes out: shown on the timeline, never added into any spending total, and a transfer never into money in."
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          }
        }
      }
    },
    "members": {
      "type": "array",
      "description": "Who a commitment is for — family, team, staff, anyone. Optional; a vault without this array is complete.",
      "items": {
        "type": "object",
        "required": [
          "id",
          "name",
          "name_key"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "name_key": {
            "type": "string",
            "description": "Normalised name (trimmed + lowercased) used for de-duplication."
          },
          "tag": {
            "type": [
              "string",
              "null"
            ],
            "description": "Their role in the owner's finances — landlord, household help, family — a name from tags[] (kind \"member\"), or a new one. Set when the owner says it, never guessed."
          },
          "details": {
            "type": "object",
            "description": "longbook's own, read as stored; never write it — a write sending non-empty details is refused. A member carries a name, a tag, an email and linked accounts, nothing else.",
            "additionalProperties": {
              "anyOf": [
                {
                  "type": [
                    "string",
                    "number",
                    "boolean",
                    "null"
                  ]
                },
                {
                  "type": "array",
                  "items": {
                    "type": [
                      "string",
                      "number"
                    ]
                  }
                }
              ]
            }
          },
          "is_incomplete": {
            "type": "boolean",
            "description": "True for a placeholder created from free-text entry."
          },
          "avatar_seed": {
            "type": "string",
            "description": "Cosmetic only."
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where a shared report reaches them; never required."
          },
          "photo": {
            "type": [
              "string",
              "null"
            ],
            "description": "Their picture: a file name under the longbook folder's images/ folder, written by the app. Never a URL; it never leaves the folder. Read it, never write it."
          },
          "starred": {
            "type": "boolean",
            "description": "True when the owner starred them: listed first on People, and in the Mac sidebar. Absent means not starred. Set only when the owner asks."
          },
          "created_at": {
            "type": "string"
          }
        }
      }
    },
    "accounts": {
      "type": "array",
      "description": "What a commitment is billed to — a card, bank account, wallet, or cash. Identification only, by design: this shape has no field for a full card or account number, CVV, expiry, or PIN, and never will. See the data-minimization rule in llms.txt. Optional; a vault without this array is complete.",
      "items": {
        "type": "object",
        "required": [
          "id",
          "name",
          "name_key"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "The display name, e.g. \"Visa 9292\" — free text, but never a full number."
          },
          "name_key": {
            "type": "string"
          },
          "nickname": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the owner calls it, if different from the display name."
          },
          "institution": {
            "type": [
              "string",
              "null"
            ],
            "description": "The bank or issuer, e.g. \"HDFC\", \"Axis\". Free text."
          },
          "last4": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9]{4}$",
            "description": "The last 4 digits only — the schema itself cannot hold more. Never the full number."
          },
          "tag": {
            "type": [
              "string",
              "null"
            ],
            "description": "What kind of account — a name from tags[] (kind \"account\"), or a new one."
          },
          "details": {
            "type": "object",
            "description": "longbook's own, read as stored; never write it — a write sending a non-empty card_name or details is refused. What is particular about an account — a card's printed name, a billing day — goes in notes, one line each.",
            "additionalProperties": {
              "anyOf": [
                {
                  "type": [
                    "string",
                    "number",
                    "boolean",
                    "null"
                  ]
                },
                {
                  "type": "array",
                  "items": {
                    "type": [
                      "string",
                      "number"
                    ]
                  }
                }
              ]
            }
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "What is particular about the account, one detail per line, each \"Label: value\" — on a card, \"Card name: Platinum Reserve\", the product printed on it, the thing to read when the owner asks what a card offers. Never a card number, CVV, expiry, PIN, or credential — a save containing one is refused."
          },
          "is_incomplete": {
            "type": "boolean"
          },
          "member_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional owner — FK → members[].id. Which member the account belongs to; null when it belongs to no one in particular (a joint or shared account). Distinct from a commitment's member_id, which is who a commitment is for."
          },
          "parent_account_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The linked account: FK → accounts[].id — the account this one is linked to, a card to the bank account that pays it. One level only; a linked account is never itself linked to another. The key keeps its older name; read it as \"linked account\"."
          },
          "balance": {
            "type": [
              "number",
              "null"
            ],
            "description": "What the account held on balance_date, as the owner or a statement last said — a card's outstanding amount, a bank account's balance. Informational: nothing is computed from it."
          },
          "balance_date": {
            "photo": {
              "type": [
                "string",
                "null"
              ],
              "description": "The account's picture: a file name under the longbook folder's images/ folder, written by the app. Never a URL; it never leaves the folder. Read it, never write it."
            },
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "The day the balance was true, YYYY-MM-DD — a statement's closing date, or the day the owner typed it."
          },
          "starred": {
            "type": "boolean",
            "description": "True when the owner starred it: listed first on Accounts, and in the Mac sidebar. Absent means not starred. Set only when the owner asks."
          },
          "created_at": {
            "type": "string"
          }
        }
      }
    },
    "tags": {
      "type": "array",
      "description": "The vault's own tag vocabulary: every tag in use plus any waiting to be. Seeded entries come from longbook's library (library.json beside this schema); the owner and their AI add more the same way. A tag is a name and an icon; no tag carries a template, and one a vault still holds is read as none.",
      "items": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "name",
          "name_key"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "UUID. Stable — preserve it when renaming."
          },
          "kind": {
            "enum": [
              "commitment",
              "member",
              "account"
            ],
            "description": "Which list this tag belongs to."
          },
          "name": {
            "type": "string",
            "description": "The tag as rows wear it — rows store this name, not the id."
          },
          "name_key": {
            "type": "string",
            "description": "Normalised name (trimmed + lowercased) used for de-duplication within a kind."
          },
          "icon": {
            "type": [
              "string",
              "null"
            ],
            "description": "An SF Symbol name. Unknown names render as a generic glyph."
          },
          "photo": {
            "type": [
              "string",
              "null"
            ],
            "description": "The tag's picture: a file name under the longbook folder's images/ folder, written by the app. Never a URL; it never leaves the folder. Read it, never write it."
          },
          "is_default": {
            "type": "boolean",
            "description": "True for a library-seeded tag. Still the owner's to rename or delete."
          },
          "sort": {
            "type": "integer",
            "description": "Display order within its kind."
          },
          "created_at": {
            "type": "string"
          }
        }
      }
    },
    "charges": {
      "type": "array",
      "description": "Every charge, past and planned — one row per commitment per due date, and the only place an occurrence exists. Nothing is computed from a rule: history is exactly the rows before today, the plan is exactly the rows from today on, and a commitment with no rows has no charges. Rows are written on request — by the owner creating a commitment, by their AI, by a mark — never by the app on its own. Keyed by (commitment_id, date); a correction edits the row in place, never adds a second beside it. Optional; a vault without this array has nothing planned and nothing recorded.",
      "items": {
        "type": "object",
        "required": [
          "commitment_id",
          "date",
          "amount",
          "paid"
        ],
        "properties": {
          "commitment_id": {
            "type": "string",
            "description": "FK → commitments[].id"
          },
          "date": {
            "type": "string",
            "description": "The due date, ISO YYYY-MM-DD — with commitment_id, the identity of the row."
          },
          "amount": {
            "type": "number",
            "description": "What this charge costs or cost, in the commitment's currency unless the row states its own."
          },
          "currency": {
            "type": [
              "string",
              "null"
            ],
            "description": "Only when it differs from the commitment's own currency. Never blended into a total in another currency."
          },
          "paid": {
            "type": "boolean",
            "description": "Whether the money left, as marked. A row dated before today reads as paid whatever this says — the day has passed; the mark matters for today and ahead."
          },
          "paid_on": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO date the money actually moved, when that differs from `date`."
          },
          "basis": {
            "enum": [
              "statement",
              "user",
              "mark_paid",
              "plan",
              "email"
            ],
            "description": "What backs the row. \"plan\": written ahead, at the commitment's price, by the owner or their AI asking for a plan — an attribute edit may reprice it. \"user\": the owner set its amount by hand. \"statement\" / \"email\": a document says so — point _provenance.source at it. \"mark_paid\": the app's own paid-tap. Anything but \"plan\" is a fact; nothing reprices a fact."
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "description": "A word about this one charge — \"₹3,000 paid ahead\". Never a card number or credential."
          },
          "written_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO timestamp of when the row was written. Bookkeeping, never a change signal."
          }
        }
      }
    },
    "groups": {
      "type": "array",
      "description": "How the owner has arranged their accounts, members and tags — see llms.txt. Labels over what already exists, never new records: a group holds ids, or for tags the normalised names, of things already in the file. Optional; a vault without this array is complete.",
      "items": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "name",
          "name_key"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "UUID. Stable — preserve it when renaming."
          },
          "kind": {
            "enum": [
              "account",
              "member",
              "tag"
            ],
            "description": "What the group holds: \"account\" groups list account_ids, \"member\" groups list member_ids, \"tag\" groups list tag_keys. longbook names these; the owner names the group."
          },
          "name": {
            "type": "string",
            "description": "The owner's words. Freely renameable."
          },
          "name_key": {
            "type": "string",
            "description": "Normalised name (trimmed + lowercased) used for de-duplication within a kind."
          },
          "icon": {
            "type": [
              "string",
              "null"
            ],
            "description": "An SF Symbol name; optional."
          },
          "is_default": {
            "type": "boolean",
            "description": "True for a group longbook seeded. Still the owner's to rename or delete."
          },
          "sort": {
            "type": "integer",
            "description": "Display order within its kind."
          },
          "account_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "FK → accounts[].id. Only when kind is \"account\"."
          },
          "member_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "FK → members[].id. Only when kind is \"member\"."
          },
          "tag_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Normalised tag names this group covers. Only when kind is \"tag\". A tag belongs to at most one group."
          },
          "created_at": {
            "type": "string"
          }
        }
      }
    },
    "to_fill_in": {
      "type": "array",
      "description": "Commitments the owner named at setup without an amount or a day: a name, the group it was picked from, the library tag it will wear. Not commitments yet. An agent that learns one's details writes it with add_commitments under the same name, and the app drops the entry. Optional; a vault without this array has nothing waiting.",
      "items": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable id; preserve it."
          },
          "name": {
            "type": "string",
            "description": "The name as the owner ticked or typed it."
          },
          "group": {
            "type": "string",
            "description": "The setup group it came from, e.g. \"Household help\"."
          },
          "tag": {
            "type": "string",
            "description": "The library tag it will wear when it becomes a commitment."
          }
        }
      }
    },
    "hints": {
      "type": "array",
      "description": "People and accounts the owner picked at setup, by role or by kind — \"Cook\", \"Savings account\" — kept as hints for reading statements. Not members and not accounts: nothing is billed to a hint and nothing points at one. A hint becomes a real person or account only when a statement or the owner names it, under that name. Optional; a vault without this array holds no hints.",
      "items": {
        "type": "object",
        "required": [
          "id",
          "name",
          "kind"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable id; preserve it."
          },
          "name": {
            "type": "string",
            "description": "The role or kind as the owner picked it, e.g. \"Cook\", \"Credit card\"."
          },
          "kind": {
            "enum": [
              "member",
              "account"
            ],
            "description": "Whether it hints at a person or an account."
          },
          "tag": {
            "type": "string",
            "description": "The tag the person or account would wear: a member role, or an account type such as \"bank account\"."
          }
        }
      }
    },
    "_provenance": {
      "type": "object",
      "description": "Who last edited each record, keyed by its id. Optional but requested: if you are a tool editing this vault, add an entry for every record you touch. longbook shows the owner each outside change for review, and an entry here is what lets it say the change came from you instead of labelling it an unknown edit. An entry may outlive its row: longbook signs a row it removed so its other device adopts the removal, and prunes such entries after a month.",
      "additionalProperties": {
        "type": "object",
        "properties": {
          "edited_by": {
            "type": "string",
            "description": "Who made the change, e.g. \"claude\", \"chatgpt\", \"cursor\", \"human\"."
          },
          "edited_at": {
            "type": "string",
            "description": "ISO timestamp of the edit."
          },
          "source": {
            "type": "string",
            "description": "Optional — where the data came from, e.g. \"documents/statement-aug.pdf\"."
          },
          "reviewed_by": {
            "type": "string",
            "description": "longbook's own: the app's name, written when the owner kept this change in Proposals so their other device adopts it. Never write it; when you edit the row, replace the whole entry."
          },
          "reviewed_at": {
            "type": "string",
            "description": "ISO timestamp of that review."
          }
        }
      }
    }
  }
}
