@mulmoclaude/accounting-plugin 2.0.0 → 2.2.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 (50) hide show
  1. package/README.md +37 -0
  2. package/dist/server/context.d.ts +45 -3
  3. package/dist/server/context.d.ts.map +1 -1
  4. package/dist/server/eventPublisher.d.ts +9 -4
  5. package/dist/server/eventPublisher.d.ts.map +1 -1
  6. package/dist/server/index.d.ts +1 -0
  7. package/dist/server/index.d.ts.map +1 -1
  8. package/dist/server/router.d.ts +27 -3
  9. package/dist/server/router.d.ts.map +1 -1
  10. package/dist/server/snapshotCache.d.ts +3 -3
  11. package/dist/server/snapshotCache.d.ts.map +1 -1
  12. package/dist/server.cjs +152 -82
  13. package/dist/server.cjs.map +1 -1
  14. package/dist/server.js +151 -81
  15. package/dist/server.js.map +1 -1
  16. package/dist/shared/api.d.ts +12 -0
  17. package/dist/shared/api.d.ts.map +1 -1
  18. package/dist/shared/channels.d.ts +22 -2
  19. package/dist/shared/channels.d.ts.map +1 -1
  20. package/dist/{shared-BZAAF-I4.js → shared-BccLo0Ux.js} +39 -5
  21. package/dist/shared-BccLo0Ux.js.map +1 -0
  22. package/dist/{shared-BpFVwa6Y.cjs → shared-Dgt-i23T.cjs} +50 -4
  23. package/dist/shared-Dgt-i23T.cjs.map +1 -0
  24. package/dist/shared.cjs +3 -1
  25. package/dist/shared.js +2 -2
  26. package/dist/style.css +19 -10
  27. package/dist/vue/View.vue.d.ts +5 -0
  28. package/dist/vue/View.vue.d.ts.map +1 -1
  29. package/dist/vue/api.d.ts +132 -100
  30. package/dist/vue/api.d.ts.map +1 -1
  31. package/dist/vue/components/AccountsModal.vue.d.ts.map +1 -1
  32. package/dist/vue/components/BalanceSheet.vue.d.ts.map +1 -1
  33. package/dist/vue/components/BookSettings.vue.d.ts.map +1 -1
  34. package/dist/vue/components/JournalEntryForm.vue.d.ts.map +1 -1
  35. package/dist/vue/components/JournalList.vue.d.ts.map +1 -1
  36. package/dist/vue/components/Ledger.vue.d.ts.map +1 -1
  37. package/dist/vue/components/NewBookForm.vue.d.ts.map +1 -1
  38. package/dist/vue/components/OpeningBalancesForm.vue.d.ts.map +1 -1
  39. package/dist/vue/components/ProfitLoss.vue.d.ts.map +1 -1
  40. package/dist/vue/hostContext.d.ts +15 -0
  41. package/dist/vue/hostContext.d.ts.map +1 -1
  42. package/dist/vue/useAccountingChannel.d.ts +13 -3
  43. package/dist/vue/useAccountingChannel.d.ts.map +1 -1
  44. package/dist/vue.cjs +229 -99
  45. package/dist/vue.cjs.map +1 -1
  46. package/dist/vue.js +230 -100
  47. package/dist/vue.js.map +1 -1
  48. package/package.json +5 -5
  49. package/dist/shared-BZAAF-I4.js.map +0 -1
  50. package/dist/shared-BpFVwa6Y.cjs.map +0 -1
package/dist/server.cjs CHANGED
@@ -16,12 +16,12 @@ var __copyProps = (to, from, except, desc) => {
16
16
  }
17
17
  return to;
18
18
  };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
20
  value: mod,
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- const require_shared = require("./shared-BpFVwa6Y.cjs");
24
+ const require_shared = require("./shared-Dgt-i23T.cjs");
25
25
  let express = require("express");
26
26
  let node_crypto = require("node:crypto");
27
27
  let node_fs = require("node:fs");
@@ -84,11 +84,41 @@ function configureAccountingServer(context) {
84
84
  }
85
85
  /** Default workspace root for io calls that don't pass one explicitly.
86
86
  * Throws if the host never configured the server — a real wiring bug
87
- * (unit tests always pass an explicit root, so they never hit this). */
87
+ * (unit tests always pass an explicit root, so they never hit this)
88
+ * and also under STRICT mode (`workspaceRoot: null`), where there is no
89
+ * ambient root to fall back to and a missing option is a bug rather
90
+ * than a default. */
88
91
  function defaultWorkspaceRoot() {
89
92
  if (!deps) throw new Error("@mulmoclaude/accounting-plugin: configureAccountingServer() must be called before serving accounting requests");
93
+ if (deps.workspaceRoot === null) throw new Error("@mulmoclaude/accounting-plugin: this host is configured with workspaceRoot: null (explicit-root mode), so every call must pass a workspaceRoot. A call reached the engine without one.");
90
94
  return deps.workspaceRoot;
91
95
  }
96
+ /** The opaque channel scope for a root, or `null` when the host declared
97
+ * none (single-root hosts, and a multi-root host's default root). Used
98
+ * by the event publisher; never contains a path.
99
+ *
100
+ * A missing root throws under explicit-root mode, exactly as
101
+ * `defaultWorkspaceRoot()` does. Returning `null` there would be worse
102
+ * than the io-path bug it mirrors: the event would go out on the
103
+ * UNSCOPED channel name, which every project's default-scope
104
+ * subscriber receives. The publisher catches it and logs, so a dropped
105
+ * root costs one event and a warning rather than a silent cross-project
106
+ * notification. */
107
+ function channelScopeFor(workspaceRoot) {
108
+ if (!deps?.channelScopeForRoot) return null;
109
+ return deps.channelScopeForRoot(workspaceRoot ?? defaultWorkspaceRoot());
110
+ }
111
+ /** Whether the host declared project scoping at all.
112
+ *
113
+ * The difference matters where a card records the project it belongs
114
+ * to: on a scoped host, "no scope" is a REAL answer (the default root)
115
+ * and must be recorded as `null`, not left out — an omitted field means
116
+ * "ask the host what is active now", which is precisely the drift a
117
+ * pinned card exists to prevent. On an unscoped host the field is left
118
+ * out entirely and every payload stays byte-identical to today's. */
119
+ function isProjectScopedHost() {
120
+ return Boolean(deps?.channelScopeForRoot);
121
+ }
92
122
  var consoleLogger = {
93
123
  error: (namespace, msg, data) => console.error(`[${namespace}] ${msg}`, data ?? ""),
94
124
  warn: (namespace, msg, data) => console.warn(`[${namespace}] ${msg}`, data ?? ""),
@@ -1165,9 +1195,16 @@ var pubsub = null;
1165
1195
  function initAccountingEventPublisher(instance) {
1166
1196
  pubsub = instance;
1167
1197
  }
1168
- function safePublish(channel, payload) {
1198
+ /** `channelFor` is called INSIDE the guard, not before it: resolving the
1199
+ * scope can throw (a write that reached the publisher with no root
1200
+ * under explicit-root mode), and a lost event with a warning beats
1201
+ * either crashing the write or publishing to the unscoped channel that
1202
+ * every project can hear. */
1203
+ function safePublish(channelFor, payload) {
1169
1204
  if (!pubsub) return;
1205
+ let channel = "(unresolved)";
1170
1206
  try {
1207
+ channel = channelFor();
1171
1208
  pubsub.publish(channel, payload);
1172
1209
  } catch (err) {
1173
1210
  log.warn("accounting", "publish failed; subscribers will miss this event", {
@@ -1178,15 +1215,20 @@ function safePublish(channel, payload) {
1178
1215
  }
1179
1216
  /** Per-book change notification. `period` should be the entry's
1180
1217
  * YYYY-MM bucket (or the earliest invalidated month for snapshot
1181
- * events). */
1182
- function publishBookChange(bookId, payload) {
1183
- safePublish(require_shared.bookChannel(bookId), payload);
1218
+ * events).
1219
+ *
1220
+ * `workspaceRoot` is the root the write happened under; the channel
1221
+ * name is namespaced by the host's opaque scope for it (see
1222
+ * `channelScopeFor`). Omit it — as a single-root host's service calls
1223
+ * do — and the name is what it has always been. */
1224
+ function publishBookChange(bookId, payload, workspaceRoot) {
1225
+ safePublish(() => require_shared.bookChannel(bookId, channelScopeFor(workspaceRoot)), payload);
1184
1226
  }
1185
1227
  /** Fired when the *list* of books changes (createBook, deleteBook).
1186
1228
  * Payload is intentionally empty — subscribers refetch from
1187
- * /api/accounting. */
1188
- function publishBooksChanged() {
1189
- safePublish(require_shared.ACCOUNTING_BOOKS_CHANNEL, {});
1229
+ * /api/accounting. Scoped by root like `publishBookChange`. */
1230
+ function publishBooksChanged(workspaceRoot) {
1231
+ safePublish(() => require_shared.booksChannel(channelScopeFor(workspaceRoot)), {});
1190
1232
  }
1191
1233
  //#endregion
1192
1234
  //#region src/server/snapshotCache.ts
@@ -1255,18 +1297,39 @@ async function rebuildAllSnapshots(bookId, workspaceRoot) {
1255
1297
  return { rebuilt: periods };
1256
1298
  }
1257
1299
  var rebuildQueues = /* @__PURE__ */ new Map();
1300
+ /** Canonical key for the root a rebuild belongs to. An absent root
1301
+ * resolves to the host's configured one, so a call that passes it
1302
+ * explicitly and one that relies on the default share a queue. Under
1303
+ * explicit-root mode there is no default and `defaultWorkspaceRoot()`
1304
+ * throws — deliberately loud: a `cancelRebuild` / `awaitRebuildIdle`
1305
+ * that quietly keyed a queue nobody scheduled would return while the
1306
+ * real rebuild is still writing, which is how `deleteBook` races a
1307
+ * `writeSnapshot` back into existence.
1308
+ *
1309
+ * Lexical resolution only — symlinks are deliberately NOT resolved,
1310
+ * matching `canonicalRoot` in `@mulmoclaude/core`'s collection engine,
1311
+ * so one project has one identity across both packages. A host that
1312
+ * hands the engine a real path in one call and a symlink alias in the
1313
+ * next would split the queue; the fix belongs in the host's own root
1314
+ * resolution, which is where the two packages agree it lives. */
1315
+ function rootKey(workspaceRoot) {
1316
+ return node_path.default.resolve(workspaceRoot ?? defaultWorkspaceRoot());
1317
+ }
1318
+ function queueKey(bookId, workspaceRoot) {
1319
+ return `${rootKey(workspaceRoot)}\u0000${bookId}`;
1320
+ }
1258
1321
  function minPeriod(lhs, rhs) {
1259
1322
  if (lhs === null) return rhs;
1260
1323
  return lhs < rhs ? lhs : rhs;
1261
1324
  }
1262
- function isInvalidatedDuringRebuild(bookId, period) {
1263
- const queue = rebuildQueues.get(bookId);
1325
+ function isInvalidatedDuringRebuild(key, period) {
1326
+ const queue = rebuildQueues.get(key);
1264
1327
  return queue !== void 0 && queue.pendingFromPeriod !== null && period >= queue.pendingFromPeriod;
1265
1328
  }
1266
- function isCancelled(bookId) {
1267
- return rebuildQueues.get(bookId)?.cancelled === true;
1329
+ function isCancelled(key) {
1330
+ return rebuildQueues.get(key)?.cancelled === true;
1268
1331
  }
1269
- async function runRebuild(bookId, fromPeriod, workspaceRoot) {
1332
+ async function runRebuild(key, bookId, fromPeriod, workspaceRoot) {
1270
1333
  const startedAt = Date.now();
1271
1334
  log.info("accounting", "snapshot rebuild started", {
1272
1335
  bookId,
@@ -1275,25 +1338,25 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
1275
1338
  publishBookChange(bookId, {
1276
1339
  kind: require_shared.BOOK_EVENT_KINDS.snapshotsRebuilding,
1277
1340
  period: fromPeriod
1278
- });
1341
+ }, workspaceRoot);
1279
1342
  const targets = (await listJournalPeriods(bookId, workspaceRoot)).filter((monthKey) => monthKey >= fromPeriod);
1280
1343
  let written = 0;
1281
1344
  for (const monthKey of targets) {
1282
- if (isCancelled(bookId)) break;
1283
- if (isInvalidatedDuringRebuild(bookId, monthKey)) break;
1345
+ if (isCancelled(key)) break;
1346
+ if (isInvalidatedDuringRebuild(key, monthKey)) break;
1284
1347
  const balances = await balancesAtEndOf(bookId, monthKey, workspaceRoot);
1285
- if (isCancelled(bookId)) break;
1286
- if (isInvalidatedDuringRebuild(bookId, monthKey)) break;
1348
+ if (isCancelled(key)) break;
1349
+ if (isInvalidatedDuringRebuild(key, monthKey)) break;
1287
1350
  await writeSnapshot(bookId, {
1288
1351
  period: monthKey,
1289
1352
  balances,
1290
1353
  builtAt: (/* @__PURE__ */ new Date()).toISOString()
1291
1354
  }, workspaceRoot);
1292
- if (isCancelled(bookId)) {
1355
+ if (isCancelled(key)) {
1293
1356
  await invalidateSnapshotsFrom(bookId, monthKey, workspaceRoot);
1294
1357
  break;
1295
1358
  }
1296
- if (isInvalidatedDuringRebuild(bookId, monthKey)) {
1359
+ if (isInvalidatedDuringRebuild(key, monthKey)) {
1297
1360
  await invalidateSnapshotsFrom(bookId, monthKey, workspaceRoot);
1298
1361
  break;
1299
1362
  }
@@ -1301,7 +1364,7 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
1301
1364
  publishBookChange(bookId, {
1302
1365
  kind: require_shared.BOOK_EVENT_KINDS.snapshotsReady,
1303
1366
  period: monthKey
1304
- });
1367
+ }, workspaceRoot);
1305
1368
  }
1306
1369
  log.info("accounting", "snapshot rebuild done", {
1307
1370
  bookId,
@@ -1309,7 +1372,7 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
1309
1372
  durationMs: Date.now() - startedAt
1310
1373
  });
1311
1374
  }
1312
- function startRebuild(bookId, fromPeriod, workspaceRoot) {
1375
+ function startRebuild(key, bookId, fromPeriod, workspaceRoot) {
1313
1376
  const entry = {
1314
1377
  running: Promise.resolve(),
1315
1378
  pendingFromPeriod: null,
@@ -1318,27 +1381,27 @@ function startRebuild(bookId, fromPeriod, workspaceRoot) {
1318
1381
  runningFromPeriod: fromPeriod,
1319
1382
  cancelled: false
1320
1383
  };
1321
- entry.running = runRebuild(bookId, fromPeriod, workspaceRoot).catch((err) => {
1384
+ entry.running = runRebuild(key, bookId, fromPeriod, workspaceRoot).catch((err) => {
1322
1385
  log.error("accounting", "snapshot rebuild failed", {
1323
1386
  bookId,
1324
1387
  fromPeriod,
1325
1388
  error: require_shared.errorMessage(err)
1326
1389
  });
1327
1390
  }).then(() => {
1328
- const current = rebuildQueues.get(bookId);
1391
+ const current = rebuildQueues.get(key);
1329
1392
  if (!current) return;
1330
1393
  if (current.cancelled) {
1331
- rebuildQueues.delete(bookId);
1394
+ rebuildQueues.delete(key);
1332
1395
  return;
1333
1396
  }
1334
1397
  if (current.pendingFromPeriod !== null) {
1335
1398
  const nextFrom = current.pendingFromPeriod;
1336
1399
  const nextRoot = current.pendingWorkspaceRoot;
1337
1400
  const carriedCount = current.coalescedWriteCount;
1338
- const successor = startRebuild(bookId, nextFrom, nextRoot);
1401
+ const successor = startRebuild(key, bookId, nextFrom, nextRoot);
1339
1402
  successor.coalescedWriteCount += carriedCount;
1340
- rebuildQueues.set(bookId, successor);
1341
- } else rebuildQueues.delete(bookId);
1403
+ rebuildQueues.set(key, successor);
1404
+ } else rebuildQueues.delete(key);
1342
1405
  });
1343
1406
  return entry;
1344
1407
  }
@@ -1347,9 +1410,10 @@ function startRebuild(bookId, fromPeriod, workspaceRoot) {
1347
1410
  * follow-up rebuild that covers the minimum `fromPeriod` seen.
1348
1411
  * Returns immediately — the rebuild runs on its own promise chain. */
1349
1412
  function scheduleRebuild(bookId, fromPeriod, workspaceRoot) {
1350
- const existing = rebuildQueues.get(bookId);
1413
+ const key = queueKey(bookId, workspaceRoot);
1414
+ const existing = rebuildQueues.get(key);
1351
1415
  if (!existing) {
1352
- rebuildQueues.set(bookId, startRebuild(bookId, fromPeriod, workspaceRoot));
1416
+ rebuildQueues.set(key, startRebuild(key, bookId, fromPeriod, workspaceRoot));
1353
1417
  return;
1354
1418
  }
1355
1419
  existing.pendingFromPeriod = minPeriod(existing.pendingFromPeriod, fromPeriod);
@@ -1360,9 +1424,10 @@ function scheduleRebuild(bookId, fromPeriod, workspaceRoot) {
1360
1424
  * `bookId`. Also called by `deleteBook` after `cancelRebuild` to
1361
1425
  * ensure a previously running rebuild has fully stopped before the
1362
1426
  * caller removes the book's directory on disk. */
1363
- async function awaitRebuildIdle(bookId) {
1364
- while (rebuildQueues.has(bookId)) {
1365
- const entry = rebuildQueues.get(bookId);
1427
+ async function awaitRebuildIdle(bookId, workspaceRoot) {
1428
+ const key = queueKey(bookId, workspaceRoot);
1429
+ while (rebuildQueues.has(key)) {
1430
+ const entry = rebuildQueues.get(key);
1366
1431
  if (!entry) return;
1367
1432
  await entry.running;
1368
1433
  }
@@ -1372,8 +1437,8 @@ async function awaitRebuildIdle(bookId) {
1372
1437
  * `removeBookDir` cannot race with a `writeSnapshot` that would
1373
1438
  * re-create the directory tree. Pair with `awaitRebuildIdle(bookId)`
1374
1439
  * to wait for the in-flight rebuild to finish bailing. */
1375
- function cancelRebuild(bookId) {
1376
- const entry = rebuildQueues.get(bookId);
1440
+ function cancelRebuild(bookId, workspaceRoot) {
1441
+ const entry = rebuildQueues.get(queueKey(bookId, workspaceRoot));
1377
1442
  if (!entry) return;
1378
1443
  entry.cancelled = true;
1379
1444
  entry.pendingFromPeriod = null;
@@ -1727,7 +1792,7 @@ async function createBook(input, workspaceRoot) {
1727
1792
  await ensureBookDir(bookId, workspaceRoot);
1728
1793
  await writeAccounts(bookId, [...DEFAULT_ACCOUNTS], workspaceRoot);
1729
1794
  await writeConfig({ books: [...config.books, book] }, workspaceRoot);
1730
- publishBooksChanged();
1795
+ publishBooksChanged(workspaceRoot);
1731
1796
  return { book };
1732
1797
  }
1733
1798
  async function updateBook(input, workspaceRoot) {
@@ -1744,7 +1809,7 @@ async function updateBook(input, workspaceRoot) {
1744
1809
  };
1745
1810
  if (country === "") delete next.country;
1746
1811
  await writeConfig({ books: config.books.map((book) => book.id === input.bookId ? next : book) }, workspaceRoot);
1747
- publishBooksChanged();
1812
+ publishBooksChanged(workspaceRoot);
1748
1813
  return { book: next };
1749
1814
  }
1750
1815
  async function deleteBook(input, workspaceRoot) {
@@ -1752,11 +1817,11 @@ async function deleteBook(input, workspaceRoot) {
1752
1817
  const config = await loadOrInitConfig(workspaceRoot);
1753
1818
  const target = findBook(config, input.bookId);
1754
1819
  if (!target) throw new AccountingError(404, `book ${JSON.stringify(input.bookId)} not found`);
1755
- cancelRebuild(input.bookId);
1756
- await awaitRebuildIdle(input.bookId);
1820
+ cancelRebuild(input.bookId, workspaceRoot);
1821
+ await awaitRebuildIdle(input.bookId, workspaceRoot);
1757
1822
  await removeBookDir(input.bookId, workspaceRoot);
1758
1823
  await writeConfig({ books: config.books.filter((book) => book.id !== input.bookId) }, workspaceRoot);
1759
- publishBooksChanged();
1824
+ publishBooksChanged(workspaceRoot);
1760
1825
  return {
1761
1826
  deletedBookId: input.bookId,
1762
1827
  deletedBookName: target.name
@@ -1806,7 +1871,7 @@ async function upsertAccount(input, workspaceRoot) {
1806
1871
  scheduleRebuild(bookId, "0000-00", workspaceRoot);
1807
1872
  await invalidateAllSnapshots(bookId, workspaceRoot);
1808
1873
  }
1809
- publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.accounts });
1874
+ publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.accounts }, workspaceRoot);
1810
1875
  return {
1811
1876
  bookId,
1812
1877
  account,
@@ -1855,7 +1920,7 @@ async function addEntries(input, workspaceRoot) {
1855
1920
  publishBookChange(bookId, {
1856
1921
  kind: require_shared.BOOK_EVENT_KINDS.journal,
1857
1922
  period: earliestPeriod
1858
- });
1923
+ }, workspaceRoot);
1859
1924
  return {
1860
1925
  bookId,
1861
1926
  entries: built
@@ -1884,7 +1949,7 @@ async function voidEntry(input, workspaceRoot) {
1884
1949
  publishBookChange(bookId, {
1885
1950
  kind: require_shared.BOOK_EVENT_KINDS.journal,
1886
1951
  period: fromPeriod
1887
- });
1952
+ }, workspaceRoot);
1888
1953
  return {
1889
1954
  bookId,
1890
1955
  reverseEntry: reverse,
@@ -1948,7 +2013,7 @@ async function setOpeningBalances(input, workspaceRoot) {
1948
2013
  await appendJournal(bookId, opening, workspaceRoot);
1949
2014
  scheduleRebuild(bookId, "0000-00", workspaceRoot);
1950
2015
  await invalidateAllSnapshots(bookId, workspaceRoot);
1951
- publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.opening });
2016
+ publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.opening }, workspaceRoot);
1952
2017
  return {
1953
2018
  bookId,
1954
2019
  openingEntry: opening,
@@ -2046,9 +2111,10 @@ function validateTimeSeriesInput(input) {
2046
2111
  async function loadTimeSeriesBookContext(requestedBookId, workspaceRoot) {
2047
2112
  const config = await loadOrInitConfig(workspaceRoot);
2048
2113
  const bookId = resolveBookId(config, requestedBookId);
2114
+ const book = findBook(config, bookId);
2049
2115
  return {
2050
2116
  bookId,
2051
- fiscalYearEnd: require_shared.resolveFiscalYearEnd(findBook(config, bookId)?.fiscalYearEnd),
2117
+ fiscalYearEnd: require_shared.resolveFiscalYearEnd(book?.fiscalYearEnd),
2052
2118
  accounts: await readAccounts(bookId, workspaceRoot)
2053
2119
  };
2054
2120
  }
@@ -2082,7 +2148,7 @@ async function getTimeSeriesReport(input, workspaceRoot) {
2082
2148
  async function rebuildSnapshots(input, workspaceRoot) {
2083
2149
  const bookId = resolveBookId(await loadOrInitConfig(workspaceRoot), input.bookId);
2084
2150
  const result = await rebuildAllSnapshots(bookId, workspaceRoot);
2085
- publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.snapshotsReady });
2151
+ publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.snapshotsReady }, workspaceRoot);
2086
2152
  return {
2087
2153
  bookId,
2088
2154
  rebuilt: result.rebuilt
@@ -2109,19 +2175,21 @@ function asyncHandler(namespace, fallbackMessage, handler) {
2109
2175
  }
2110
2176
  //#endregion
2111
2177
  //#region src/server/router.ts
2112
- async function handleOpenBook(rest) {
2178
+ async function handleOpenBook(rest, root) {
2113
2179
  if (typeof rest.bookId !== "string" || rest.bookId === "") throw new AccountingError(400, "openBook: bookId is required. Call 'getBooks' to enumerate, or 'createBook' first on a fresh workspace.");
2114
- const list = await listBooks();
2180
+ const list = await listBooks(root);
2115
2181
  if (!list.books.some((book) => book.id === rest.bookId)) throw new AccountingError(404, `openBook: book ${JSON.stringify(rest.bookId)} not found`);
2116
2182
  const initialTab = typeof rest.initialTab === "string" ? rest.initialTab : void 0;
2183
+ const scopeField = isProjectScopedHost() ? { scope: channelScopeFor(root) } : {};
2117
2184
  return {
2118
2185
  kind: "accounting-app",
2119
2186
  bookId: rest.bookId,
2120
2187
  initialTab,
2121
- books: list.books
2188
+ books: list.books,
2189
+ ...scopeField
2122
2190
  };
2123
2191
  }
2124
- async function handleGetReport(rest) {
2192
+ async function handleGetReport(rest, root) {
2125
2193
  const kind = typeof rest.kind === "string" ? rest.kind : "";
2126
2194
  const periodInput = optionalReportPeriod(rest.period);
2127
2195
  const bookId = optionalString(rest.bookId);
@@ -2132,14 +2200,14 @@ async function handleGetReport(rest) {
2132
2200
  return getBalanceSheetReport({
2133
2201
  bookId,
2134
2202
  period: periodInput
2135
- });
2203
+ }, root);
2136
2204
  }
2137
2205
  if (kind === "pl") {
2138
2206
  if (!periodInput) throw periodRequired("getReport pl");
2139
2207
  return getProfitLossReport({
2140
2208
  bookId,
2141
2209
  period: periodInput
2142
- });
2210
+ }, root);
2143
2211
  }
2144
2212
  if (kind === "ledger") {
2145
2213
  if (typeof rest.accountCode !== "string" || rest.accountCode === "") throw new AccountingError(400, "getReport ledger: accountCode is required");
@@ -2147,79 +2215,79 @@ async function handleGetReport(rest) {
2147
2215
  bookId,
2148
2216
  accountCode: rest.accountCode,
2149
2217
  period: periodInput
2150
- });
2218
+ }, root);
2151
2219
  }
2152
2220
  throw new AccountingError(400, `getReport: unknown kind ${JSON.stringify(kind)}`);
2153
2221
  }
2154
2222
  var ACTION_HANDLERS = {
2155
2223
  [require_shared.ACCOUNTING_ACTIONS.openBook]: handleOpenBook,
2156
- [require_shared.ACCOUNTING_ACTIONS.getBooks]: () => listBooks(),
2157
- [require_shared.ACCOUNTING_ACTIONS.createBook]: async (rest) => {
2224
+ [require_shared.ACCOUNTING_ACTIONS.getBooks]: (_rest, root) => listBooks(root),
2225
+ [require_shared.ACCOUNTING_ACTIONS.createBook]: async (rest, root) => {
2158
2226
  const result = await createBook({
2159
2227
  name: typeof rest.name === "string" ? rest.name : "",
2160
2228
  currency: typeof rest.currency === "string" ? rest.currency : void 0,
2161
2229
  country: typeof rest.country === "string" ? rest.country : void 0,
2162
2230
  fiscalYearEnd: rest.fiscalYearEnd
2163
- });
2231
+ }, root);
2164
2232
  return {
2165
2233
  bookId: result.book.id,
2166
2234
  ...result
2167
2235
  };
2168
2236
  },
2169
- [require_shared.ACCOUNTING_ACTIONS.updateBook]: async (rest) => {
2237
+ [require_shared.ACCOUNTING_ACTIONS.updateBook]: async (rest, root) => {
2170
2238
  const result = await updateBook({
2171
2239
  bookId: typeof rest.bookId === "string" ? rest.bookId : "",
2172
2240
  name: typeof rest.name === "string" ? rest.name : void 0,
2173
2241
  country: typeof rest.country === "string" ? rest.country : void 0,
2174
2242
  fiscalYearEnd: rest.fiscalYearEnd
2175
- });
2243
+ }, root);
2176
2244
  return {
2177
2245
  bookId: result.book.id,
2178
2246
  ...result
2179
2247
  };
2180
2248
  },
2181
- [require_shared.ACCOUNTING_ACTIONS.deleteBook]: (rest) => deleteBook({
2249
+ [require_shared.ACCOUNTING_ACTIONS.deleteBook]: (rest, root) => deleteBook({
2182
2250
  bookId: typeof rest.bookId === "string" ? rest.bookId : "",
2183
2251
  confirm: rest.confirm === true
2184
- }),
2185
- [require_shared.ACCOUNTING_ACTIONS.getAccounts]: (rest) => listAccounts({ bookId: optionalString(rest.bookId) }),
2186
- [require_shared.ACCOUNTING_ACTIONS.upsertAccount]: (rest) => upsertAccount({
2252
+ }, root),
2253
+ [require_shared.ACCOUNTING_ACTIONS.getAccounts]: (rest, root) => listAccounts({ bookId: optionalString(rest.bookId) }, root),
2254
+ [require_shared.ACCOUNTING_ACTIONS.upsertAccount]: (rest, root) => upsertAccount({
2187
2255
  bookId: optionalString(rest.bookId),
2188
2256
  account: rest.account
2189
- }),
2190
- [require_shared.ACCOUNTING_ACTIONS.addEntries]: (rest) => addEntries({
2257
+ }, root),
2258
+ [require_shared.ACCOUNTING_ACTIONS.addEntries]: (rest, root) => addEntries({
2191
2259
  bookId: optionalString(rest.bookId),
2192
2260
  entries: rest.entries
2193
- }),
2194
- [require_shared.ACCOUNTING_ACTIONS.voidEntry]: (rest) => voidEntry({
2261
+ }, root),
2262
+ [require_shared.ACCOUNTING_ACTIONS.voidEntry]: (rest, root) => voidEntry({
2195
2263
  bookId: optionalString(rest.bookId),
2196
2264
  entryId: typeof rest.entryId === "string" ? rest.entryId : "",
2197
2265
  reason: optionalString(rest.reason),
2198
2266
  voidDate: optionalString(rest.voidDate)
2199
- }),
2200
- [require_shared.ACCOUNTING_ACTIONS.getJournalEntries]: (rest) => listEntries({
2267
+ }, root),
2268
+ [require_shared.ACCOUNTING_ACTIONS.getJournalEntries]: (rest, root) => listEntries({
2201
2269
  bookId: optionalString(rest.bookId),
2202
2270
  from: optionalString(rest.from),
2203
2271
  to: optionalString(rest.to),
2204
2272
  accountCode: optionalString(rest.accountCode)
2205
- }),
2206
- [require_shared.ACCOUNTING_ACTIONS.getOpeningBalances]: (rest) => getOpeningBalances({ bookId: optionalString(rest.bookId) }),
2207
- [require_shared.ACCOUNTING_ACTIONS.setOpeningBalances]: (rest) => setOpeningBalances({
2273
+ }, root),
2274
+ [require_shared.ACCOUNTING_ACTIONS.getOpeningBalances]: (rest, root) => getOpeningBalances({ bookId: optionalString(rest.bookId) }, root),
2275
+ [require_shared.ACCOUNTING_ACTIONS.setOpeningBalances]: (rest, root) => setOpeningBalances({
2208
2276
  bookId: optionalString(rest.bookId),
2209
2277
  asOfDate: typeof rest.asOfDate === "string" ? rest.asOfDate : "",
2210
2278
  lines: rest.lines ?? [],
2211
2279
  memo: optionalString(rest.memo)
2212
- }),
2280
+ }, root),
2213
2281
  [require_shared.ACCOUNTING_ACTIONS.getReport]: handleGetReport,
2214
- [require_shared.ACCOUNTING_ACTIONS.getTimeSeries]: (rest) => getTimeSeriesReport({
2282
+ [require_shared.ACCOUNTING_ACTIONS.getTimeSeries]: (rest, root) => getTimeSeriesReport({
2215
2283
  bookId: optionalString(rest.bookId),
2216
2284
  metric: rest.metric,
2217
2285
  granularity: rest.granularity,
2218
2286
  from: rest.from,
2219
2287
  to: rest.to,
2220
2288
  accountCode: rest.accountCode
2221
- }),
2222
- [require_shared.ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest) => rebuildSnapshots({ bookId: optionalString(rest.bookId) })
2289
+ }, root),
2290
+ [require_shared.ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest, root) => rebuildSnapshots({ bookId: optionalString(rest.bookId) }, root)
2223
2291
  };
2224
2292
  var PREVIEW_ACTIONS = /* @__PURE__ */ new Set([
2225
2293
  require_shared.ACCOUNTING_ACTIONS.openBook,
@@ -2296,11 +2364,11 @@ function previewMessage(action, fields) {
2296
2364
  const head = ownEntry(MESSAGE_BUILDERS, action)?.(fields);
2297
2365
  return head ? `${head} ${VIEW_VISIBLE_TRAILER}` : VIEW_VISIBLE_TRAILER;
2298
2366
  }
2299
- async function dispatch(body) {
2367
+ async function dispatch(body, root) {
2300
2368
  const { action, ...rest } = body;
2301
2369
  const handler = ownEntry(ACTION_HANDLERS, action);
2302
2370
  if (!handler) throw new AccountingError(400, `unknown action ${JSON.stringify(action)}`);
2303
- const result = await handler(rest);
2371
+ const result = await handler(rest, root);
2304
2372
  const handlerFields = require_shared.isRecord(result) ? result : { value: result };
2305
2373
  const dataField = PREVIEW_ACTIONS.has(action) ? { data: {
2306
2374
  action,
@@ -2317,8 +2385,9 @@ async function dispatch(body) {
2317
2385
  /** Build the accounting Express router. The host injects its workspace
2318
2386
  * root + logger via `configureAccountingServer(...)` and pub/sub via
2319
2387
  * `initAccountingEventPublisher(...)`, then mounts the returned router
2320
- * with `app.use(...)`. */
2321
- function createAccountingRouter() {
2388
+ * with `app.use(...)`. Pass `resolveWorkspaceRoot` to serve more than
2389
+ * one root; omit it and every request uses the configured one. */
2390
+ function createAccountingRouter(options = {}) {
2322
2391
  const router = (0, express.Router)();
2323
2392
  router.post(require_shared.ACCOUNTING_API.dispatch.path, asyncHandler("accounting", "accounting dispatch failed", async (req, res) => {
2324
2393
  const { body } = req;
@@ -2330,7 +2399,8 @@ function createAccountingRouter() {
2330
2399
  const { action } = body;
2331
2400
  log.info("accounting", "POST dispatch: start", { action });
2332
2401
  try {
2333
- const result = await dispatch(body);
2402
+ const root = options.resolveWorkspaceRoot?.(req);
2403
+ const result = await dispatch(body, root);
2334
2404
  log.info("accounting", "POST dispatch: ok", { action });
2335
2405
  res.json(result);
2336
2406
  } catch (err) {