@vrtmrz/livesync-commonlib 0.1.43 → 0.1.44

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.
@@ -1,3 +1,4 @@
1
+ const USAGE_ACCOUNTING_YIELD_EVERY = 64;
1
2
  function asRecord(value) {
2
3
  return value;
3
4
  }
@@ -30,13 +31,29 @@ async function analyseDatabaseUsage(options) {
30
31
  const errors = [];
31
32
  const documentMap = /* @__PURE__ */ new Map();
32
33
  const chunkMap = /* @__PURE__ */ new Map();
33
- const progress = { idsProcessed: 0, revisionsProcessed: 0 };
34
+ const progress = {
35
+ phase: "reading",
36
+ idsProcessed: 0,
37
+ revisionsProcessed: 0,
38
+ processed: 0,
39
+ total: 0,
40
+ totalIsFinal: false
41
+ };
34
42
  let startUpdateSeq;
35
43
  let endUpdateSeq;
36
44
  let enumerationFailed = false;
37
- const reportProgress = () => onProgress?.({ ...progress });
45
+ const reportProgress = () => {
46
+ try {
47
+ onProgress?.({ ...progress });
48
+ } catch {
49
+ }
50
+ };
38
51
  const isCancelled = () => signal?.aborted === true;
39
52
  let cancelled = isCancelled();
53
+ let documentRevisionCount = 0;
54
+ let referenceCount = 0;
55
+ reportProgress();
56
+ if (isCancelled()) cancelled = true;
40
57
  if (!cancelled) {
41
58
  try {
42
59
  const info = await source.info();
@@ -47,6 +64,7 @@ async function analyseDatabaseUsage(options) {
47
64
  }
48
65
  if (isCancelled()) cancelled = true;
49
66
  }
67
+ let enumerationFinished = false;
50
68
  try {
51
69
  if (!cancelled && !isCancelled()) {
52
70
  for await (const id of source.enumerateIds()) {
@@ -74,7 +92,16 @@ async function analyseDatabaseUsage(options) {
74
92
  errors.push({ code: "revision_list_read_failed", id });
75
93
  }
76
94
  progress.idsProcessed++;
95
+ for (const revisionInfo of revisionInfos) {
96
+ if (revisionInfo.status === "available" && typeof revisionInfo.rev === "string") {
97
+ progress.total++;
98
+ }
99
+ }
77
100
  reportProgress();
101
+ if (isCancelled()) {
102
+ cancelled = true;
103
+ break;
104
+ }
78
105
  for (const revisionInfo of revisionInfos) {
79
106
  if (isCancelled()) {
80
107
  cancelled = true;
@@ -86,27 +113,28 @@ async function analyseDatabaseUsage(options) {
86
113
  errors.push({ code: "revision_document_invalid", id });
87
114
  continue;
88
115
  }
89
- progress.revisionsProcessed++;
90
116
  let rawDocument;
91
117
  try {
92
118
  rawDocument = await source.get(id, { rev: revision });
93
119
  } catch {
94
120
  if (isCancelled()) {
95
121
  cancelled = true;
96
- reportProgress();
97
122
  break;
98
123
  }
99
124
  errors.push({ code: "revision_read_failed", id, revision });
125
+ progress.revisionsProcessed++;
126
+ progress.processed++;
100
127
  reportProgress();
101
128
  continue;
102
129
  }
103
130
  if (isCancelled()) {
104
131
  cancelled = true;
105
- reportProgress();
106
132
  break;
107
133
  }
108
134
  if (!rawDocument) {
109
135
  errors.push({ code: "revision_not_found", id, revision });
136
+ progress.revisionsProcessed++;
137
+ progress.processed++;
110
138
  reportProgress();
111
139
  continue;
112
140
  }
@@ -124,9 +152,13 @@ async function analyseDatabaseUsage(options) {
124
152
  path,
125
153
  chunks,
126
154
  uniqueChunks: /* @__PURE__ */ new Set(),
127
- sharedChunks: /* @__PURE__ */ new Set()
155
+ sharedChunks: /* @__PURE__ */ new Set(),
156
+ uniqueChunkSize: 0,
157
+ sharedChunkSize: 0
128
158
  });
129
159
  documentMap.set(documentId, revisions);
160
+ documentRevisionCount++;
161
+ referenceCount += chunks.size;
130
162
  } else if (document.type === "leaf") {
131
163
  const chunkId = getStringField(document._id);
132
164
  if (!chunkMap.has(chunkId) && !document._deleted) {
@@ -137,11 +169,14 @@ async function analyseDatabaseUsage(options) {
137
169
  } catch {
138
170
  errors.push({ code: "revision_document_invalid", id, revision });
139
171
  }
172
+ progress.revisionsProcessed++;
173
+ progress.processed++;
140
174
  reportProgress();
141
175
  }
142
176
  if (isCancelled()) cancelled = true;
143
177
  if (cancelled) break;
144
178
  }
179
+ if (!cancelled && !isCancelled()) enumerationFinished = true;
145
180
  } else if (isCancelled()) {
146
181
  cancelled = true;
147
182
  }
@@ -152,60 +187,127 @@ async function analyseDatabaseUsage(options) {
152
187
  errors.push({ code: "id_enumeration_failed" });
153
188
  }
154
189
  }
155
- for (const revisions of documentMap.values()) {
156
- for (const document of revisions) {
157
- for (const chunkId of document.chunks) {
158
- const chunk = chunkMap.get(chunkId);
159
- if (!chunk) continue;
160
- if (chunk.refCount === 0) document.uniqueChunks.add(chunkId);
161
- else document.sharedChunks.add(chunkId);
162
- chunk.refCount++;
190
+ if (isCancelled()) cancelled = true;
191
+ if (enumerationFinished) {
192
+ progress.totalIsFinal = true;
193
+ reportProgress();
194
+ if (isCancelled()) cancelled = true;
195
+ }
196
+ const readingWasCancelled = cancelled;
197
+ progress.phase = "accounting";
198
+ progress.processed = 0;
199
+ progress.total = referenceCount + documentRevisionCount + chunkMap.size;
200
+ progress.totalIsFinal = true;
201
+ reportProgress();
202
+ let accountingCancelled = !readingWasCancelled && isCancelled();
203
+ const rows = [];
204
+ const accountingErrors = [];
205
+ const recordAccountingWork = () => {
206
+ progress.processed++;
207
+ return progress.processed % USAGE_ACCOUNTING_YIELD_EVERY === 0;
208
+ };
209
+ const yieldAfterAccountingBatch = async () => {
210
+ reportProgress();
211
+ await new Promise((resolve) => setTimeout(resolve, 0));
212
+ return readingWasCancelled || !isCancelled();
213
+ };
214
+ if (!readingWasCancelled && isCancelled()) accountingCancelled = true;
215
+ if (!accountingCancelled) {
216
+ for (const revisions of documentMap.values()) {
217
+ for (const document of revisions) {
218
+ for (const chunkId of document.chunks) {
219
+ const chunk = chunkMap.get(chunkId);
220
+ if (chunk) {
221
+ if (chunk.refCount === 0) {
222
+ document.uniqueChunks.add(chunkId);
223
+ document.uniqueChunkSize += chunk.length;
224
+ } else {
225
+ document.sharedChunks.add(chunkId);
226
+ document.sharedChunkSize += chunk.length;
227
+ }
228
+ chunk.refCount++;
229
+ }
230
+ if (recordAccountingWork() && !await yieldAfterAccountingBatch()) {
231
+ accountingCancelled = true;
232
+ break;
233
+ }
234
+ }
235
+ if (accountingCancelled) break;
236
+ }
237
+ if (accountingCancelled) break;
238
+ }
239
+ }
240
+ if (!accountingCancelled) {
241
+ for (const revisions of documentMap.values()) {
242
+ for (const document of revisions) {
243
+ const revisionNumber = getRevisionNumber(document.revision);
244
+ let revisionHash;
245
+ try {
246
+ revisionHash = getRevisionHash(document.revision);
247
+ } catch {
248
+ accountingErrors.push({
249
+ code: "revision_document_invalid",
250
+ id: document.id,
251
+ revision: document.revision
252
+ });
253
+ if (recordAccountingWork() && !await yieldAfterAccountingBatch()) {
254
+ accountingCancelled = true;
255
+ break;
256
+ }
257
+ continue;
258
+ }
259
+ rows.push({
260
+ title: `${document.path} (${document.revision})`,
261
+ id: document.id,
262
+ path: document.path,
263
+ revision: document.revision,
264
+ rev: revisionNumber,
265
+ revHash: revisionHash,
266
+ uniqueChunkCount: document.uniqueChunks.size,
267
+ sharedChunkCount: document.sharedChunks.size,
268
+ uniqueChunkSize: document.uniqueChunkSize,
269
+ sharedChunkSize: document.sharedChunkSize
270
+ });
271
+ if (recordAccountingWork() && !await yieldAfterAccountingBatch()) {
272
+ accountingCancelled = true;
273
+ break;
274
+ }
163
275
  }
276
+ if (accountingCancelled) break;
164
277
  }
165
278
  }
166
- const totalSize = (ids) => {
167
- let size = 0;
168
- for (const id of ids) size += chunkMap.get(id)?.length ?? 0;
169
- return size;
170
- };
171
- const rows = [];
172
- for (const revisions of documentMap.values()) {
173
- for (const document of revisions) {
174
- const revisionNumber = getRevisionNumber(document.revision);
175
- let revisionHash;
176
- try {
177
- revisionHash = getRevisionHash(document.revision);
178
- } catch {
179
- errors.push({ code: "revision_document_invalid", id: document.id, revision: document.revision });
180
- continue;
279
+ let orphanChunkCount = 0;
280
+ let orphanChunkSize = 0;
281
+ if (!accountingCancelled) {
282
+ for (const chunk of chunkMap.values()) {
283
+ if (chunk.refCount === 0) {
284
+ orphanChunkCount++;
285
+ orphanChunkSize += chunk.length;
286
+ }
287
+ if (recordAccountingWork() && !await yieldAfterAccountingBatch()) {
288
+ accountingCancelled = true;
289
+ break;
181
290
  }
182
- rows.push({
183
- title: `${document.path} (${document.revision})`,
184
- id: document.id,
185
- path: document.path,
186
- revision: document.revision,
187
- rev: revisionNumber,
188
- revHash: revisionHash,
189
- uniqueChunkCount: document.uniqueChunks.size,
190
- sharedChunkCount: document.sharedChunks.size,
191
- uniqueChunkSize: totalSize(document.uniqueChunks),
192
- sharedChunkSize: totalSize(document.sharedChunks)
193
- });
194
291
  }
195
292
  }
196
- const orphanChunks = [...chunkMap.entries()].filter(([, chunk]) => chunk.refCount === 0);
197
- rows.push({
198
- title: "__orphan",
199
- id: "__orphan",
200
- path: "__orphan",
201
- revision: "1-xxxxx",
202
- rev: "1",
203
- revHash: "xxxxx",
204
- uniqueChunkCount: orphanChunks.length,
205
- sharedChunkCount: 0,
206
- uniqueChunkSize: orphanChunks.reduce((size, [, chunk]) => size + chunk.length, 0),
207
- sharedChunkSize: 0
208
- });
293
+ if (accountingCancelled) {
294
+ cancelled = true;
295
+ rows.length = 0;
296
+ } else {
297
+ errors.push(...accountingErrors);
298
+ rows.push({
299
+ title: "__orphan",
300
+ id: "__orphan",
301
+ path: "__orphan",
302
+ revision: "1-xxxxx",
303
+ rev: "1",
304
+ revHash: "xxxxx",
305
+ uniqueChunkCount: orphanChunkCount,
306
+ sharedChunkCount: 0,
307
+ uniqueChunkSize: orphanChunkSize,
308
+ sharedChunkSize: 0
309
+ });
310
+ }
209
311
  if (!cancelled && !isCancelled()) {
210
312
  try {
211
313
  const info = await source.info();
@@ -216,6 +318,8 @@ async function analyseDatabaseUsage(options) {
216
318
  }
217
319
  }
218
320
  if (isCancelled()) cancelled = true;
321
+ reportProgress();
322
+ if (isCancelled()) cancelled = true;
219
323
  const changedDuringRun = startUpdateSeq === void 0 || endUpdateSeq === void 0 ? void 0 : startUpdateSeq !== endUpdateSeq;
220
324
  const snapshotUncertain = startUpdateSeq === void 0 || endUpdateSeq === void 0;
221
325
  const hasDocumentRows = rows.length > 1;
@@ -11,14 +11,59 @@ The focused `/diagnostics` entry supplies read-only database usage analysis and
11
11
 
12
12
  Usage retains the existing available-revision selection and incremental chunk accounting. The first processed reference counts a chunk as unique; later references count it as shared. Sizes count JavaScript string units, as in the existing report. These columns do not measure physical database size or establish whether a chunk can be deleted.
13
13
 
14
+ Usage progress emits a `reading` phase while it discovers revisions, with `idsProcessed` and `revisionsProcessed` retained for existing consumers. `processed` counts settled available-revision reads, including failures; `total` grows as available revisions are discovered and is not final until enumeration finishes. The analyser does not pre-scan to calculate a total. The separate `accounting` phase has a fixed work-item total and yields between batches so the host can respond to cancellation. Cancellation during reading retains collected evidence; cancellation during accounting omits incomplete rows and orphan statistics. A failing progress callback does not change the result.
15
+
14
16
  Integrity inspection enumerates actual stored document IDs, including logically deleted normal-file Metadata and current conflict branches. It uses the same identity classifier as the existing Metadata inspector. It reads content only for groups with identity discrepancies, conflicting branches, duplicate path claims, or differences between the inspected local and remote Metadata. Content comparisons use reconstructed bytes, including the Vault file where available. Missing, unreadable, and uninspected content remain distinct outcomes. Equal content within this scope does not establish that an entry is safe to delete.
15
17
 
18
+ Standard integrity observations include recorded document type, size, and total, unique, and embedded Chunk-reference counts. When suspect bytes have already been read, they also include measured decoded-content and Vault sizes, their differences from recorded or reconstructed sizes, and the Vault-to-Metadata modification-time difference. Case-only and NFC-equivalent path group membership is included once for each group. These facts use the normal decoded and decrypted Metadata and reads already needed for inspection; they do not add database, HTTP, or Vault reads. Facts which were not inspected remain absent or `not-checked`.
19
+
20
+ Integrity inspection classifies up to four Metadata documents or processes up to four suspect content groups concurrently. Hosts can set `concurrency` between one and sixteen for comparison or resource limits. Local and remote Metadata scans remain separate, and results retain enumeration order regardless of completion order. Sources with `getMany` fetch current Metadata and required Chunks in groups of up to 64 documents. Metadata classification follows each bulk read; content groups read their Chunk batches and Vault files sequentially. The CouchDB reader separately decodes up to four documents per bulk response, so `concurrency` limits analyser work rather than total concurrent decryption across active groups. Exact conflict revisions still use individual reads. Cancellation stops dispatching new work and drains active reads before releasing the remote source; collected findings remain available.
21
+
22
+ Integrity progress identifies the phase and source. Metadata progress counts completed documents, including excluded IDs, against the source's `doc_count` when available. Content progress counts completed suspect groups against their known total. Missing document totals remain unknown until enumeration finishes. A source changed during inspection can change the total and makes the report partial. Hosts should label these separate units rather than combine them into one percentage.
23
+
24
+ The optional `onLog` callback receives phase start and finish events, with progress events at most once every five seconds while work completes. Events contain counts, elapsed time, the concurrency limit, active and peak reads, read operations, and summed read duration. Read operations count requested documents; a bulk read counts once towards active reads and elapsed read duration. Summed read duration includes overlapping reads. HTTP request counts belong to the host's injected fetch boundary and can differ from document reads. Grouping has its own timing event. These events contain no paths, document identifiers, connection details, or raw errors. Progress and log callback failures do not interrupt inspection.
25
+
16
26
  The first implementation supports the local database and CouchDB. Internal-file, customisation, and plug-in storage Metadata, older revision history, and other remote backends are explicitly outside integrity inspection. Usage continues to cover the revisions available to its local source. A remote failure retains the local findings and marks the integrity report partial.
17
27
 
28
+ `includeRemote` defaults to true. Setting it to false does not open the remote source: local identity, conflict, and duplicate-path checks still run, with content comparisons against the Vault for suspect groups. Remote coverage and cross-source comparisons remain `not-checked`, and `scope.remote` is `excluded` in JSON and Markdown reports. A local-only run can complete within that declared scope; excluding a remote is distinct from a failed connection. Hosts should collect this choice for each run rather than change synchronisation settings.
29
+
18
30
  The CouchDB reader only sends GET requests. It reads existing synchronisation parameters when decryption needs a Security Seed and never creates missing parameters, initialises a database, or invokes replication admission. Local content reads do not fetch or cache remote Chunks. Owned requests are cancelled and released when the run ends; host-native request adapters may finish an already dispatched request after cancellation, but its result is discarded.
19
31
 
32
+ Bulk reads use `_all_docs` with selected `keys`, `include_docs`, and optional `conflicts`. The reader limits each request to 64 documents and a 6,000-character encoded URL; an individual key which exceeds that URL limit uses the existing single-document GET. ID enumeration does not include document bodies, so unrelated Chunks are not downloaded. Embedded Chunks require no request, and repeated Chunk IDs within one batch share a read. Missing rows, deleted documents, unreadable results, and failed HTTP requests retain distinct evidence; a failed batch does not establish that its documents are absent. Bulk results use the same decryption and decompression as individual reads and are released after each batch.
33
+
20
34
  Database update sequences and the host's configuration identity are checked again before completion. Storage readers must detect changes while reading a file. These observations can detect concurrent changes; they do not provide an atomic snapshot or prevent a file from changing later.
21
35
 
22
36
  Reports project a whitelist of facts. Pseudonyms preserve exact path equality across sources and keep document, revision, and Chunk identifiers separate. Case and Unicode normalisation facts are computed before projection. Maps are local to one run and are not persisted. Original-name mode still excludes credentials, connection addresses, encryption keys, raw errors, and file contents. JSON and Markdown use the same projected integrity result; TSV uses the same name mapper for Usage.
23
37
 
38
+ Each pseudonym report uses a random eight-character public salt. Path tokens have the form `<salt>$<hash>`; `<hash>` is the first 128 bits of a SHA-256 digest, shown as 32 lowercase hexadecimal characters. The digest is domain-separated by name kind and uses the exact stored Vault-relative path, without lower-casing or Unicode NFC normalisation. The mapper keeps no persistent salt or correspondence table. A consumer can parse a complete path token with `parseDiagnosticPathToken` and pass its salt to `createDiagnosticNameMapper('pseudonym', parsed.salt)` to reproduce path, document, revision, and Chunk pseudonyms for a targeted follow-up. Revision pseudonyms retain a valid generation prefix, such as `7-psu…`.
39
+
40
+ The salt is public, so someone with the report can test guessed path values against a token; pseudonyms are not encryption. Earlier ordinal pseudonyms cannot be looked up this way. The path-token parser only extracts a salt and hash from a complete token; it does not recover the original path.
41
+
42
+ ### Compact Integrity Markdown report
43
+
44
+ Integrity Markdown retains its status, name mode, versions, settings, scope, coverage, and count summary, with separate counts for content and Vault checks which were not performed. **State counts** count observations, not unique files; each local or remote observation contributes to its state total. The **Review index** lists references to observations which match review conditions, before **Findings**. Those conditions are identity mismatches, current conflicts, case- or NFC-equivalent paths, non-NFC paths, a missing group in the other inspected source, missing or unreadable content or Vault files, unavailable Chunks, different or unavailable comparisons, non-zero measured differences, and absent Metadata. These are review candidates, not findings of corruption. An observation outside the index is still present in **Findings**, and unchecked content or Vault state is not evidence of health.
45
+
46
+ Each observation uses a compact field order while preserving every observation and its complete path, document ID, revision, and reference values:
47
+
48
+ ```text
49
+ ### `remote:4410` source=remote
50
+ path: `<salt>$<hash>`
51
+ doc: `<document-id>` rev=`<revision>` expected=self
52
+ state: consistent-winner-logicallyLive-NFC-newnote conflicts=0
53
+ paths: case=none nfcPeers=none | otherDB=present
54
+ data: recorded=47584B chunks=1/1/0 read=0
55
+ check: content=not-checked vault=not-checked@`vault:remote:4410`
56
+ measure: content=47584B vault=47584B deltaContent=0B deltaVault=0B deltaMtime=0ms
57
+ ```
58
+
59
+ The `state` value orders identity, current branch, logical deletion, exact stored-path NFC status, and Metadata type. Identity is `consistent`, `documentIdMismatch`, or `namespaceMismatch`; JSON keeps the values `document-id-mismatch` and `namespace-mismatch` for the latter two states. The branch is `winner` or `conflict`; `winner` can coexist with a non-zero conflict count. `logicallyLive` and `logicallyDeleted` describe the deletion marker. `NFC` means the stored path is normalised to Unicode NFC; `nonNFC` means it is not. The type is `plain`, `newnote`, `notes`, or `unknown`, and is `notRecorded` when Metadata is absent. `consistent` confirms the identity check only; it does not establish readable or equal content.
60
+
61
+ `expected=self` means the actual document ID exactly matches the expected ID. Otherwise, the field contains the expected ID, or `?` if it was not recorded. In `paths`, `case` and `nfcPeers` report case-equivalent and NFC-equivalent path groups: `none` means no equivalent path was found, `yes` means one was found, and `yes@case:1` or `yes@nfc:1` adds the group reference when available. `otherDB` describes whether the same identity and path group is present in the other inspected source; it does not compare content. An excluded or unavailable source is `not-checked`.
62
+
63
+ In `data`, `chunks` is the total/unique/embedded Chunk-reference count, and `read` is the number of Chunk reads, not HTTP requests. When Metadata is absent, its recorded size and Chunk counts are `?`. A `?` means a value was not recorded; it is distinct from zero, missing, unreadable, and `not-checked`. The `check` line distinguishes `available`, `missing`, `unreadable`, and `not-checked` for content and Vault evidence; a recorded reason follows the status in parentheses. The optional `measure` line includes only available values: decoded content size and Vault size in bytes, `deltaContent` (decoded content minus recorded Metadata size), `deltaVault` (Vault size minus decoded content size), and `deltaMtime` (Vault modification time minus Metadata modification time) in milliseconds. Positive differences have a `+` prefix, negative differences retain `-`, and zero is `0`; these measurements describe collected evidence and do not select an authoritative version.
64
+
65
+ Unavailable Chunk entries, comparisons, case/NFC equivalence groups, and errors retain their names, reasons, and references. The collapsible details section and fenced text copy remain available for sharing the report. The compact Markdown representation does not change the JSON data or schema.
66
+
67
+ Integrity Markdown wraps the complete report in a `details` element and a fenced text block for pasting into an issue. The fence is longer than any backtick run in the report, including original names. This keeps the full evidence together without expanding every observation in the issue body.
68
+
24
69
  Hosts may pass Commonlib, plug-in, and application versions to either report projector through its optional third argument. Only those three version fields are retained. Integrity reports also retain the five identity-related settings used for the inspection; credentials and the active connection configuration remain outside the report.
@@ -8,7 +8,13 @@ The code paths below identify the maintained Self-hosted LiveSync composition po
8
8
 
9
9
  ## Obsidian plug-in
10
10
 
11
- Unreleased [database diagnostics](database-diagnostics.md) tests cover preserved Usage accounting, normal-file identity classification, current conflict branches, byte comparisons, embedded Chunks, missing and unreadable content, cancellation, concurrent changes, and consistent pseudonyms. Real in-memory PouchDB tests verify pagination and conflict revision reads while retaining the original database handle. A CouchDB 3.5.0 integration test verifies GET-only access, encrypted Metadata and Chunk reads with an existing Security Seed, missing-seed failure without initialisation, and unchanged database update sequences and Seed documents. Focused tests also reject ciphertext which remains undecoded after an ID mismatch. Self-hosted LiveSync owns the serviceFeature, run-time name choice, dialogue lifecycle, and report copy and download controls. Its real Obsidian 1.12.7 test on Linux verifies cancellation before a run, both name modes, the nine-column Usage report, copy and download payloads, local integrity findings when CouchDB is unavailable, and unchanged settings, database update sequences, and Vault file names. Mobile emulation verifies the choice dialogue's viewport, safe-area, and touch-target layout; native iOS and Android behaviour remains unverified.
11
+ Unreleased remote-inspection tests compare individual and bulk reads against CouchDB 3.5.0. A fixture with 65 Metadata documents and 130 Chunks produces identical integrity reports while reducing HTTP requests from 198 to 8. Encrypted bulk reads preserve current conflicts, exact revision access, missing-document evidence, and unchanged database and Security Seed state. Unit tests cover bounded GET batches, encoded URL limits, per-document failures, cancellation, and explicitly excluded remote coverage. These counts describe the fixture; they do not establish large-Vault elapsed-time improvements.
12
+
13
+ Unreleased [database diagnostics](database-diagnostics.md) tests cover preserved Usage accounting, normal-file identity classification, current conflict branches, byte comparisons, embedded Chunks, missing and unreadable content, cancellation, concurrent changes, and consistent pseudonyms. Real in-memory PouchDB tests verify pagination and conflict revision reads while retaining the original database handle. A CouchDB 3.5.0 integration test verifies GET-only access, encrypted Metadata and Chunk reads with an existing Security Seed, missing-seed failure without initialisation, and unchanged database update sequences and Seed documents. Focused tests also reject ciphertext which remains undecoded after an ID mismatch. Commonlib owns the result contract and compact Integrity Markdown formatter; Self-hosted LiveSync owns the serviceFeature, run-time name choice, dialogue lifecycle, and report copy and download controls. Its real Obsidian 1.12.7 test on Linux verifies cancellation before a run, both name modes, the nine-column Usage report, copy and download payloads, local integrity findings when CouchDB is unavailable, and unchanged settings, database update sequences, and Vault file names. Mobile emulation verifies the choice dialogue's viewport, safe-area, and touch-target layout; native iOS and Android behaviour remains unverified.
14
+
15
+ The Commonlib `/diagnostics` entry provides a path-token parser and a mapper which can reuse a report's public salt to reproduce path, document, revision, and Chunk pseudonyms for a targeted follow-up. Its Usage progress contract separates revision reading from cancellable accounting. Self-hosted LiveSync owns the current-Vault lookup, local-only candidate display, follow-up choice, and dialogue lifecycle; the public salt permits guessed-path checks, while older ordinal pseudonyms cannot be looked up.
16
+
17
+ Self-hosted LiveSync's real Obsidian 1.12.7 test on Linux also verifies local lookup, selection between distinct document IDs for one path, and a follow-up report which reuses the selected ID and original salt. It checks that compact Markdown retains every JSON observation reference and pseudonym, and that copy and save preserve the complete Markdown output. Desktop and mobile-mode viewports verify the candidate list, report controls, and horizontal overflow. These host tests complement the package contracts; they do not establish native iOS or Android behaviour.
12
18
 
13
19
  Unreleased content-evidence tests replicate 99, 100, and 599 updates between real in-memory PouchDB databases, including a receiver with `revs_limit: 100`. They verify unchanged reflection after the recorded revision is pruned, including a recreated handler, and preserve same-size edits, intentional reverts, and records without valid content evidence. Separate tests verify legacy upgrades and changes during hashing. These are package-owned tests; they do not establish Windows, mobile, or remote CouchDB behaviour.
14
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrtmrz/livesync-commonlib",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
4
4
  "description": "Shared data, storage, and replication primitives for Self-hosted LiveSync clients.",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -1005,6 +1005,7 @@
1005
1005
  }
1006
1006
  },
1007
1007
  "dependencies": {
1008
+ "@aws-crypto/sha256-js": "5.2.0",
1008
1009
  "@aws-sdk/client-s3": "^3.808.0",
1009
1010
  "@smithy/fetch-http-handler": "^5.3.10",
1010
1011
  "@smithy/md5-js": "^4.2.9",