@vrtmrz/livesync-commonlib 0.1.41 → 0.1.43

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 (74) hide show
  1. package/README.md +29 -1
  2. package/dist/API/DirectFileManipulatorV2.js +16 -1
  3. package/dist/API/processSetting.d.ts +1 -0
  4. package/dist/API/processSetting.js +6 -1
  5. package/dist/common/fileContentHash.d.ts +2 -0
  6. package/dist/common/fileContentHash.js +18 -0
  7. package/dist/common/models/setting.const.defaults.js +1 -0
  8. package/dist/common/models/setting.const.qr.js +1 -0
  9. package/dist/common/models/setting.policy.d.ts +4 -0
  10. package/dist/common/models/setting.policy.js +1 -0
  11. package/dist/common/models/setting.type.d.ts +8 -1
  12. package/dist/common/models/shared.definition.configNames.js +5 -0
  13. package/dist/common/models/tweak.definition.d.ts +6 -0
  14. package/dist/common/models/tweak.definition.js +1 -0
  15. package/dist/common/pathIdentity.d.ts +19 -0
  16. package/dist/common/pathIdentity.js +35 -0
  17. package/dist/common/pathNormalization.d.ts +7 -0
  18. package/dist/common/pathNormalization.js +13 -0
  19. package/dist/common/settingConstants.d.ts +1 -0
  20. package/dist/diagnostics/couchdb.d.ts +15 -0
  21. package/dist/diagnostics/couchdb.js +129 -0
  22. package/dist/diagnostics/index.d.ts +10 -0
  23. package/dist/diagnostics/index.js +26 -0
  24. package/dist/diagnostics/integrity.d.ts +5 -0
  25. package/dist/diagnostics/integrity.js +674 -0
  26. package/dist/diagnostics/local.d.ts +4 -0
  27. package/dist/diagnostics/local.js +25 -0
  28. package/dist/diagnostics/reports.d.ts +108 -0
  29. package/dist/diagnostics/reports.js +353 -0
  30. package/dist/diagnostics/types.d.ts +130 -0
  31. package/dist/diagnostics/types.js +13 -0
  32. package/dist/diagnostics/usage.d.ts +51 -0
  33. package/dist/diagnostics/usage.js +241 -0
  34. package/dist/hashing.d.ts +1 -0
  35. package/dist/hashing.js +3 -1
  36. package/dist/interfaces/FileReflectionProvenance.d.ts +4 -0
  37. package/dist/managers/EntryManager/EntryManager.d.ts +1 -0
  38. package/dist/managers/EntryManager/EntryManager.js +9 -0
  39. package/dist/pouchdb/StreamingFetch.d.ts +3 -1
  40. package/dist/pouchdb/StreamingFetch.js +3 -1
  41. package/dist/pouchdb/encryption.d.ts +3 -1
  42. package/dist/pouchdb/encryption.js +38 -30
  43. package/dist/pouchdb/pathIdentityCompatibility.d.ts +8 -0
  44. package/dist/pouchdb/pathIdentityCompatibility.js +64 -0
  45. package/dist/pouchdb/remoteFeatureCompatibility.d.ts +2 -0
  46. package/dist/pouchdb/remoteFeatureCompatibility.js +9 -1
  47. package/dist/replication/couchdb/LiveSyncReplicator.js +11 -0
  48. package/dist/replication/index.d.ts +1 -1
  49. package/dist/replication/index.js +2 -0
  50. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +2 -0
  51. package/dist/replication/journal/LiveSyncJournalReplicator.js +28 -6
  52. package/dist/replication/trystero/P2PRoomSessionOwner.js +18 -5
  53. package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
  54. package/dist/replication/trystero/useP2PReplicatorFeature.js +2 -1
  55. package/dist/serviceFeatures/offlineScanner.d.ts +12 -3
  56. package/dist/serviceFeatures/offlineScanner.js +4 -2
  57. package/dist/serviceFeatures/targetFilter.d.ts +1 -1
  58. package/dist/serviceFeatures/targetFilter.js +28 -17
  59. package/dist/serviceModules/Rebuilder.d.ts +5 -1
  60. package/dist/serviceModules/Rebuilder.js +85 -3
  61. package/dist/serviceModules/ServiceFileHandlerBase.d.ts +5 -2
  62. package/dist/serviceModules/ServiceFileHandlerBase.js +110 -49
  63. package/dist/services/base/IService.d.ts +2 -1
  64. package/dist/services/base/PathService.d.ts +3 -3
  65. package/dist/services/base/PathService.js +6 -3
  66. package/dist/settings.d.ts +2 -0
  67. package/dist/settings.js +3 -0
  68. package/dist/worker/bgWorker.js +1 -1
  69. package/docs/conflict-resolution.md +17 -8
  70. package/docs/database-diagnostics.md +24 -0
  71. package/docs/development.md +1 -0
  72. package/docs/proven-in-use.md +11 -1
  73. package/docs/unicode-path-identity.md +98 -0
  74. package/package.json +9 -1
@@ -0,0 +1,25 @@
1
+ function createLocalDiagnosticSource(database) {
2
+ return {
3
+ info: () => database.info(),
4
+ get: (id, options) => database.get(id, options),
5
+ async *enumerateIds() {
6
+ let previous;
7
+ while (true) {
8
+ const page = await database.allDocs({
9
+ limit: 256,
10
+ ...previous === void 0 ? {} : { startkey: previous, skip: 1 }
11
+ });
12
+ for (const row of page.rows) {
13
+ if (!row.value.deleted) yield row.id;
14
+ }
15
+ if (page.rows.length < 256) return;
16
+ const last = page.rows.at(-1).id;
17
+ if (last === previous) throw new Error("Document enumeration did not advance");
18
+ previous = last;
19
+ }
20
+ }
21
+ };
22
+ }
23
+ export {
24
+ createLocalDiagnosticSource
25
+ };
@@ -0,0 +1,108 @@
1
+ import type { DatabaseIntegrityResult, DiagnosticCoverage, DiagnosticError, DiagnosticIdentitySettings, DiagnosticNameMode, DiagnosticObservation, DiagnosticSourceName, DiagnosticStatus } from "./types.js";
2
+ import type { DatabaseUsageError, DatabaseUsageResult, DatabaseUsageStatus } from "./usage.js";
3
+ export interface DiagnosticNameMapper {
4
+ readonly mode: DiagnosticNameMode;
5
+ path(value: string): string;
6
+ id(value: string): string;
7
+ revision(value: string): string;
8
+ chunk(value: string): string;
9
+ }
10
+ export interface DiagnosticReportVersions {
11
+ commonlib?: string;
12
+ plugin?: string;
13
+ application?: string;
14
+ }
15
+ /** Creates run-local name aliases. Each name domain has an independent map. */
16
+ export declare function createDiagnosticNameMapper(mode: DiagnosticNameMode): DiagnosticNameMapper;
17
+ export interface ProjectedDiagnosticError {
18
+ source: DiagnosticError["source"];
19
+ reason: DiagnosticError["reason"];
20
+ id?: string;
21
+ revision?: string;
22
+ path?: string;
23
+ chunkId?: string;
24
+ }
25
+ export interface ProjectedDiagnosticObservation {
26
+ reference: string;
27
+ source: DiagnosticSourceName;
28
+ id: string;
29
+ revision?: string;
30
+ path: string;
31
+ expectedId?: string;
32
+ identity: DiagnosticObservation["identity"];
33
+ logicallyDeleted: boolean;
34
+ branch: DiagnosticObservation["branch"];
35
+ conflictCount: number;
36
+ pathIsNFC: boolean;
37
+ caseEquivalentPaths: boolean;
38
+ nfcEquivalentPaths: boolean;
39
+ otherSourceGroup: DiagnosticObservation["otherSourceGroup"];
40
+ vault: {
41
+ reference: string;
42
+ status: DiagnosticObservation["vault"]["status"];
43
+ reason?: DiagnosticObservation["vault"]["reason"];
44
+ };
45
+ content: {
46
+ status: DiagnosticObservation["content"]["status"];
47
+ reason?: DiagnosticObservation["content"]["reason"];
48
+ chunksRead: number;
49
+ unavailableChunks: {
50
+ id: string;
51
+ status: "missing" | "unreadable";
52
+ }[];
53
+ };
54
+ comparisons: {
55
+ against: string;
56
+ relation: DiagnosticObservation["comparisons"][number]["relation"];
57
+ }[];
58
+ }
59
+ export interface ProjectedDatabaseIntegrityReport {
60
+ schemaVersion: 1;
61
+ kind: "integrity";
62
+ status: DiagnosticStatus;
63
+ nameMode: DiagnosticNameMode;
64
+ versions: DiagnosticReportVersions;
65
+ settings: DiagnosticIdentitySettings;
66
+ coverage: Record<DiagnosticSourceName, DiagnosticCoverage>;
67
+ observations: ProjectedDiagnosticObservation[];
68
+ errors: ProjectedDiagnosticError[];
69
+ scope: DatabaseIntegrityResult["scope"];
70
+ }
71
+ export interface ProjectedDatabaseUsageRow {
72
+ title: string;
73
+ id: string;
74
+ path: string;
75
+ revision: string;
76
+ rev: string;
77
+ revHash: string;
78
+ uniqueChunkCount: number;
79
+ sharedChunkCount: number;
80
+ uniqueChunkSize: number;
81
+ sharedChunkSize: number;
82
+ }
83
+ export interface ProjectedDatabaseUsageError {
84
+ code: DatabaseUsageError["code"];
85
+ id?: string;
86
+ revision?: string;
87
+ }
88
+ export interface ProjectedDatabaseUsageReport {
89
+ version: 1;
90
+ kind: "usage";
91
+ status: DatabaseUsageStatus;
92
+ nameMode: DiagnosticNameMode;
93
+ versions: DiagnosticReportVersions;
94
+ changedDuringRun?: boolean;
95
+ rows: ProjectedDatabaseUsageRow[];
96
+ errors: ProjectedDatabaseUsageError[];
97
+ }
98
+ export type ProjectedDiagnosticReport = ProjectedDatabaseIntegrityReport | ProjectedDatabaseUsageReport;
99
+ /** Copies only the documented report fields and projects every stored name through the run mapper. */
100
+ export declare function projectIntegrityReport(result: DatabaseIntegrityResult, mapper: DiagnosticNameMapper, versions?: DiagnosticReportVersions): ProjectedDatabaseIntegrityReport;
101
+ /** Projects Usage rows and errors without carrying through extra analyser fields. */
102
+ export declare function projectUsageReport(result: DatabaseUsageResult, mapper: DiagnosticNameMapper, versions?: DiagnosticReportVersions): ProjectedDatabaseUsageReport;
103
+ /** Formats the legacy nine-column Usage export from an already projected report. */
104
+ export declare function formatUsageTSV(projected: ProjectedDatabaseUsageReport): string;
105
+ /** Serialises a projected report. Raw analyser values never enter this formatter. */
106
+ export declare function formatDiagnosticJSON(projected: ProjectedDiagnosticReport): string;
107
+ /** Renders the same projected integrity fields used by JSON, with user-controlled values in escaped code spans. */
108
+ export declare function formatIntegrityMarkdown(projected: ProjectedDatabaseIntegrityReport): string;
@@ -0,0 +1,353 @@
1
+ function createDomainMapper(prefix) {
2
+ const aliases = /* @__PURE__ */ new Map();
3
+ return (value) => {
4
+ const existing = aliases.get(value);
5
+ if (existing !== void 0) return existing;
6
+ const alias = `${prefix}-${String(aliases.size + 1).padStart(3, "0")}`;
7
+ aliases.set(value, alias);
8
+ return alias;
9
+ };
10
+ }
11
+ function createDiagnosticNameMapper(mode) {
12
+ if (mode === "original") {
13
+ const original = (value) => value;
14
+ return { mode, path: original, id: original, revision: original, chunk: original };
15
+ }
16
+ return {
17
+ mode,
18
+ path: createDomainMapper("file"),
19
+ id: createDomainMapper("document"),
20
+ revision: createDomainMapper("revision"),
21
+ chunk: createDomainMapper("chunk")
22
+ };
23
+ }
24
+ function copyVersions(versions) {
25
+ return {
26
+ ...versions.commonlib === void 0 ? {} : { commonlib: versions.commonlib },
27
+ ...versions.plugin === void 0 ? {} : { plugin: versions.plugin },
28
+ ...versions.application === void 0 ? {} : { application: versions.application }
29
+ };
30
+ }
31
+ function copyCoverage(coverage) {
32
+ return {
33
+ status: coverage.status,
34
+ documents: coverage.documents,
35
+ metadata: coverage.metadata,
36
+ excluded: coverage.excluded,
37
+ conflictBranches: coverage.conflictBranches,
38
+ contentReads: coverage.contentReads,
39
+ changedDuringRun: coverage.changedDuringRun
40
+ };
41
+ }
42
+ function projectIntegrityObservation(observation, mapper, projectReference) {
43
+ return {
44
+ reference: projectReference(observation.reference),
45
+ source: observation.source,
46
+ id: mapper.id(observation.id),
47
+ ...observation.revision === void 0 ? {} : { revision: mapper.revision(observation.revision) },
48
+ path: mapper.path(observation.path),
49
+ ...observation.expectedId === void 0 ? {} : { expectedId: mapper.id(observation.expectedId) },
50
+ identity: observation.identity,
51
+ logicallyDeleted: observation.logicallyDeleted,
52
+ branch: observation.branch,
53
+ conflictCount: observation.conflictCount,
54
+ pathIsNFC: observation.pathIsNFC,
55
+ caseEquivalentPaths: observation.caseEquivalentPaths,
56
+ nfcEquivalentPaths: observation.nfcEquivalentPaths,
57
+ otherSourceGroup: observation.otherSourceGroup,
58
+ vault: {
59
+ reference: projectReference(observation.vault.reference),
60
+ status: observation.vault.status,
61
+ ...observation.vault.reason === void 0 ? {} : { reason: observation.vault.reason }
62
+ },
63
+ content: {
64
+ status: observation.content.status,
65
+ ...observation.content.reason === void 0 ? {} : { reason: observation.content.reason },
66
+ chunksRead: observation.content.chunksRead,
67
+ unavailableChunks: observation.content.unavailableChunks.map((chunk) => ({
68
+ id: mapper.chunk(chunk.id),
69
+ status: chunk.status
70
+ }))
71
+ },
72
+ comparisons: observation.comparisons.map((comparison) => ({
73
+ against: projectReference(comparison.against),
74
+ relation: comparison.relation
75
+ }))
76
+ };
77
+ }
78
+ function projectIntegrityError(error, mapper) {
79
+ return {
80
+ source: error.source,
81
+ reason: error.reason,
82
+ ...error.id === void 0 ? {} : { id: mapper.id(error.id) },
83
+ ...error.revision === void 0 ? {} : { revision: mapper.revision(error.revision) },
84
+ ...error.path === void 0 ? {} : { path: mapper.path(error.path) },
85
+ ...error.chunkId === void 0 ? {} : { chunkId: mapper.chunk(error.chunkId) }
86
+ };
87
+ }
88
+ function projectIntegrityReport(result, mapper, versions = {}) {
89
+ const safeReference = /^((local|remote):[0-9]+|vault:(local|remote):[0-9]+)$/;
90
+ const referenceAliases = /* @__PURE__ */ new Map();
91
+ const projectReference = (value) => {
92
+ if (safeReference.test(value)) return value;
93
+ const existing = referenceAliases.get(value);
94
+ if (existing !== void 0) return existing;
95
+ const alias = `finding-${String(referenceAliases.size + 1).padStart(3, "0")}`;
96
+ referenceAliases.set(value, alias);
97
+ return alias;
98
+ };
99
+ return {
100
+ schemaVersion: 1,
101
+ kind: "integrity",
102
+ status: result.status,
103
+ nameMode: mapper.mode,
104
+ versions: copyVersions(versions),
105
+ settings: {
106
+ encrypt: result.settings.encrypt,
107
+ usePathObfuscation: result.settings.usePathObfuscation,
108
+ handleFilenameCaseSensitive: result.settings.handleFilenameCaseSensitive,
109
+ idDerivationVersion: result.settings.idDerivationVersion,
110
+ pathNormalizationVersion: result.settings.pathNormalizationVersion
111
+ },
112
+ coverage: {
113
+ local: copyCoverage(result.coverage.local),
114
+ remote: copyCoverage(result.coverage.remote)
115
+ },
116
+ observations: result.observations.map(
117
+ (observation) => projectIntegrityObservation(observation, mapper, projectReference)
118
+ ),
119
+ errors: result.errors.map((error) => projectIntegrityError(error, mapper)),
120
+ scope: {
121
+ metadata: "normal-files-and-current-conflicts",
122
+ content: "suspect-groups-only",
123
+ history: "not-checked",
124
+ atomicSnapshot: false
125
+ }
126
+ };
127
+ }
128
+ function isOrphanRow(row) {
129
+ return row.id === "__orphan" && row.path === "__orphan" && row.title === "__orphan";
130
+ }
131
+ function projectUsageRow(row, mapper) {
132
+ const statistics = {
133
+ uniqueChunkCount: row.uniqueChunkCount,
134
+ sharedChunkCount: row.sharedChunkCount,
135
+ uniqueChunkSize: row.uniqueChunkSize,
136
+ sharedChunkSize: row.sharedChunkSize
137
+ };
138
+ if (isOrphanRow(row)) {
139
+ return {
140
+ title: "__orphan",
141
+ id: "__orphan",
142
+ path: "__orphan",
143
+ revision: "1-xxxxx",
144
+ rev: "1",
145
+ revHash: "xxxxx",
146
+ ...statistics
147
+ };
148
+ }
149
+ const path = mapper.path(row.path);
150
+ const revision = mapper.revision(row.revision);
151
+ return {
152
+ title: `${path} (${revision})`,
153
+ id: mapper.id(row.id),
154
+ path,
155
+ revision,
156
+ rev: row.rev,
157
+ revHash: mapper.mode === "pseudonym" ? revision : row.revHash,
158
+ ...statistics
159
+ };
160
+ }
161
+ function projectUsageError(error, mapper) {
162
+ return {
163
+ code: error.code,
164
+ ...error.id === void 0 ? {} : { id: mapper.id(error.id) },
165
+ ...error.revision === void 0 ? {} : { revision: mapper.revision(error.revision) }
166
+ };
167
+ }
168
+ function projectUsageReport(result, mapper, versions = {}) {
169
+ return {
170
+ version: 1,
171
+ kind: "usage",
172
+ status: result.status,
173
+ nameMode: mapper.mode,
174
+ versions: copyVersions(versions),
175
+ ...result.changedDuringRun === void 0 ? {} : { changedDuringRun: result.changedDuringRun },
176
+ rows: result.rows.map((row) => projectUsageRow(row, mapper)),
177
+ errors: result.errors.map((error) => projectUsageError(error, mapper))
178
+ };
179
+ }
180
+ const USAGE_COLUMNS = [
181
+ "title",
182
+ "id",
183
+ "path",
184
+ "rev",
185
+ "revHash",
186
+ "uniqueChunkCount",
187
+ "sharedChunkCount",
188
+ "uniqueChunkSize",
189
+ "sharedChunkSize"
190
+ ];
191
+ const USAGE_HEADERS = [
192
+ "Title",
193
+ "Document ID",
194
+ "Path",
195
+ "Revision No",
196
+ "Revision Hash",
197
+ "Unique Chunk Count",
198
+ "Shared Chunk Count",
199
+ "Unique Chunk Size",
200
+ "Shared Chunk Size"
201
+ ];
202
+ function escapeTSVField(value) {
203
+ const text = String(value);
204
+ return /[\t\r\n"]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
205
+ }
206
+ function formatUsageTSV(projected) {
207
+ const lines = [USAGE_HEADERS.map(escapeTSVField).join(" ")];
208
+ for (const row of projected.rows) {
209
+ lines.push(USAGE_COLUMNS.map((column) => escapeTSVField(row[column])).join(" "));
210
+ }
211
+ return lines.join("\n");
212
+ }
213
+ function formatDiagnosticJSON(projected) {
214
+ return JSON.stringify(projected, null, 2);
215
+ }
216
+ function markdownCode(value) {
217
+ const text = value.replace(/\r\n?|\n|\t/g, " ").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
218
+ const backtickRuns = text.match(/`+/g) ?? [];
219
+ const fence = "`".repeat(Math.max(0, ...backtickRuns.map((run) => run.length)) + 1);
220
+ const padding = /^\s|\s$/.test(text) ? " " : "";
221
+ return `${fence}${padding}${text}${padding}${fence}`;
222
+ }
223
+ function renderMaybe(value) {
224
+ return value === void 0 ? "not recorded" : markdownCode(value);
225
+ }
226
+ function renderObservation(observation) {
227
+ const lines = [
228
+ `### ${markdownCode(observation.reference)}`,
229
+ `- Source: ${observation.source}`,
230
+ `- Path: ${markdownCode(observation.path)}`,
231
+ `- Document ID: ${markdownCode(observation.id)}`,
232
+ `- Revision: ${renderMaybe(observation.revision)}`,
233
+ `- Expected ID: ${renderMaybe(observation.expectedId)}`,
234
+ `- Identity: ${observation.identity}`,
235
+ `- Branch: ${observation.branch}`,
236
+ `- Logically deleted: ${observation.logicallyDeleted}`,
237
+ `- Conflict count: ${observation.conflictCount}`,
238
+ `- Path is NFC: ${observation.pathIsNFC}`,
239
+ `- Case-equivalent paths: ${observation.caseEquivalentPaths}`,
240
+ `- NFC-equivalent paths: ${observation.nfcEquivalentPaths}`,
241
+ `- Other inspected database group: ${observation.otherSourceGroup}`,
242
+ `- Vault content status: ${observation.vault.status}${observation.vault.reason === void 0 ? "" : ` (${observation.vault.reason})`}`,
243
+ `- Vault reference: ${markdownCode(observation.vault.reference)}`,
244
+ `- Content: ${observation.content.status}${observation.content.reason === void 0 ? "" : ` (${observation.content.reason})`}`,
245
+ `- Chunks read: ${observation.content.chunksRead}`
246
+ ];
247
+ if (observation.content.unavailableChunks.length > 0) {
248
+ lines.push(
249
+ `- Unavailable chunks: ${observation.content.unavailableChunks.map((chunk) => `${markdownCode(chunk.id)} (${chunk.status})`).join(", ")}`
250
+ );
251
+ }
252
+ if (observation.comparisons.length > 0) {
253
+ lines.push(
254
+ `- Comparisons: ${observation.comparisons.map((comparison) => `${markdownCode(comparison.against)}: ${comparison.relation}`).join("; ")}`
255
+ );
256
+ }
257
+ return lines;
258
+ }
259
+ function formatIntegrityMarkdown(projected) {
260
+ const observations = projected.observations;
261
+ const mismatchCount = observations.filter((observation) => observation.identity !== "consistent").length;
262
+ const conflictCount = observations.filter((observation) => observation.branch === "conflict").length;
263
+ const unavailableContentCount = observations.filter(
264
+ (observation) => observation.content.status === "missing" || observation.content.status === "unreadable"
265
+ ).length;
266
+ const unavailableChunkCount = observations.reduce(
267
+ (count, observation) => count + observation.content.unavailableChunks.length,
268
+ 0
269
+ );
270
+ const coverageRows = [
271
+ ["Local", projected.coverage.local],
272
+ ["Remote", projected.coverage.remote]
273
+ ];
274
+ const lines = [
275
+ "# Database integrity report",
276
+ "",
277
+ `Status: ${projected.status}`,
278
+ `Names: ${projected.nameMode}`,
279
+ "",
280
+ "A complete status means the declared inspection finished. It does not mean that every finding is healthy.",
281
+ "Equal content applies only within the inspected scope and is not deletion advice.",
282
+ "",
283
+ "## Versions",
284
+ "",
285
+ ...projected.versions.commonlib === void 0 ? [] : [`- Commonlib version: ${markdownCode(projected.versions.commonlib)}`],
286
+ ...projected.versions.plugin === void 0 ? [] : [`- Plug-in version: ${markdownCode(projected.versions.plugin)}`],
287
+ ...projected.versions.application === void 0 ? [] : [`- Application version: ${markdownCode(projected.versions.application)}`],
288
+ ...projected.versions.commonlib === void 0 && projected.versions.plugin === void 0 && projected.versions.application === void 0 ? ["No version information was provided."] : [],
289
+ "",
290
+ "## Settings",
291
+ "",
292
+ `- Encryption: ${projected.settings.encrypt ? "enabled" : "disabled"}`,
293
+ `- Path obfuscation: ${projected.settings.usePathObfuscation ? "enabled" : "disabled"}`,
294
+ `- Filename case sensitivity: ${projected.settings.handleFilenameCaseSensitive ? "enabled" : "disabled"}`,
295
+ `- ID derivation version: ${projected.settings.idDerivationVersion}`,
296
+ `- Path normalisation version: ${projected.settings.pathNormalizationVersion}`,
297
+ "",
298
+ "## Scope",
299
+ "",
300
+ "- Metadata: normal files and current conflict branches",
301
+ "- Content: suspect groups only",
302
+ "- History: not checked",
303
+ "- Atomic snapshot: no",
304
+ "",
305
+ "## Coverage",
306
+ "",
307
+ "| Source | Status | Documents | Metadata | Excluded | Conflict branches | Content reads | Changed during run |",
308
+ "| --- | --- | ---: | ---: | ---: | ---: | ---: | --- |"
309
+ ];
310
+ for (const [source, coverage] of coverageRows) {
311
+ lines.push(
312
+ `| ${source} | ${coverage.status} | ${coverage.documents} | ${coverage.metadata} | ${coverage.excluded} | ${coverage.conflictBranches} | ${coverage.contentReads} | ${coverage.changedDuringRun} |`
313
+ );
314
+ }
315
+ lines.push(
316
+ "",
317
+ "## Counts",
318
+ "",
319
+ `- Identity mismatches: ${mismatchCount}`,
320
+ `- Conflict observations: ${conflictCount}`,
321
+ `- Missing or unreadable content observations: ${unavailableContentCount}`,
322
+ `- Unavailable chunks: ${unavailableChunkCount}`,
323
+ `- Errors: ${projected.errors.length}`,
324
+ "",
325
+ "## Findings",
326
+ ""
327
+ );
328
+ if (observations.length === 0) lines.push("No observations were returned.");
329
+ for (const observation of observations) lines.push(...renderObservation(observation), "");
330
+ if (projected.errors.length > 0) {
331
+ lines.push("## Errors", "");
332
+ for (const error of projected.errors) {
333
+ const details = [
334
+ `source=${error.source}`,
335
+ `reason=${error.reason}`,
336
+ ...error.id === void 0 ? [] : [`id=${markdownCode(error.id)}`],
337
+ ...error.revision === void 0 ? [] : [`revision=${markdownCode(error.revision)}`],
338
+ ...error.path === void 0 ? [] : [`path=${markdownCode(error.path)}`],
339
+ ...error.chunkId === void 0 ? [] : [`chunkId=${markdownCode(error.chunkId)}`]
340
+ ];
341
+ lines.push(`- ${details.join(", ")}`);
342
+ }
343
+ }
344
+ return lines.join("\n").trimEnd();
345
+ }
346
+ export {
347
+ createDiagnosticNameMapper,
348
+ formatDiagnosticJSON,
349
+ formatIntegrityMarkdown,
350
+ formatUsageTSV,
351
+ projectIntegrityReport,
352
+ projectUsageReport
353
+ };
@@ -0,0 +1,130 @@
1
+ import type { EntryDoc, MetaEntry } from "../common/types.js";
2
+ import type { IPathService } from "../services/base/IService.js";
3
+ export type DiagnosticStatus = "complete" | "partial" | "failed" | "cancelled";
4
+ export type DiagnosticSourceName = "local" | "remote";
5
+ export type DiagnosticNameMode = "original" | "pseudonym";
6
+ export type DiagnosticReason = "source-unavailable" | "unsupported-remote" | "document-unreadable" | "revision-unavailable" | "invalid-metadata" | "unsupported-content" | "chunk-missing" | "chunk-unreadable" | "security-seed-unavailable" | "content-unreadable" | "source-changed" | "storage-changed" | "configuration-changed";
7
+ export interface DiagnosticReadOptions {
8
+ rev?: string;
9
+ conflicts?: boolean;
10
+ revs?: boolean;
11
+ revs_info?: boolean;
12
+ }
13
+ /** Only these reads are available to an analyser; it cannot initialise or repair a database. */
14
+ export interface DiagnosticDatabaseSource {
15
+ info(): Promise<{
16
+ update_seq?: string | number;
17
+ }>;
18
+ enumerateIds(): AsyncIterable<string>;
19
+ get(id: string, options?: DiagnosticReadOptions): Promise<EntryDoc>;
20
+ }
21
+ export interface DiagnosticError {
22
+ source: DiagnosticSourceName | "storage" | "configuration";
23
+ reason: DiagnosticReason;
24
+ id?: string;
25
+ revision?: string;
26
+ path?: string;
27
+ chunkId?: string;
28
+ }
29
+ export type DiagnosticContent = {
30
+ status: "available";
31
+ bytes: Uint8Array<ArrayBuffer>;
32
+ reason?: never;
33
+ } | {
34
+ status: "missing" | "unreadable" | "not-checked";
35
+ reason?: DiagnosticReason;
36
+ };
37
+ export interface DiagnosticCoverage {
38
+ status: "complete" | "partial" | "failed" | "not-checked";
39
+ documents: number;
40
+ metadata: number;
41
+ excluded: number;
42
+ conflictBranches: number;
43
+ contentReads: number;
44
+ changedDuringRun: boolean;
45
+ }
46
+ export interface DiagnosticObservation {
47
+ reference: string;
48
+ source: DiagnosticSourceName;
49
+ id: string;
50
+ revision?: string;
51
+ /** Exact stored spelling, retained independently of the host's identity normalisation. */
52
+ path: string;
53
+ expectedId?: string;
54
+ identity: "consistent" | "document-id-mismatch" | "namespace-mismatch";
55
+ logicallyDeleted: boolean;
56
+ branch: "winner" | "conflict";
57
+ conflictCount: number;
58
+ pathIsNFC: boolean;
59
+ caseEquivalentPaths: boolean;
60
+ nfcEquivalentPaths: boolean;
61
+ /** Presence of this identity/path group in the other inspected database. */
62
+ otherSourceGroup: "present" | "missing" | "not-checked";
63
+ vault: {
64
+ reference: string;
65
+ status: DiagnosticContent["status"];
66
+ reason?: DiagnosticReason;
67
+ };
68
+ content: {
69
+ status: DiagnosticContent["status"];
70
+ reason?: DiagnosticReason;
71
+ chunksRead: number;
72
+ unavailableChunks: {
73
+ id: string;
74
+ status: "missing" | "unreadable";
75
+ }[];
76
+ };
77
+ comparisons: {
78
+ against: string;
79
+ relation: "equal" | "different" | "unavailable" | "not-checked";
80
+ }[];
81
+ }
82
+ /** A whitelist: connection details, keys, and passphrases have no report field. */
83
+ export interface DiagnosticIdentitySettings {
84
+ encrypt: boolean;
85
+ usePathObfuscation: boolean;
86
+ handleFilenameCaseSensitive: boolean;
87
+ idDerivationVersion: number;
88
+ pathNormalizationVersion: number;
89
+ }
90
+ export interface DatabaseIntegrityResult {
91
+ schemaVersion: 1;
92
+ kind: "integrity";
93
+ status: DiagnosticStatus;
94
+ settings: DiagnosticIdentitySettings;
95
+ coverage: Record<DiagnosticSourceName, DiagnosticCoverage>;
96
+ observations: DiagnosticObservation[];
97
+ errors: DiagnosticError[];
98
+ /** Scope declarations are part of every report, including a successful one. */
99
+ scope: {
100
+ metadata: "normal-files-and-current-conflicts";
101
+ content: "suspect-groups-only";
102
+ history: "not-checked";
103
+ atomicSnapshot: false;
104
+ };
105
+ }
106
+ export interface DatabaseIntegrityOptions {
107
+ local: DiagnosticDatabaseSource;
108
+ remote?: () => Promise<DiagnosticDatabaseSource & {
109
+ close(): Promise<void>;
110
+ }>;
111
+ remoteUnavailableReason?: "unsupported-remote" | "source-unavailable";
112
+ path: Pick<IPathService, "getPath" | "path2id">;
113
+ settings: DiagnosticIdentitySettings;
114
+ readStorage(path: string): Promise<DiagnosticContent>;
115
+ /** Captures profile and relevant settings identity without exporting it. */
116
+ isConfigurationCurrent(): boolean;
117
+ signal?: AbortSignal;
118
+ onProgress?(progress: {
119
+ phase: "metadata" | "content";
120
+ processed: number;
121
+ }): void;
122
+ }
123
+ export declare class DiagnosticReadError extends Error {
124
+ readonly reason: DiagnosticReason;
125
+ readonly missing: boolean;
126
+ constructor(reason: DiagnosticReason, missing?: boolean);
127
+ }
128
+ export type InspectedMetadata = MetaEntry & {
129
+ _conflicts?: string[];
130
+ };
@@ -0,0 +1,13 @@
1
+ class DiagnosticReadError extends Error {
2
+ constructor(reason, missing = false) {
3
+ super(reason);
4
+ this.reason = reason;
5
+ this.missing = missing;
6
+ this.name = "DiagnosticReadError";
7
+ }
8
+ reason;
9
+ missing;
10
+ }
11
+ export {
12
+ DiagnosticReadError
13
+ };
@@ -0,0 +1,51 @@
1
+ export interface DatabaseUsageGetOptions {
2
+ revs?: boolean;
3
+ revs_info?: boolean;
4
+ conflicts?: boolean;
5
+ rev?: string;
6
+ }
7
+ export interface DatabaseUsageSource {
8
+ info(): Promise<{
9
+ update_seq?: string | number;
10
+ }>;
11
+ enumerateIds(): AsyncIterable<string>;
12
+ get(id: string, options?: DatabaseUsageGetOptions): Promise<object | null | undefined>;
13
+ }
14
+ export interface DatabaseUsageProgress {
15
+ idsProcessed: number;
16
+ revisionsProcessed: number;
17
+ }
18
+ export type DatabaseUsageStatus = "complete" | "partial" | "failed" | "cancelled";
19
+ export type DatabaseUsageErrorCode = "start_info_read_failed" | "start_update_seq_unavailable" | "end_info_read_failed" | "end_update_seq_unavailable" | "configuration_changed" | "id_enumeration_failed" | "revision_list_read_failed" | "revision_not_found" | "revision_read_failed" | "revision_document_invalid";
20
+ export interface DatabaseUsageError {
21
+ code: DatabaseUsageErrorCode;
22
+ id?: string;
23
+ revision?: string;
24
+ }
25
+ export interface DatabaseUsageRow {
26
+ title: string;
27
+ id: string;
28
+ path: string;
29
+ revision: string;
30
+ rev: string;
31
+ revHash: string;
32
+ uniqueChunkCount: number;
33
+ sharedChunkCount: number;
34
+ uniqueChunkSize: number;
35
+ sharedChunkSize: number;
36
+ }
37
+ export interface DatabaseUsageResult {
38
+ version: 1;
39
+ kind: "usage";
40
+ status: DatabaseUsageStatus;
41
+ startUpdateSeq?: string | number;
42
+ endUpdateSeq?: string | number;
43
+ changedDuringRun?: boolean;
44
+ rows: DatabaseUsageRow[];
45
+ errors: DatabaseUsageError[];
46
+ }
47
+ export declare function analyseDatabaseUsage(options: {
48
+ source: DatabaseUsageSource;
49
+ signal?: AbortSignal;
50
+ onProgress?: (progress: DatabaseUsageProgress) => void;
51
+ }): Promise<DatabaseUsageResult>;