@book.dev/sdk 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/backup.d.ts +75 -1
  2. package/dist/backup.js +8 -1
  3. package/dist/backup.js.map +1 -1
  4. package/dist/client.d.ts +134 -2
  5. package/dist/client.js +171 -0
  6. package/dist/client.js.map +1 -1
  7. package/dist/csv.d.ts +49 -0
  8. package/dist/csv.js +116 -0
  9. package/dist/csv.js.map +1 -0
  10. package/dist/importAssets.d.ts +10 -0
  11. package/dist/importAssets.js +17 -0
  12. package/dist/importAssets.js.map +1 -1
  13. package/dist/index.d.ts +10 -4
  14. package/dist/index.js +9 -3
  15. package/dist/index.js.map +1 -1
  16. package/dist/ledger.d.ts +772 -0
  17. package/dist/ledger.js +493 -0
  18. package/dist/ledger.js.map +1 -0
  19. package/dist/ledgerBeancount.d.ts +117 -0
  20. package/dist/ledgerBeancount.js +309 -0
  21. package/dist/ledgerBeancount.js.map +1 -0
  22. package/dist/ledgerBeancountFixture.d.ts +61 -0
  23. package/dist/ledgerBeancountFixture.js +459 -0
  24. package/dist/ledgerBeancountFixture.js.map +1 -0
  25. package/dist/ledgerCsv.d.ts +76 -0
  26. package/dist/ledgerCsv.js +197 -0
  27. package/dist/ledgerCsv.js.map +1 -0
  28. package/dist/money.d.ts +179 -0
  29. package/dist/money.js +283 -0
  30. package/dist/money.js.map +1 -0
  31. package/dist/notionImport.d.ts +5 -3
  32. package/dist/notionImport.js +6 -56
  33. package/dist/notionImport.js.map +1 -1
  34. package/dist/plugins.d.ts +24 -0
  35. package/dist/plugins.js +25 -0
  36. package/dist/plugins.js.map +1 -1
  37. package/dist/provenance.d.ts +17 -0
  38. package/dist/provenance.js.map +1 -1
  39. package/dist/routes.d.ts +121 -0
  40. package/dist/routes.js +122 -0
  41. package/dist/routes.js.map +1 -1
  42. package/dist/types.d.ts +8 -0
  43. package/dist/types.js.map +1 -1
  44. package/package.json +1 -1
package/dist/ledger.js ADDED
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Ledger contract (LGR-3): the shared types, typed errors, and pure audit-replay
3
+ * reducer for the server-enforced double-entry ledger.
4
+ *
5
+ * Data model: ledger data lives in FOUR server-managed OpenBook databases
6
+ * (accounts / transactions / postings / reconciliations) seeded on a restricted
7
+ * host page. Rows are ordinary pages; every ledger value is a database property.
8
+ * The server's `LedgerStore` is the ONLY writer — the generic page/row mutation
9
+ * surface (HTTP routes AND direct store calls) rejects writes to ledger rows, so
10
+ * the invariants hold in both server mode and browser-local (PGlite) mode.
11
+ *
12
+ * Money: amounts are SIGNED INTEGER MINOR UNITS (LGR-2, see `./money`). Amounts
13
+ * on the wire are integers — never parsed strings.
14
+ */
15
+ /**
16
+ * HTTP status the API maps each {@link LedgerErrorCode} to.
17
+ *
18
+ * The three LGR-11 reconciliation rejections are 409, not 400: they are all
19
+ * "not valid for the CURRENT state of the book" rather than malformed requests.
20
+ * The identical call becomes legal once the open reconciliation is finished,
21
+ * once the difference reaches zero, or once the posting is posted — which is
22
+ * what tells a client to re-read and retry instead of fixing its payload.
23
+ */
24
+ export function ledgerErrorStatus(code) {
25
+ switch (code) {
26
+ case 'not-initialized':
27
+ case 'not-found':
28
+ return 404;
29
+ case 'managed':
30
+ case 'immutable':
31
+ case 'reconciled-locked':
32
+ return 403;
33
+ // The three LGR-12 period rejections are state-of-the-book conflicts too:
34
+ // the identical call becomes legal once the period is reopened (or, for a
35
+ // close conflict, simply retried against the settled book). So is the LGR-14
36
+ // evidence rejection: the identical post becomes legal once evidence is
37
+ // attached to the draft (or the account's evidence-required toggle is turned
38
+ // off) — re-read and retry, don't fix the payload.
39
+ case 'invalid-state':
40
+ case 'nonzero-balance':
41
+ case 'reconciliation-exists':
42
+ case 'reconciliation-unbalanced':
43
+ case 'posting-not-reconcilable':
44
+ case 'reconciliation-too-large':
45
+ case 'period-closed':
46
+ case 'period-overlap':
47
+ case 'period-out-of-order':
48
+ case 'period-close-conflict':
49
+ case 'evidence-required':
50
+ return 409;
51
+ default:
52
+ return 400;
53
+ }
54
+ }
55
+ /**
56
+ * Typed ledger error. The server REJECTS invariant violations with these (never
57
+ * advises); the HTTP layer maps them to `{error, code}` bodies via
58
+ * {@link ledgerErrorStatus} and `HttpDataClient` re-materializes them, so a
59
+ * caller catches the same error class over either transport.
60
+ */
61
+ export class LedgerError extends Error {
62
+ constructor(code, message) {
63
+ super(message);
64
+ this.name = 'LedgerError';
65
+ this.code = code;
66
+ }
67
+ }
68
+ /** The set of valid codes, for wire-body → {@link LedgerError} re-materialization. */
69
+ export const LEDGER_ERROR_CODES = [
70
+ 'not-initialized',
71
+ 'not-found',
72
+ 'managed',
73
+ 'immutable',
74
+ 'unbalanced',
75
+ 'too-few-postings',
76
+ 'invalid-amount',
77
+ 'account-not-found',
78
+ 'account-closed',
79
+ 'currency-mismatch',
80
+ 'invalid-state',
81
+ 'nonzero-balance',
82
+ 'reconciled-locked',
83
+ 'reconciliation-exists',
84
+ 'reconciliation-unbalanced',
85
+ 'posting-not-reconcilable',
86
+ 'reconciliation-too-large',
87
+ 'period-closed',
88
+ 'period-overlap',
89
+ 'period-out-of-order',
90
+ 'period-close-conflict',
91
+ 'evidence-required',
92
+ 'invalid-input',
93
+ ];
94
+ // ── Domain enums ───────────────────────────────────────────────────────────────
95
+ export const LEDGER_ACCOUNT_TYPES = ['asset', 'liability', 'equity', 'revenue', 'expense'];
96
+ export const LEDGER_ACCOUNT_STATUSES = ['open', 'closed'];
97
+ export const LEDGER_TRANSACTION_STATES = ['draft', 'posted', 'void'];
98
+ export const LEDGER_CLEARED_STATES = ['pending', 'cleared', 'reconciled'];
99
+ /**
100
+ * A reconciliation's lifecycle (LGR-11 + LGR-22).
101
+ *
102
+ * `abandoned` is a TERMINAL status, not a deletion: a statement someone opened
103
+ * against the wrong closing balance still happened, and the record of it — with
104
+ * the audit events that amended and ended it — is what lets a later reader tell
105
+ * "this account was never reconciled" apart from "this attempt was given up on".
106
+ * It is invisible to the one-open-per-account rule (which asks only for `open`),
107
+ * so abandoning immediately frees the account for a fresh start.
108
+ */
109
+ export const LEDGER_RECONCILIATION_STATUSES = ['open', 'finished', 'abandoned'];
110
+ /**
111
+ * A period's lifecycle (LGR-12). `reopened` is TERMINAL for the RECORD, not for
112
+ * the range: reopening keeps the row (with the reversal that voided its closing
113
+ * entry) so the settings UI and the audit trail can show that a close happened
114
+ * and was undone; closing the range again writes a NEW period record. Only
115
+ * `closed` periods lock dates.
116
+ */
117
+ export const LEDGER_PERIOD_STATUSES = ['closed', 'reopened'];
118
+ /**
119
+ * The account types a period close sweeps into retained earnings (LGR-12): the
120
+ * income-statement ("flow") accounts. ONE list, exported, because the server's
121
+ * closing-entry generator and any report fold that classifies flow accounts
122
+ * must agree — this epic's recurring defect is two copies of one fact.
123
+ */
124
+ export const LEDGER_INCOME_STATEMENT_ACCOUNT_TYPES = ['revenue', 'expense'];
125
+ /** Whether `type` names an income-statement (flow) account. */
126
+ export function isIncomeStatementAccountType(type) {
127
+ return LEDGER_INCOME_STATEMENT_ACCOUNT_TYPES.includes(type);
128
+ }
129
+ // ── Read bounds ────────────────────────────────────────────────────────────────
130
+ /**
131
+ * The server's HARD CAP on one `listTransactions` page: a larger `limit` is
132
+ * clamped to this, silently.
133
+ *
134
+ * Exported because a reader that totals what it fetched — a report — cannot
135
+ * tell a full page from a complete book without it, and a duplicated literal
136
+ * would rot: if this cap ever dropped, a report asking for the old number would
137
+ * be clamped, conclude it had read everything, and render a PARTIAL total as a
138
+ * complete one. Both the server clamp and the ledger plugin's report read this
139
+ * one constant, and `ledger.test.ts` asserts the clamp actually holds.
140
+ */
141
+ export const LEDGER_MAX_TRANSACTION_LIMIT = 1000;
142
+ /** The page size `listTransactions` uses when the caller names none. */
143
+ export const LEDGER_DEFAULT_TRANSACTION_LIMIT = 500;
144
+ // ── Property ids (stable — the seeded database schemas key rows by these) ──────
145
+ /** Stable property ids for the seeded ledger database schemas. */
146
+ export const LEDGER_PROP = {
147
+ account: {
148
+ type: 'lp_type',
149
+ status: 'lp_status',
150
+ currency: 'lp_currency',
151
+ /** LGR-14: `true` on an account that refuses to accept a posting from an
152
+ * entry with no evidence attached; absent everywhere else (additive — no
153
+ * migration, pre-LGR-14 accounts read back `false`). */
154
+ evidenceRequired: 'lp_evidence_required',
155
+ },
156
+ transaction: {
157
+ date: 'lp_date',
158
+ description: 'lp_description',
159
+ state: 'lp_state',
160
+ postedAt: 'lp_posted_at',
161
+ postedBy: 'lp_posted_by',
162
+ reverses: 'lp_reverses',
163
+ evidence: 'lp_evidence',
164
+ entryNo: 'lp_entry_no',
165
+ /** LGR-12: `'closing'` on a period-close entry; absent on ordinary entries. */
166
+ kind: 'lp_kind',
167
+ },
168
+ posting: {
169
+ transaction: 'lp_transaction',
170
+ account: 'lp_account',
171
+ amount: 'lp_amount_minor',
172
+ cleared: 'lp_cleared',
173
+ reconciliation: 'lp_reconciliation',
174
+ memo: 'lp_memo',
175
+ },
176
+ reconciliation: {
177
+ account: 'lp_account',
178
+ statementDate: 'lp_statement_date',
179
+ statementBalance: 'lp_statement_balance_minor',
180
+ status: 'lp_status',
181
+ },
182
+ };
183
+ /**
184
+ * THE period predicate (LGR-12), shared by the store's post/reverse guards and
185
+ * every UI surface that wants to say WHY a date is refused: the `closed` period
186
+ * containing `date`, or `null`. Pure; string comparison is safe because both
187
+ * sides are ISO `YYYY-MM-DD`.
188
+ */
189
+ export function closedPeriodContaining(periods, date) {
190
+ for (const period of periods) {
191
+ if (period.status !== 'closed')
192
+ continue;
193
+ if (period.start <= date && date <= period.end)
194
+ return period;
195
+ }
196
+ return null;
197
+ }
198
+ /**
199
+ * Every CLOSED period overlapping the inclusive range `[from, to]` (`''` means
200
+ * open at that end) — the display-only "this report crosses a closed period"
201
+ * marker the report blocks render. Sorted by start date so the marker reads in
202
+ * calendar order.
203
+ */
204
+ export function closedPeriodsOverlapping(periods, from, to) {
205
+ return periods
206
+ .filter((p) => p.status === 'closed' && (to === '' || p.start <= to) && (from === '' || from <= p.end))
207
+ .sort((a, b) => (a.start < b.start ? -1 : a.start > b.start ? 1 : a.id < b.id ? -1 : 1));
208
+ }
209
+ /**
210
+ * Every known audit action — the validation set for a stored `action` value.
211
+ *
212
+ * `as const satisfies` (not a widened annotation) so the array's element type is
213
+ * the literal union: adding a member to {@link LedgerAuditAction} without adding
214
+ * it here is then caught by the exhaustiveness check below at COMPILE time,
215
+ * rather than surfacing at runtime as a log this build refuses to interpret.
216
+ */
217
+ export const LEDGER_AUDIT_ACTIONS = [
218
+ 'ledger.init',
219
+ 'ledger.acl',
220
+ 'account.create',
221
+ 'account.update',
222
+ 'transaction.create',
223
+ 'transaction.update',
224
+ 'transaction.delete',
225
+ 'transaction.post',
226
+ 'transaction.reverse',
227
+ 'posting.cleared',
228
+ 'reconciliation.start',
229
+ 'reconciliation.finish',
230
+ 'reconciliation.reopen',
231
+ 'reconciliation.amend',
232
+ 'reconciliation.abandon',
233
+ 'period.close',
234
+ 'period.reopen',
235
+ 'ledger.autoExportPath',
236
+ 'ledger.restore',
237
+ ];
238
+ /**
239
+ * Compile-time exhaustiveness: every {@link LedgerAuditAction} must appear in
240
+ * {@link LEDGER_AUDIT_ACTIONS}. If a new union member is added without listing
241
+ * it, this assignment fails to typecheck (the union is not assignable to the
242
+ * array's narrower element type).
243
+ */
244
+ const _LEDGER_AUDIT_ACTIONS_EXHAUSTIVE = null;
245
+ void _LEDGER_AUDIT_ACTIONS_EXHAUSTIVE;
246
+ /**
247
+ * Pure reducer: fold the audit event stream (ascending `seq`) into the expected
248
+ * current ledger state. Deterministic and side-effect free — the replay test
249
+ * compares this against the live store to prove the audit log is a complete
250
+ * record of every ledger mutation.
251
+ */
252
+ export function replayLedgerAudit(events) {
253
+ const state = { initialized: false, accounts: {}, transactions: {}, reconciliations: {}, periods: {} };
254
+ /** Apply one recorded posting change to the replayed transaction holding it. */
255
+ const applyPostingChange = (change) => {
256
+ const tx = state.transactions[change.transactionId];
257
+ if (!tx)
258
+ return;
259
+ state.transactions[change.transactionId] = {
260
+ ...tx,
261
+ postings: tx.postings.map((p) => p.id === change.postingId ? { ...p, cleared: change.cleared, reconciliationId: change.reconciliationId } : p),
262
+ };
263
+ };
264
+ for (const ev of events) {
265
+ // An action this reducer does not know about means the log records a
266
+ // mutation the replay cannot account for — so the reconstruction would be
267
+ // silently INCOMPLETE (state drifting from the books with no signal). That
268
+ // is exactly the failure a replayable audit log exists to rule out, so it is
269
+ // a hard error, not a skipped iteration. Any new action MUST land here.
270
+ const p = ev.payload;
271
+ switch (ev.action) {
272
+ case 'ledger.init':
273
+ state.initialized = true;
274
+ break;
275
+ case 'ledger.acl':
276
+ // A sharing grant/revoke on a ledger page: recorded for the trail, but it
277
+ // carries no ledger CONTENT, so replayed state is unchanged.
278
+ break;
279
+ case 'ledger.autoExportPath':
280
+ // An instance-policy change (where the canonical export is written):
281
+ // recorded for the trail, but it touches no ledger entity, so replayed
282
+ // state is unchanged. Explicit rather than falling through to `default`,
283
+ // which must keep throwing on actions this build genuinely cannot model.
284
+ break;
285
+ case 'ledger.restore':
286
+ // A bundle's history was installed (LGR-15): provenance for the trail —
287
+ // the restored entity events precede this one and already carry all the
288
+ // content, so replayed state is unchanged. Explicit for the same reason
289
+ // as `autoExportPath` above.
290
+ break;
291
+ case 'account.create':
292
+ case 'account.update':
293
+ if (p.account)
294
+ state.accounts[p.account.id] = p.account;
295
+ break;
296
+ case 'transaction.create':
297
+ case 'transaction.update':
298
+ case 'transaction.post':
299
+ if (p.transaction)
300
+ state.transactions[p.transaction.id] = p.transaction;
301
+ break;
302
+ case 'transaction.delete':
303
+ if (p.transactionId)
304
+ delete state.transactions[p.transactionId];
305
+ break;
306
+ case 'transaction.reverse':
307
+ if (p.transaction)
308
+ state.transactions[p.transaction.id] = p.transaction;
309
+ if (p.originalId && state.transactions[p.originalId]) {
310
+ state.transactions[p.originalId] = {
311
+ ...state.transactions[p.originalId],
312
+ state: p.originalState ?? 'void',
313
+ };
314
+ }
315
+ break;
316
+ case 'posting.cleared':
317
+ if (p.transactionId && p.postingId && p.cleared && state.transactions[p.transactionId]) {
318
+ const tx = state.transactions[p.transactionId];
319
+ state.transactions[p.transactionId] = {
320
+ ...tx,
321
+ postings: tx.postings.map((post) => (post.id === p.postingId ? { ...post, cleared: p.cleared } : post)),
322
+ };
323
+ }
324
+ break;
325
+ case 'reconciliation.start':
326
+ case 'reconciliation.amend':
327
+ case 'reconciliation.abandon':
328
+ // ROW-ONLY events. Neither an amend nor an abandon touches a posting —
329
+ // that is the LGR-22 guarantee, stated here as well as enforced in the
330
+ // store — so unlike finish/reopen below there is no posting change to
331
+ // apply, and a payload that carried one would be a bug in the writer.
332
+ if (p.reconciliation)
333
+ state.reconciliations[p.reconciliation.id] = p.reconciliation;
334
+ break;
335
+ case 'reconciliation.finish':
336
+ case 'reconciliation.reopen':
337
+ // ONE event, TWO effects: the reconciliation's new status, and the
338
+ // cleared/reconciliationId state of every posting it froze or unfroze.
339
+ // Both must land, or the replayed postings drift from the book and the
340
+ // verifier's content-hash comparison reports a clean ledger as tampered.
341
+ if (p.reconciliation)
342
+ state.reconciliations[p.reconciliation.id] = p.reconciliation;
343
+ for (const change of p.postings ?? [])
344
+ applyPostingChange(change);
345
+ break;
346
+ case 'period.close':
347
+ // ONE event, TWO effects (LGR-12): the period record, and the closing
348
+ // entry it posted (absent when the range held nothing to close).
349
+ if (p.period)
350
+ state.periods[p.period.id] = p.period;
351
+ if (p.transaction)
352
+ state.transactions[p.transaction.id] = p.transaction;
353
+ break;
354
+ case 'period.reopen':
355
+ // The reversal pair rides in the same shape `transaction.reverse` uses:
356
+ // the reversal lands, and the original closing entry flips to void.
357
+ if (p.period)
358
+ state.periods[p.period.id] = p.period;
359
+ if (p.transaction)
360
+ state.transactions[p.transaction.id] = p.transaction;
361
+ if (p.originalId && state.transactions[p.originalId]) {
362
+ state.transactions[p.originalId] = {
363
+ ...state.transactions[p.originalId],
364
+ state: p.originalState ?? 'void',
365
+ };
366
+ }
367
+ break;
368
+ default:
369
+ throw new LedgerError('invalid-state', `replayLedgerAudit: unknown audit action ${JSON.stringify(ev.action)} — the replay cannot reconstruct state it does not model`);
370
+ }
371
+ }
372
+ return state;
373
+ }
374
+ /**
375
+ * The tamper-evidence hash of ONE audit event: SHA-256 over its canonical
376
+ * content INCLUDING `prevHash`, which is what links each event to its
377
+ * predecessor. Async because it uses WebCrypto (isomorphic: Node, browser,
378
+ * sidecar). `seq` is deliberately part of the hashed content, so renumbering
379
+ * events breaks the chain just as rewriting one does.
380
+ */
381
+ export async function ledgerAuditEventHash(event) {
382
+ const canonical = canonicalLedgerJson({
383
+ seq: event.seq,
384
+ id: event.id,
385
+ actorSubject: event.actorSubject,
386
+ actorName: event.actorName,
387
+ action: event.action,
388
+ entityIds: event.entityIds,
389
+ payload: event.payload,
390
+ beforeHash: event.beforeHash,
391
+ afterHash: event.afterHash,
392
+ prevHash: event.prevHash,
393
+ createdAt: event.createdAt,
394
+ });
395
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonical));
396
+ return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
397
+ }
398
+ /**
399
+ * Verify the audit log's hash chain over `events` (ASCENDING `seq`, contiguous
400
+ * from the genesis event). Each event must carry its predecessor's
401
+ * {@link ledgerAuditEventHash} in `prevHash`; the first must carry `null`.
402
+ *
403
+ * THE PRECISE GUARANTEE — a green result is NOT "this log is authentic":
404
+ *
405
+ * - DETECTED: any edit, reordering, or middle deletion whose perpetrator does
406
+ * not also recompute every following link. That covers the realistic
407
+ * accident-and-opportunist cases (a stray `UPDATE`, a row deleted to hide one
408
+ * entry), and it distinguishes a deleted event from the innocuous BIGSERIAL
409
+ * gap a rolled-back transaction leaves.
410
+ * - NOT DETECTED: truncation of the HEAD or the TAIL — a prefix or suffix of
411
+ * the log removed wholesale still verifies clean, because nothing in the data
412
+ * states where the chain is supposed to start or end. Nor a WHOLESALE REWRITE:
413
+ * this hash is unkeyed and this function is exported, so an actor with
414
+ * database access can rewrite every event and recompute every link.
415
+ *
416
+ * Detecting those requires an off-box ANCHOR — periodically publishing the tail
417
+ * hash somewhere the database's owner cannot rewrite — which is tracked
418
+ * separately (LGR-18) and is not implemented here. Treat this as integrity
419
+ * against tampering that does not control the whole log, not as a signature.
420
+ *
421
+ * Verifying a PAGE of the log (not starting at genesis) is supported: pass the
422
+ * slice and it checks the links WITHIN it, reporting the first break.
423
+ */
424
+ export async function verifyLedgerAuditChain(events) {
425
+ let expectedPrev = undefined; // undefined = first seen
426
+ for (let i = 0; i < events.length; i += 1) {
427
+ const event = events[i];
428
+ if (i > 0 && event.seq <= events[i - 1].seq) {
429
+ return { ok: false, checked: i, brokenAtSeq: event.seq, reason: 'events are not in ascending seq order' };
430
+ }
431
+ if (expectedPrev !== undefined && event.prevHash !== expectedPrev) {
432
+ return {
433
+ ok: false,
434
+ checked: i,
435
+ brokenAtSeq: event.seq,
436
+ reason: `prevHash mismatch: expected ${String(expectedPrev)}, got ${String(event.prevHash)} — an event was rewritten, reordered, or removed`,
437
+ };
438
+ }
439
+ expectedPrev = await ledgerAuditEventHash(event);
440
+ }
441
+ return { ok: true, checked: events.length, brokenAtSeq: null, reason: null };
442
+ }
443
+ /**
444
+ * Canonical JSON of an entity for audit content-hashing: keys sorted at every
445
+ * depth, so a byte-identical serialization is independent of insertion order.
446
+ */
447
+ export function canonicalLedgerJson(value) {
448
+ const sortValue = (v) => {
449
+ if (Array.isArray(v))
450
+ return v.map(sortValue);
451
+ if (v !== null && typeof v === 'object') {
452
+ const out = {};
453
+ for (const key of Object.keys(v).sort()) {
454
+ out[key] = sortValue(v[key]);
455
+ }
456
+ return out;
457
+ }
458
+ return v;
459
+ };
460
+ return JSON.stringify(sortValue(value));
461
+ }
462
+ /**
463
+ * Validate a hierarchical, colon-delimited account name (`Assets:Bank:Checking`):
464
+ * one or more non-empty, non-whitespace-only segments separated by single colons.
465
+ */
466
+ export function isValidLedgerAccountName(name) {
467
+ if (typeof name !== 'string' || name.trim() === '')
468
+ return false;
469
+ return name.split(':').every((seg) => seg.trim().length > 0);
470
+ }
471
+ /** Validate an ISO `YYYY-MM-DD` date string (a real calendar date). */
472
+ export function isValidLedgerDate(date) {
473
+ if (typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date))
474
+ return false;
475
+ const parsed = new Date(`${date}T00:00:00Z`);
476
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === date;
477
+ }
478
+ /**
479
+ * The ADVISORY band of the finding-code union (LGR-14 Q3): codes that report
480
+ * "the book does not satisfy today's POLICY", never "storage was tampered
481
+ * with". Every other code is the tamper band. Keyed off the code union — not
482
+ * message text — because exit policies and alerting hang off this distinction:
483
+ * `--verify-ledger` exits 0 on an advisory-only report (still printed), so a
484
+ * healthy book that enables evidence-required over bare history does not turn
485
+ * every backup-verification script permanently red and teach operators to
486
+ * ignore the one alarm that matters. Any future advisory code MUST land here.
487
+ */
488
+ export const LEDGER_VERIFY_ADVISORY_CODES = ['evidence-required-missing'];
489
+ /** Whether `code` is an advisory (current-policy) finding, not a tamper one. */
490
+ export function isLedgerVerifyAdvisory(code) {
491
+ return LEDGER_VERIFY_ADVISORY_CODES.includes(code);
492
+ }
493
+ //# sourceMappingURL=ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.js","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Beancount export (LGR-13): the ledger serialized as a Beancount journal, so
3
+ * the whole book can be re-checked by an INDEPENDENT accounting implementation
4
+ * (`bean-check`, the beancount loader, Fava) — free QA for every invariant the
5
+ * ledger claims to enforce.
6
+ *
7
+ * This module is a PURE function from ledger entities to journal text — no
8
+ * I/O, no clock, no locale — with the same hard guarantee as the canonical CSV
9
+ * (`./ledgerCsv`): the SAME DATA always produces IDENTICAL BYTES. It reads the
10
+ * same entities the CSV export reads (one read model, two serializers).
11
+ *
12
+ * WHAT IS EMITTED (and why — the full contract lives in
13
+ * `docs/ledger/beancount-export.md`):
14
+ *
15
+ * - `open` for every account (earliest reported posting date, else the
16
+ * account's creation date), pinning the account's currency so bean-check
17
+ * enforces the ledger's own one-currency-per-account rule independently;
18
+ * - one `txn` per POSTED or VOID transaction — the exact set the report folds
19
+ * count (`REPORTED_STATES` in the ledger plugin's reports): a void original
20
+ * is offset by its posted reversal, and exporting only one half of the pair
21
+ * would misstate every balance. Drafts are not on the books and never
22
+ * export. Ledger-only facts (id, entry number, state, kind, reverses,
23
+ * evidence, memo, cleared state) ride as `lp-*` metadata;
24
+ * - `balance` assertions the day after each CLOSED period's end, for every
25
+ * account with a reported posting on or before the end — so bean-check
26
+ * re-verifies both the arithmetic and the closing sweep (income-statement
27
+ * accounts assert to zero) from the directives alone.
28
+ *
29
+ * SIGN MAPPING — the identity. The ledger stores signed DEBIT-POSITIVE minor
30
+ * units (LGR-2); Beancount amounts are signed decimals where an asset increase
31
+ * is positive and a credit-normal balance (Income/Liabilities/Equity) is
32
+ * negative — the same convention. No re-signing happens anywhere, including
33
+ * for contra/credit-normal accounts: a revenue balance exports negative, which
34
+ * is exactly how Beancount's own Income accounts carry it.
35
+ *
36
+ * MONEY DISCIPLINE: amounts pass through {@link formatBeancountAmount} (exact
37
+ * BigInt digit math, the `formatAmount` discipline without display grouping)
38
+ * and {@link sumAmounts} — never float arithmetic.
39
+ *
40
+ * CORRUPT BOOKS: unlike the insurance CSV (which must always leave the
41
+ * building), this export REFUSES a damaged book — an unresolvable account or a
42
+ * non-safe-integer amount throws a typed error instead of serializing a
43
+ * plausible-but-wrong journal. The reference implementation must never be
44
+ * handed data the ledger itself cannot vouch for; the LGR-7 verifier is the
45
+ * tool that names the damage.
46
+ */
47
+ import { type LedgerAccount, type LedgerAccountType, type LedgerPeriod, type LedgerTransaction } from './ledger';
48
+ /**
49
+ * The Beancount ROOT for each ledger account type. Beancount requires every
50
+ * account to live under one of exactly five roots; the ledger's five types map
51
+ * onto them 1:1 (`revenue` → `Income` is the only rename).
52
+ */
53
+ export declare const BEANCOUNT_ROOT_BY_TYPE: Readonly<Record<LedgerAccountType, string>>;
54
+ /**
55
+ * Serialize signed integer minor units as Beancount's plain decimal
56
+ * (`-1234.56`, `0.00`) — {@link formatAmount}'s exact BigInt digit math minus
57
+ * the display affordances (no thousands grouping, no symbols, no parens),
58
+ * because Beancount wants a machine number. Negative zero normalises to
59
+ * `0.00`. Throws {@link MoneyRangeError} on anything that is not a safe
60
+ * integer — see the module doc's corrupt-books stance.
61
+ */
62
+ export declare function formatBeancountAmount(minor: number): string;
63
+ /**
64
+ * Quote a string for Beancount: backslash and double-quote are escaped (the
65
+ * two characters the lexer treats specially inside a string); everything else
66
+ * — including literal newlines and non-ASCII — is verbatim, which the lexer
67
+ * accepts (verified against beancount 3.1.0). Escaping only these two keeps
68
+ * the mapping injective: a re-importer that unescapes `\\` and `\"` recovers
69
+ * the original exactly.
70
+ */
71
+ export declare function quoteBeancountString(value: string): string;
72
+ /**
73
+ * Mangle ONE colon-separated component of a ledger account name into
74
+ * Beancount's component charset, deterministically:
75
+ *
76
+ * 1. every character outside `[A-Za-z0-9-]` becomes `-` (one `-` per
77
+ * character — no collapsing, so distinct inputs stay distinct more often);
78
+ * 2. a leading lowercase letter is uppercased; any other invalid lead
79
+ * (digit-ok, letter-ok; a `-` or nothing) is prefixed with `X`.
80
+ *
81
+ * The ledger's own name validator guarantees non-empty, non-whitespace-only
82
+ * components, but raw storage is not trusted: an empty component mangles to
83
+ * `X` instead of producing an invalid name.
84
+ */
85
+ export declare function mangleBeancountComponent(segment: string): string;
86
+ /**
87
+ * Map ONE ledger account name onto a Beancount account name (before collision
88
+ * handling — see {@link buildBeancountAccountNames} for the full map):
89
+ *
90
+ * - the ROOT comes from the account TYPE ({@link BEANCOUNT_ROOT_BY_TYPE}),
91
+ * never from the name — `Revenue:Sales` typed `revenue` becomes
92
+ * `Income:Revenue:Sales`;
93
+ * - each component is mangled ({@link mangleBeancountComponent});
94
+ * - a first component that already equals the root is not repeated
95
+ * (`Assets:Bank` stays `Assets:Bank`, not `Assets:Assets:Bank`) — unless
96
+ * dropping it would leave the bare root, which Beancount rejects (an
97
+ * account named exactly `Assets` becomes `Assets:Assets`).
98
+ */
99
+ export declare function beancountAccountName(account: Pick<LedgerAccount, 'name' | 'type'>): string;
100
+ /**
101
+ * The full account-id → Beancount-name map, with DETERMINISTIC collision
102
+ * handling: accounts are visited in (createdAt, id) order; the first claimant
103
+ * keeps the mapped name, later ones append `-2`, `-3`, … to the final
104
+ * component (bumping until free). Stable for a given book — the suffix order
105
+ * depends only on stored creation data, never on read order.
106
+ */
107
+ export declare function buildBeancountAccountNames(accounts: readonly LedgerAccount[]): Map<string, string>;
108
+ /**
109
+ * Build the Beancount journal from the full account list, the full
110
+ * transaction list (with postings) and the period records — the same read
111
+ * model the canonical CSV uses. Pure and byte-deterministic; see module doc.
112
+ *
113
+ * Throws {@link LedgerError} `account-not-found` when a posting references an
114
+ * account the list does not carry, and {@link MoneyRangeError} on a stored
115
+ * amount that is not a safe integer — the corrupt-books stance.
116
+ */
117
+ export declare function buildLedgerBeancount(accounts: readonly LedgerAccount[], transactions: readonly LedgerTransaction[], periods: readonly LedgerPeriod[]): string;