# Asset Notation Specification - Version 0.4.0

Status: RELEASED. Pre-1.0: the data model is exercised against real data before any
v1.0, and a minor version may break; this one does not.

Changes from 0.3.0 (additive, the rest clarifies what 0.3.0 left unsaid):

- **Ownership over time.** An ownership row MAY carry `from` and `until`, the
  interval during which the share holds, so a share that changes (a gift, a
  marriage, a buy-in) is recorded as history rather than rewritten. Two rows for
  the same subject and holding MUST NOT overlap (section 5.7).
- **A closed holding stops counting.** A holding contributes nothing to a net worth
  dated on or after its `closedAt`, nor before its `openedAt`; a producer SHOULD
  still emit a final valuation at closure (sections 5.3, 6, 14).
- **The sign of a transaction is the holding's own.** A positive `amount` raises the
  holding's value under its sign convention: on a liability, a drawdown is positive
  and a repayment negative. The legs of one `transferId` net to zero once each is
  signed by its holding's nature (section 5.6).
- **Parent chains are acyclic, and a child keeps its own nature.** A `parentId`
  loop is an integrity error. In the derivation, each holding is signed by its own
  `nature`: a liability held inside an asset account is subtracted, not added
  (sections 4, 6).
- **A calendar date must exist.** The schema applies the `date` format on top of the
  `YYYY-MM-DD` pattern, so `2026-02-30` is refused as a timestamp without a zone
  already was (section 3).

A document valid under 0.3.0 stays valid here unless it carried a date that does
not exist.

## 1. Purpose and scope

Asset Notation is an open, portable data format for a person's or household's
**assets and liabilities over time**: the accounts, positions, properties, debts and
their values, across institutions and asset classes, in a single self-describing
document that any application can read and write. The consolidated net worth is one
thing it makes derivable, but the format itself is the broader notation: it records
*what you own and owe*, not a dashboard.

It exists to fill a concrete gap: today wealth data is locked inside each aggregator
and institution, with no neutral interchange format. Asset Notation is the neutral
payload. It does not replace bank-messaging standards (ISO 20022) or brokerage feeds;
it consolidates their outputs into one portable picture the owner controls.

In scope (v0.4): subjects/owners, institutions, **holdings** (the single generic
asset-or-liability entity), optional instruments for market-priced positions,
valuations over time, optional transactions, ownership shares.

Out of scope (v0.4): distribution of market-data and FX rate tables, tax computation,
budgeting categories, performance analytics, real-time messaging. These belong in
tools or in namespaced extensions, not in the core. A record MAY however capture the
single FX rate it was booked at (sections 5.5, 5.6); what stays out of scope is
serving rates as data.

## 2. Design principles

1. **One generic entity.** Anything you own or owe is a **Holding** - a bank account,
   a savings book, a brokerage position, a house, a car, a mortgage. An open `kind`
   says what flavour it is; a `nature` (`asset` or `liability`) says which side of the
   balance sheet it sits on. There is no separate account / holding / liability
   hierarchy to learn or to migrate.
2. **Reuse existing identifiers; never invent.** Currencies, dates, securities,
   institutions and accounts are referenced through established ISO standards.
3. **Model facts, not dashboards.** The document stores what is true (a holding, a
   value as of a date). "Net worth at time T" is *derived*, never stored.
4. **Valuation is first-class, time-stamped and sourced.** A value always carries its
   as-of date and where it came from, because wealth includes illiquid,
   subjectively-valued assets, and because the value of everything moves.
5. **Money is never a float.** Amounts are decimal strings with an explicit currency;
   quantities and ownership shares are decimal strings too. No accounting figure is
   ever a binary float.
6. **Forward-compatible and extensible.** Unknown fields and unknown `kind` / `source`
   values are preserved, not rejected. App-specific data lives in namespaced
   extensions, keeping the core small. New kinds and new fields never force a migration.
7. **Privacy by construction.** The document is plaintext; confidentiality is the
   container's job (section 11). Any subset of a document is still valid, so partial
   disclosure is natural.
8. **Human-inspectable.** Plain JSON: diffable, greppable, hand-editable.

## 3. Conventions

- The key words MUST, SHOULD, MAY are used as in RFC 2119.
- Encoding is JSON (RFC 8259), UTF-8.
- **Dates** are ISO 8601 calendar dates (`YYYY-MM-DD`) that exist: the schema
  refuses `2026-02-30`. **Timestamps** are RFC 3339
  (`2026-06-24T20:00:00Z`), UTC unless an offset is given. Where a field records *when*
  something held or happened - a valuation's `asOf`, a transaction's `date` - it MAY be
  either a date or a timestamp; consumers SHOULD compare them as instants, treating a
  bare date as the start of that day, UTC.
- **Currencies** are ISO 4217 alphabetic codes (`EUR`, `USD`).
- **Countries** are ISO 3166-1 alpha-2 (`FR`, `US`).
- **Money** is an object `{ "amount": string, "currency": string }`, where `amount` is
  a base-10 decimal string (e.g. `"1234.56"`, `"-50.00"`). Producers MUST NOT emit
  floating-point numbers for monetary values. Quantities (share counts, units) are
  also decimal strings to avoid precision loss, and so are ownership shares
  (section 5.7).
- **Identifiers**: every entity has an `id`, a string unique within its collection. An
  `id` is opaque and stable across exports of the same logical thing (a UUID or a
  stable slug is RECOMMENDED). External identifiers (ISIN, IBAN, BIC, LEI, ...) live in
  an `ids` object and are never used as the `id`.

## 4. Document structure

A document is a single JSON object:

```json
{
  "assetnotation": "0.4.0",
  "generatedAt": "2026-07-08T09:00:00Z",
  "generator": { "name": "example", "version": "1.0.0" },
  "baseCurrency": "EUR",
  "subjects": [],
  "institutions": [],
  "holdings": [],
  "instruments": [],
  "valuations": [],
  "transactions": [],
  "ownership": [],
  "extensions": {}
}
```

| Field | Req | Type | Notes |
|---|---|---|---|
| `assetnotation` | MUST | string | Spec version `major.minor.patch` of this document. |
| `generatedAt` | MUST | timestamp | When the document was produced. |
| `generator` | MAY | object | `{ name, version }` of the producing app. |
| `baseCurrency` | SHOULD | ISO 4217 | Reporting currency for derived totals. |
| `subjects` | SHOULD | Subject[] | Owners. If omitted, a single implicit owner is assumed. |
| `institutions` | MAY | Institution[] | Banks, brokers, lenders. |
| `holdings` | MUST | Holding[] | The assets and liabilities. The heart of the document. |
| `instruments` | MAY | Instrument[] | Market-priced securities referenced by holdings. |
| `valuations` | SHOULD | Valuation[] | Time-stamped values of holdings. |
| `transactions` | MAY | Transaction[] | Optional movement history. |
| `ownership` | MAY | Ownership[] | Shares. Defaults to 100% to the primary subject. |
| `extensions` | MAY | object | Namespaced app-specific data (section 10). |

Collections are unordered sets keyed by `id`. This maps directly onto a
schema-agnostic store of named collections (section 12).

**Document integrity.** An `id` MUST be unique within its collection. References
(`holdingId`, `parentId`, `institutionId`, `instrumentId`, `subjectId`,
`counterpartHoldingId`, a link's `holdingId`) SHOULD resolve to an entity in the same
document, but MAY dangle: any subset of a valid document is itself valid (section 11),
so a partial disclosure can omit a referenced entity. A consumer MUST tolerate an
unresolved reference - treat the relation as unknown - and MUST NOT reject the
document for it. A chain of `parentId` MUST be acyclic: a holding is never its own
ancestor, since the derivation of section 6 walks the chain and a loop has no root.
The reference validator rejects duplicate ids and cycles, and reports unresolved
references informatively (section 13).

## 5. Core entities

### 5.1 Subject (owner)

A person or legal entity that can own assets and owe liabilities.

| Field | Req | Type | Notes |
|---|---|---|---|
| `id` | MUST | string | |
| `type` | MUST | enum | `person`, `entity`. |
| `name` | SHOULD | string | Display name. |
| `kind` | MAY | enum | e.g. `self`, `spouse`, `child`, `company`, `trust`, `sci`, `joint`. Open. |
| `country` | MAY | ISO 3166 | Tax residence or registration. |
| `ids` | MAY | object | e.g. `{ "lei": "..." }` for legal entities. |

### 5.2 Institution

A bank, broker, insurer, exchange, custodian or lender.

| Field | Req | Type | Notes |
|---|---|---|---|
| `id` | MUST | string | |
| `name` | SHOULD | string | |
| `kind` | MAY | string | Open. RECOMMENDED: `bank`, `broker`, `insurer`, `exchange`, `custodian`, `other`. A consumer MUST treat an unknown kind generically. |
| `country` | MAY | ISO 3166 | |
| `ids` | MAY | object | `{ "bic"?: ISO 9362, "lei"?: ISO 17442 }`. |

### 5.3 Holding (the generic entity)

Anything of value, owned or owed. A bank account, a savings book, a brokerage
position, a directly-held house or car, a mortgage or a consumer loan are all
Holdings. Its values over time are Valuations (5.5); its movements, if any, are
Transactions (5.6).

| Field | Req | Type | Notes |
|---|---|---|---|
| `id` | MUST | string | |
| `kind` | MUST | string | Open: `checking`, `savings`, `brokerage`, `retirement`, `life_insurance`, `crypto_wallet`, `cash`, `real_estate`, `vehicle`, `valuable`, `mortgage`, `loan`, `credit_line`... A consumer MUST accept an unknown kind and render it generically. |
| `nature` | MUST | enum | `asset` or `liability`. Drives the sign in net worth. |
| `label` | SHOULD | string | Display name. |
| `currency` | SHOULD | ISO 4217 | The holding's native currency. |
| `parentId` | MAY | ref | The containing holding (a stock in a fund, a fund in a securities account). A holding with no parent is a root. |
| `institutionId` | MAY | ref | -> Institution. |
| `ids` | MAY | object | `{ "iban"?: ISO 13616, "bic"?, "number"? }`. `number` SHOULD be masked. |
| `instrumentId` | MAY | ref | -> Instrument, for a market-priced position. |
| `quantity` | MAY | decimal string | Units held, when `instrumentId` is set. |
| `costBasis` | MAY | Money | Total acquisition cost of the current position, in any currency. Informative: a tool MAY maintain it from transactions. A corporate action (5.6) changes `quantity` but leaves `costBasis` unchanged. |
| `links` | MAY | Link[] | Relations to other holdings (below). |
| `attributes` | MAY | object | Type-specific scalars the core does not model (a loan's `rate`, a property's `address`...). Forward-compatible: producers put extra kind-specific data here, not in core fields. |
| `openedAt` / `closedAt` | MAY | date | The holding exists from `openedAt` inclusive to `closedAt` exclusive; outside that interval it contributes nothing to a derived total (section 6). A producer SHOULD emit a final valuation at closure (a zero balance, the payoff of a loan), because a consumer that reads the last value alone would otherwise show a balance that no longer exists. |

A **Link** is `{ "rel": string, "holdingId": ref }`. `rel` is an open vocabulary:
`funded_by` (a card -> its account), `financed_by` (a property -> its mortgage),
`held_in` (a position -> its account), `cash_of` (a cash account -> the holding it
serves: the settlement cash paired with a securities account, physically separate but
logically one envelope). Links express the relationships a 360 view needs without
nesting.

Sign convention: a holding's values are its **native signed amounts**. An
account-style holding carries a signed balance (a negative balance is an overdraft or
a card debt, and lowers net worth on its own). A loan-style holding carries a
**positive outstanding** and is marked `nature: "liability"`, so net worth subtracts
it. This keeps both representations consistent (section 6). A liability's values MUST
be the outstanding amount as stated by the institution (typically positive): a
producer MUST NOT negate a value to express what `nature` already expresses, since the
double negative would flip the sign back.

### 5.4 Instrument (optional)

A market-priced thing referenced by a holding, so the shared price lives once. Only
needed for securities and similar; a house is just a holding with a value.

| Field | Req | Type | Notes |
|---|---|---|---|
| `id` | MUST | string | |
| `name` | SHOULD | string | |
| `kind` | MUST | string | Open. RECOMMENDED: `security`, `cash`, `crypto`, `commodity`, `other`. A consumer MUST treat an unknown kind generically. |
| `ids` | MAY | object | `{ "isin"?: ISO 6166, "figi"?, "ticker"?, "mic"?: ISO 10383 }`. |
| `currency` | MAY | ISO 4217 | Pricing currency. |

### 5.5 Valuation

The value of a holding, **as of a date**, from a stated source. Valuations form a time
series; multiple per holding are expected (a statement each month, an appraisal each
year, the amortized balance of a loan over its life).

| Field | Req | Type | Notes |
|---|---|---|---|
| `id` | MUST | string | |
| `holdingId` | MUST | ref | -> Holding. |
| `asOf` | MUST | date/timestamp | When the value held true. |
| `value` | MUST | Money | The total value of the holding (signed per 5.3). |
| `unitPrice` | MAY | Money | For an instrument-priced holding: price per unit. |
| `quantity` | MAY | decimal string | Units held as of `asOf`, making the valuation a self-sufficient position snapshot. |
| `fx` | MAY | FxRate | The conversion rate captured when the record was made (below). |
| `source` | SHOULD | string | Open: `purchase`, `statement`, `market`, `estimate`, `appraisal`, `repayment`, `manual`, `derived`... |
| `confidence` | MAY | number | 0..1. An estimate is softer than a statement. |

An **FxRate** is `{ "base": ISO 4217, "rate": decimal string }`: one unit of the
record's currency equals `rate` units of `base`, at the record's date. It freezes the
conversion the producer actually used, so a foreign-currency record keeps its exact
counter-value even when historical rate feeds differ or disappear.

When `quantity` and `unitPrice` are both present and their product disagrees with
`value` (statement rounding), **`value` takes precedence**: it is the institution's
stated total. A valuation whose `asOf` lies in the future is a **projection** (a
loan's amortization schedule, a vesting plan), not a fact; a producer SHOULD mark it
with a projective `source` (`projected`, `schedule`) so consumers can keep facts and
forecasts apart.

### 5.6 Transaction (optional)

A movement on a holding. Optional: a document MAY carry valuations only. Semantics
align loosely with ISO 20022 / OFX.

| Field | Req | Type | Notes |
|---|---|---|---|
| `id` | MUST | string | |
| `holdingId` | MUST | ref | -> Holding. |
| `date` | MUST | date/timestamp | The day, or a full RFC 3339 instant when the exact time matters (a trade execution, an on-chain settlement). |
| `amount` | MUST* | Money | Signed from the holding's own perspective, in the direction of its value (below). *Not required when `type` is `corporate_action` (below). |
| `type` | MAY | string | Open. RECOMMENDED: `transfer`, `trade`, `fee`, `dividend`, `interest`, `income`, `expense`, `corporate_action`, `other`. A consumer MUST treat an unknown type generically. |
| `factor` | MAY* | decimal string | Quantity multiplier applied by a corporate action: `"2"` for a 2-for-1 split, `"0.5"` for a reverse split, an exchange ratio for a merger. *Required when `type` is `corporate_action`. |
| `fx` | MAY | FxRate | The conversion rate captured at booking time (5.5). |
| `transferId` | MAY | string | Opaque id shared by the legs of one internal movement (below). |
| `description` | MAY | string | |
| `category` | MAY | string | Open budgeting category. |
| `counterpartHoldingId` | MAY | ref | The other side of the flow: a transfer target, the loan repaid, the asset funded. |
| `instrumentId` / `quantity` | MAY | ref / decimal string | For trades; for a corporate action, the instrument affected (and optionally the resulting instrument of a merger via a new `instrumentId` on the holding). |

**Sign.** A positive `amount` raises the holding's value as section 5.3 states it,
a negative one lowers it. On an asset, an inflow is positive and an outflow negative.
On a liability, whose value is its positive outstanding, a drawdown or a capitalized
interest is positive and a repayment is negative. A repayment of 990 from a current
account to a mortgage is therefore `-990` on the account and `-990` on the mortgage:
each leg lowers its own holding's value, and the owner's net worth is unchanged.

**Corporate actions.** A split, reverse split or merger changes the number of units
held without any cash moving and without changing the capital invested. It is recorded
as a `corporate_action` transaction carrying a `factor` and no required `amount`:

```json
{ "id": "t2", "holdingId": "h3", "date": "2026-06-16", "type": "corporate_action",
  "factor": "2", "description": "2-for-1 stock split" }
```

After the event the holding's `quantity` reflects the new unit count and `costBasis`
is unchanged. An `amount` MAY still appear (cash in lieu of fractional units). A
consumer that ignores corporate actions still derives correct totals from valuations;
the event exists so that unit counts, cost basis and per-unit history remain
explainable.

**Internal movements.** When both legs of one internal movement of value appear in the
document - the debit and credit of a transfer between two holdings, a trade and its
cash settlement - they SHOULD share a `transferId`, an opaque id meaningful only
within the document. Each leg's `amount` is signed from its own holding's perspective,
and the legs of one movement SHOULD net to zero once each leg is signed by its
holding's nature (`+1` asset, `-1` liability) and converted by any captured FX: that
is the net-worth effect of the move, zero by definition. A consumer
computing external flows (income, spending, savings rate) SHOULD net or exclude the
transactions grouped by a `transferId`: without pairing, every internal transfer
double-counts as both an expense and an income. `counterpartHoldingId` names the other
side; `transferId` pairs the exact records.

### 5.7 Ownership (shares)

Who owns how much of what. Enables joint accounts, family consolidation and holding
structures. If `ownership` is omitted, every holding is owned 100% by the **primary
subject**: the first entry of `subjects`, or the single implicit owner when `subjects`
is omitted too.

| Field | Req | Type | Notes |
|---|---|---|---|
| `subjectId` | MUST | ref | -> Subject. |
| `holdingId` | MUST | ref | -> Holding. |
| `share` | MUST | decimal string | In [0, 1], e.g. `"0.5"`, `"0.333333"`. Shares of one holding SHOULD sum to <= 1 on any day; exact thirds do not exist in decimal, so a producer SHOULD round down. |
| `from` | MAY | date | First day the share holds, inclusive. Absent: since always. |
| `until` | MAY | date | Day the share stops holding, exclusive. Absent: still holds. |

Ownership is history, like valuations: a share that changes is recorded by closing
the old row with `until` and adding a new one with `from`, never by rewriting
(section 11). Two rows for the same `subjectId` and `holdingId` MUST NOT overlap in
time; the reference validator rejects an overlap (section 13). The share of a holding
owned by a subject as of a date T is the `share` of the row whose interval contains T,
or zero when there is none.

## 6. Deriving net worth (non-normative)

A standard models facts; tools compute. Net worth for a set of subjects S as of date T:

```text
netWorth(S, T) =
    sum over LEAF holdings h (no holding names h as parentId) of
        sign(h.nature) * ownValue(h, T) * ownedShare(root(h), S, T)
  where ownValue(h, T) = 0 when T < h.openedAt or T >= h.closedAt,
                         else latestValue(h, T);
        sign(asset) = +1, sign(liability) = -1;
        root(h) = the top of h's parentId chain, which is finite (section 4);
        all converted to baseCurrency using an FX rate as of T.
```

A container (a fund, a securities account) is therefore counted through its leaves,
never twice, and each leaf carries its own sign: a margin loan held inside a brokerage
account is subtracted, whatever the account's nature. Ownership is read on the root
of the chain, since a position is owned through the account that holds it. A holding
that exists at T but has no valuation dated on or before T contributes zero.

`latestValue(h, T)` is the `value` of the valuation of `h` with the greatest `asOf` <=
T. A richer tool MAY refine an account's value between statements using its
transactions; this reference derivation uses valuations only. When a record carries a
captured `fx` (5.5), a tool SHOULD use that rate to convert that record, and an
external rate otherwise; rate tables are not distributed by Asset Notation (out of
scope). `ownedShare(h, S, T)` sums, over h's ownership rows whose subject is in S and
whose interval (`from` inclusive, `until` exclusive, section 5.7) contains T, the
`share` decimal strings parsed exactly; with no `ownership` at all, it is 1 for the
primary subject. This is non-normative: implementations MAY derive differently, but
SHOULD document how.

## 7. Identifiers and code lists

| Concept | Standard |
|---|---|
| Currency | ISO 4217 |
| Date / time | ISO 8601 / RFC 3339 |
| Country | ISO 3166-1 alpha-2 |
| Language (in extensions) | BCP 47 |
| Security | ISIN (ISO 6166); FIGI, ticker as alternates |
| Exchange | MIC (ISO 10383) |
| Institution | BIC (ISO 9362), LEI (ISO 17442) |
| Bank account | IBAN (ISO 13616) |

Producers SHOULD populate `ids` with whichever external identifiers they have.
Consumers MUST NOT assume any `ids` entry is present.

## 8. Money representation

Money is `{ "amount": "<decimal string>", "currency": "<ISO 4217>" }`. The decimal
string is base-10, optional leading `-`, `.` as the decimal separator, no thousands
separators, no currency symbol. Rationale: binary floats cannot represent most decimal
cash values exactly; ISO 20022 likewise uses decimal.

## 9. Compatibility and versioning

- The `assetnotation` field carries the spec version as `major.minor.patch`.
  Compatibility is determined by **major.minor**; the patch is editorial (clarifications
  that do not change the data model).
- A consumer MUST reject a document whose `major` it does not support. A consumer
  SHOULD accept a higher `minor` and MUST ignore fields it does not recognize.
- A consumer that round-trips a document (reads then writes) MUST preserve unknown
  fields and unknown `extensions` namespaces.
- New `kind` and `source` values, and new enum values, MAY appear in a minor version; a
  consumer MUST treat an unknown one generically rather than failing.
- Version 0.2.0 was additive over 0.1.0. Version 0.3.0 is **not** strictly additive
  over 0.2.0 (the pre-1.0 policy allows a minor to break): `ownership.share` changed
  from a JSON number to a decimal string, so a document carrying `ownership` must
  quote its shares to validate as 0.3.0. Everything else is additive: the opened
  vocabularies accept every previously-valid value, and a 0.2.0 consumer reading a
  0.3.0 document per the rules above ignores `transferId` and a valuation's
  `quantity`.
- Version 0.4.0 is additive over 0.3.0: `from` and `until` on ownership rows. The
  schema also refuses a calendar date that does not exist, which 0.3.0 accepted by
  oversight. The rest of 0.4.0 states what 0.3.0 left unsaid (closed holdings, the
  sign of a transaction on a liability, acyclic parents, a child's own sign); a
  document is not changed by it, a derived total may be.

## 10. Extensibility

App-specific or domain-specific data that is not part of the core MUST NOT be placed in
core fields. Three mechanisms, from most to least scoped:

- A holding's `attributes` object for kind-specific scalars (a loan's rate, a
  property's surface). Already part of the core, so no namespace needed.
- A top-level `extensions` object, keyed by a namespace (reverse-DNS or app id):
  `"extensions": { "com.example": { ... } }`.
- An `x-` prefixed field on any entity for small additions: `"x-color": "..."`.

This keeps the core small and stable while letting tools carry their own state (for
example an app's own categorization rules) without polluting the standard.

## 11. Security and privacy

Asset Notation documents are plaintext and inherently sensitive. Confidentiality,
integrity and tamper-evidence are the responsibility of the **container** that carries
the document (for example an encrypted, authenticated container), not of this schema.

- Account numbers SHOULD be masked; an IBAN MAY be partially redacted.
- Any subset of a valid document is itself a valid document, so a producer can support
  partial disclosure (omit transactions, omit a subject) without special handling.
- The schema introduces no field whose presence is required to reveal more PII than the
  owner chooses.

**Records are append-only by convention.** Past valuations and transactions are facts:
a producer SHOULD correct history by adding a record (a new valuation that supersedes
by `asOf`, a corrective transaction), not by rewriting an old one. Rewriting silently
changes every view derived from the document; detecting such a rewrite remains the
container's job (versioned or content-addressed storage), not the schema's.

## 12. Containers and transport

Asset Notation is a *payload schema*, independent of how a document is stored or moved.
A document MAY live in a plain file, inside a versioned and optionally encrypted
container, in a ZIP, or be served over an API; none of that is the schema's concern.

Because a document is a set of collections keyed by `id` (`subjects`, `institutions`,
`holdings`, `valuations`, `transactions`, `ownership`), it maps one-to-one onto any
document or key-value store. A producer whose internal model is Asset-Notation-shaped
can therefore emit a conformant document directly from its own state, reconciling
entities by `id`.

## 13. Validation policy

The JSON Schema (`schema/<version>/`) is the machine-readable contract. Its strictness
is deliberate and follows one rule, so that "valid" means the same thing to every
implementation:

- **Value objects are closed.** `money` (`{ amount, currency }`) and `fxRate`
  (`{ base, rate }`) set `additionalProperties: false`. They are complete, universal
  shapes with no room to grow: an unexpected key is a mistake, and the schema rejects it.
  This catches the common producer bug - a misspelled or misplaced monetary field - at
  the boundary.
- **Domain entities are open.** Subjects, institutions, holdings, instruments,
  valuations, transactions, ownership and links set `additionalProperties: true`. This is
  the forward-compatibility guarantee of principle 6 and section 9 made concrete: a newer
  producer MAY add a field, and an older validator MUST NOT reject the document for it.
  Structured app-specific data still belongs in the scoped channels of section 10
  (`attributes`, `extensions`, `x-`), never as a loose core key.
- **Known identifier formats are validated where masking cannot apply.** Inside `ids`,
  the keys that never carry sensitive account data - `bic`, `isin`, `lei`, `mic` -
  MUST match their ISO patterns when present. The maskable keys (`iban`, `number`;
  section 11) stay free-form: a redacted value is a feature, not a typo. The `ids`
  objects themselves stay open to unknown keys.

The consequence, stated plainly: open validation does **not** catch a misspelled
*optional* field on an entity (a stray `amoutn` on a transaction validates as an unknown
field and is preserved untouched). The schema guarantees the structure and types of the
fields it knows, and the closure of value objects; it does not guarantee that a producer
spelled every optional field correctly. Producers are responsible for that, and SHOULD
validate their output against both the schema and their own fixtures.

A stricter, closed-world profile (`additionalProperties: false` everywhere, extensions
allowed only through the declared channels) would turn every such typo into an error. It
is intentionally **not** adopted in 0.x: it would reject documents that are valid today
(any document carrying an extra field), so it is a breaking change reserved for a future
major version. Until then, strictness is exactly this: closed value objects, open
entities.

Beyond the schema, the reference validator enforces the document-level invariants a
JSON Schema cannot express: a duplicate `id` within a collection, a `parentId` chain
that loops, and two ownership rows of one subject on one holding whose intervals
overlap are errors; an unresolved reference is reported informatively but permitted
(section 4), and so are shares of one holding summing above 1 on some day.

## 14. Conformance

- A **conformant producer** MUST emit `assetnotation` and `generatedAt`; MUST give every
  entity an `id` unique within its collection; MUST give every holding a `kind` and a
  `nature`; MUST represent money, quantities and ownership shares as decimal strings
  with ISO 4217 currencies and dates as ISO 8601; MUST give a `corporate_action`
  transaction a `factor`; MUST keep `parentId` chains acyclic and ownership intervals
  of one subject on one holding disjoint; SHOULD pair the legs of an internal
  movement with a `transferId` netting to zero; SHOULD populate `valuations` so
  totals can be derived, and emit a final one when a holding closes.
- A **conformant consumer** MUST parse the core entities, MUST preserve unknown fields on
  round-trip, MUST treat an unknown `kind`/`source`/enum value generically, and MUST
  reject an unsupported `major` version.
- The minimal valid document is
  `{ "assetnotation": "0.4.0", "generatedAt": "<ts>", "holdings": [] }`.

## 15. Open questions (for v0.4+)

- FX: record-level captured rates landed in 0.2.0; distributing rate *tables*
  in-document stays out of scope. Revisit only if real interchange needs it.
- Ownership shares are decimal strings; exact thirds still round. If real interchange
  ever needs exactness, a fraction form (`{ "num": 1, "den": 3 }`) could join as an
  alternative representation. Not before demand. Ownership carries a time interval
  since 0.4.0; rights that differ in kind (below) still do not.
- Cost basis is position-level in 0.2.0; per-lot basis (tax lots) may matter for tax
  tooling. Extension or core?
- Split ownership rights (usufruct / bare ownership and other dismemberments): a
  scalar `share` cannot express rights that differ in kind and carry their own
  time-evolving values. Needs a rights model if real demand appears.
- Performance/return series: extension, or core?
- Should `instruments` stay optional, or is the position model (instrument + quantity)
  worth promoting once securities are first-class in implementations?
- Vesting/options/illiquid comp: model now or later?
- A conformance test corpus (beyond the example fixtures) before v1.0.
- Governance: BDFL draft now; align with or submit to a standards body later, or remain a
  de-facto open spec (the realistic target). See [GOVERNANCE.md](../GOVERNANCE.md).

## 16. Worked example

A household: one person, a checking account with a cash balance, a brokerage position in
an ETF, a directly-owned apartment financed by a mortgage, and the month's mortgage
payment recorded on both sides.

```json
{
  "assetnotation": "0.4.0",
  "generatedAt": "2026-07-08T09:00:00Z",
  "generator": { "name": "example", "version": "1.0.0" },
  "baseCurrency": "EUR",
  "subjects": [
    { "id": "s1", "type": "person", "name": "A. Doe", "country": "FR" }
  ],
  "institutions": [
    { "id": "i1", "name": "Example Bank", "kind": "bank", "ids": { "bic": "EXAMFRPPXXX" } },
    { "id": "i2", "name": "Example Broker", "kind": "broker" }
  ],
  "holdings": [
    { "id": "h1", "label": "Current account", "kind": "checking", "nature": "asset", "currency": "EUR", "institutionId": "i1", "ids": { "iban": "FR76 XXXX XXXX XX" } },
    { "id": "h2", "label": "World ETF", "kind": "brokerage", "nature": "asset", "currency": "EUR", "institutionId": "i2", "instrumentId": "n1", "quantity": "40", "costBasis": { "amount": "3936.00", "currency": "EUR" } },
    { "id": "h3", "label": "12 Main St", "kind": "real_estate", "nature": "asset", "currency": "EUR", "links": [{ "rel": "financed_by", "holdingId": "h4" }] },
    { "id": "h4", "label": "Mortgage", "kind": "mortgage", "nature": "liability", "currency": "EUR", "institutionId": "i1", "attributes": { "rate": 1.8, "startDate": "2022-01-01", "endDate": "2042-01-01", "payment": "990.00" } }
  ],
  "instruments": [
    { "id": "n1", "name": "World ETF", "kind": "security", "ids": { "isin": "IE00B4L5Y983" }, "currency": "EUR" }
  ],
  "valuations": [
    { "id": "v1", "holdingId": "h1", "asOf": "2026-06-24", "value": { "amount": "4210.55", "currency": "EUR" }, "source": "statement" },
    { "id": "v2", "holdingId": "h2", "asOf": "2026-06-24", "value": { "amount": "4920.00", "currency": "EUR" }, "unitPrice": { "amount": "123.00", "currency": "EUR" }, "source": "market" },
    { "id": "v3", "holdingId": "h3", "asOf": "2026-06-01", "value": { "amount": "300000.00", "currency": "EUR" }, "source": "estimate", "confidence": 0.5 },
    { "id": "v4", "holdingId": "h4", "asOf": "2026-06-24", "value": { "amount": "176400.00", "currency": "EUR" }, "source": "statement" }
  ],
  "transactions": [
    { "id": "t1", "holdingId": "h1", "date": "2026-06-05", "type": "transfer", "amount": { "amount": "-990.00", "currency": "EUR" }, "counterpartHoldingId": "h4", "transferId": "x1", "description": "Mortgage payment" },
    { "id": "t2", "holdingId": "h4", "date": "2026-06-05", "type": "transfer", "amount": { "amount": "-990.00", "currency": "EUR" }, "counterpartHoldingId": "h1", "transferId": "x1", "description": "Mortgage payment" }
  ],
  "ownership": [
    { "subjectId": "s1", "holdingId": "h1", "share": "1" },
    { "subjectId": "s1", "holdingId": "h2", "share": "1" },
    { "subjectId": "s1", "holdingId": "h3", "share": "1" },
    { "subjectId": "s1", "holdingId": "h4", "share": "1" }
  ]
}
```

Net worth as of 2026-06-24: 4210.55 + 4920.00 + 300000.00 - 176400.00 = 132730.55 EUR.
The payment `x1` lowers the account by 990 and the outstanding by 990: signed by
nature, (+1)(-990) + (-1)(-990) = 0, the owner's net worth is unchanged by it.
A multi-currency brokerage with a settlement cash account, a captured FX rate, a pair
of trade legs sharing a `transferId` and a stock split is worked through in
[`examples/brokerage.json`](../examples/brokerage.json).
